+ {/* Legend */}
+
+ {Object.entries(CATEGORY_COLORS).map(([key, colors]) => (
+
+ ))}
+
-
- {/* Keyboard layout */}
-
- {KEYBOARD_ROWS.map((row, rowIndex) => (
-
- {row.map(renderKey)}
-
- ))}
-
-
- {/* Stats */}
-
-
- {Object.keys(keyboardShortcuts).length} {' '}
- shortcuts configured
-
-
- {Object.keys(keyToShortcuts).length} keys
- in use
-
-
-
- {KEYBOARD_ROWS.flat().length - Object.keys(keyToShortcuts).length}
- {' '}
- keys available
-
+
-
+
+ {/* Keyboard layout */}
+
+ {KEYBOARD_ROWS.map((row, rowIndex) => (
+
+ {row.map(renderKey)}
+
+ ))}
+
+
+ {/* Stats */}
+
+
+ {Object.keys(keyboardShortcuts).length} {' '}
+ shortcuts configured
+
+
+ {Object.keys(keyToShortcuts).length} keys in
+ use
+
+
+
+ {KEYBOARD_ROWS.flat().length - Object.keys(keyToShortcuts).length}
+ {' '}
+ keys available
+
+
+
);
}
@@ -508,196 +506,194 @@ export function ShortcutReferencePanel({ editable = false }: ShortcutReferencePa
};
return (
-
-
- {editable && (
-
- resetKeyboardShortcuts()}
- className="gap-2 text-xs"
- data-testid="reset-all-shortcuts-button"
- >
-
- Reset All to Defaults
-
-
- )}
- {Object.entries(groupedShortcuts).map(([category, shortcuts]) => {
- const colors = CATEGORY_COLORS[category as keyof typeof CATEGORY_COLORS];
- return (
-
-
{colors.label}
-
- {shortcuts.map(({ key, label, value }) => {
- const isModified = mergedShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key];
- const isEditing = editingShortcut === key;
+
+ {editable && (
+
+ resetKeyboardShortcuts()}
+ className="gap-2 text-xs"
+ data-testid="reset-all-shortcuts-button"
+ >
+
+ Reset All to Defaults
+
+
+ )}
+ {Object.entries(groupedShortcuts).map(([category, shortcuts]) => {
+ const colors = CATEGORY_COLORS[category as keyof typeof CATEGORY_COLORS];
+ return (
+
+
{colors.label}
+
+ {shortcuts.map(({ key, label, value }) => {
+ const isModified = mergedShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key];
+ const isEditing = editingShortcut === key;
- return (
-
editable && !isEditing && handleStartEdit(key)}
- data-testid={`shortcut-row-${key}`}
- >
-
{label}
-
- {isEditing ? (
-
e.stopPropagation()}
- >
- {/* Modifier checkboxes */}
-
-
-
- handleModifierChange('cmdCtrl', !!checked, key)
- }
- className="h-3.5 w-3.5"
- />
-
- {isMac ? '⌘' : 'Ctrl'}
-
-
-
-
- handleModifierChange('alt', !!checked, key)
- }
- className="h-3.5 w-3.5"
- />
-
- {isMac ? '⌥' : 'Alt'}
-
-
-
-
- handleModifierChange('shift', !!checked, key)
- }
- className="h-3.5 w-3.5"
- />
-
- ⇧
-
-
+ return (
+
editable && !isEditing && handleStartEdit(key)}
+ data-testid={`shortcut-row-${key}`}
+ >
+
{label}
+
+ {isEditing ? (
+
e.stopPropagation()}
+ >
+ {/* Modifier checkboxes */}
+
+
+
+ handleModifierChange('cmdCtrl', !!checked, key)
+ }
+ className="h-3.5 w-3.5"
+ />
+
+ {isMac ? '⌘' : 'Ctrl'}
+
+
+
+
+ handleModifierChange('alt', !!checked, key)
+ }
+ className="h-3.5 w-3.5"
+ />
+
+ {isMac ? '⌥' : 'Alt'}
+
+
+
+
+ handleModifierChange('shift', !!checked, key)
+ }
+ className="h-3.5 w-3.5"
+ />
+
+ ⇧
+
-
+
-
handleKeyChange(e.target.value, key)}
- onKeyDown={handleKeyDown}
- className={cn(
- 'w-12 h-7 text-center font-mono text-xs uppercase',
- shortcutError && 'border-red-500 focus-visible:ring-red-500'
- )}
- placeholder="Key"
- maxLength={1}
- autoFocus
- data-testid={`edit-shortcut-input-${key}`}
- />
-
{
- e.stopPropagation();
- handleSaveShortcut();
- }}
- disabled={!!shortcutError || !keyValue}
- data-testid={`save-shortcut-${key}`}
- >
-
-
-
{
- e.stopPropagation();
- handleCancelEdit();
- }}
- data-testid={`cancel-shortcut-${key}`}
- >
-
-
- ) : (
- <>
-
- {formatShortcut(value, true)}
-
- {isModified && editable && (
-
-
- {
- e.stopPropagation();
- handleResetShortcut(key);
- }}
- data-testid={`reset-shortcut-${key}`}
- >
-
-
-
-
- Reset to default ({DEFAULT_KEYBOARD_SHORTCUTS[key]})
-
-
+
+
+
handleKeyChange(e.target.value, key)}
+ onKeyDown={handleKeyDown}
+ className={cn(
+ 'w-12 h-7 text-center font-mono text-xs uppercase',
+ shortcutError && 'border-red-500 focus-visible:ring-red-500'
)}
- {isModified && !editable && (
-
+ placeholder="Key"
+ maxLength={1}
+ autoFocus
+ data-testid={`edit-shortcut-input-${key}`}
+ />
+
{
+ e.stopPropagation();
+ handleSaveShortcut();
+ }}
+ disabled={!!shortcutError || !keyValue}
+ data-testid={`save-shortcut-${key}`}
+ >
+
+
+
{
+ e.stopPropagation();
+ handleCancelEdit();
+ }}
+ data-testid={`cancel-shortcut-${key}`}
+ >
+
+
+
+ ) : (
+ <>
+
- )}
- >
- )}
-
+ >
+ {formatShortcut(value, true)}
+
+ {isModified && editable && (
+
+
+ {
+ e.stopPropagation();
+ handleResetShortcut(key);
+ }}
+ data-testid={`reset-shortcut-${key}`}
+ >
+
+
+
+
+ Reset to default ({DEFAULT_KEYBOARD_SHORTCUTS[key]})
+
+
+ )}
+ {isModified && !editable && (
+
+ )}
+ {editable && !isModified && (
+
+ )}
+ >
+ )}
- );
- })}
-
- {editingShortcut &&
- shortcutError &&
- SHORTCUT_CATEGORIES[editingShortcut] === category && (
-
{shortcutError}
- )}
+
+ );
+ })}
- );
- })}
-
-
+ {editingShortcut &&
+ shortcutError &&
+ SHORTCUT_CATEGORIES[editingShortcut] === category && (
+
{shortcutError}
+ )}
+
+ );
+ })}
+
);
}
diff --git a/apps/ui/src/components/views/board-view/board-controls.tsx b/apps/ui/src/components/views/board-view/board-controls.tsx
index 8584bbdb..2baa7ceb 100644
--- a/apps/ui/src/components/views/board-view/board-controls.tsx
+++ b/apps/ui/src/components/views/board-view/board-controls.tsx
@@ -1,4 +1,4 @@
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { ImageIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
@@ -18,24 +18,22 @@ export function BoardControls({ isMounted, onShowBoardBackground }: BoardControl
);
return (
-
-
- {/* Board Background Button */}
-
-
-
-
-
-
-
- Board Background Settings
-
-
-
-
+
+ {/* Board Background Button */}
+
+
+
+
+
+
+
+ Board Background Settings
+
+
+
);
}
diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/card-badges.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/card-badges.tsx
index 4563bc06..90b709c2 100644
--- a/apps/ui/src/components/views/board-view/components/kanban-card/card-badges.tsx
+++ b/apps/ui/src/components/views/board-view/components/kanban-card/card-badges.tsx
@@ -2,7 +2,7 @@
import { memo, useEffect, useMemo, useState } from 'react';
import { Feature, useAppStore } from '@/store/app-store';
import { cn } from '@/lib/utils';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { AlertCircle, Lock, Hand, Sparkles, SkipForward } from 'lucide-react';
import { getBlockingDependencies } from '@automaker/dependency-resolver';
import { useShallow } from 'zustand/react/shallow';
@@ -28,24 +28,22 @@ export const CardBadges = memo(function CardBadges({ feature }: CardBadgesProps)
return (
{/* Error badge */}
-
-
-
-
-
-
- {feature.error}
-
-
-
+
+
+
+
+
+ {feature.error}
+
+
);
});
@@ -138,147 +136,137 @@ export const PriorityBadges = memo(function PriorityBadges({
{/* Priority badge */}
{feature.priority && (
-
-
-
-
-
- {feature.priority === 1 ? 'H' : feature.priority === 2 ? 'M' : 'L'}
-
-
-
-
-
- {feature.priority === 1
- ? 'High Priority'
- : feature.priority === 2
- ? 'Medium Priority'
- : 'Low Priority'}
-
-
-
-
+
+
+
+
+ {feature.priority === 1 ? 'H' : feature.priority === 2 ? 'M' : 'L'}
+
+
+
+
+
+ {feature.priority === 1
+ ? 'High Priority'
+ : feature.priority === 2
+ ? 'Medium Priority'
+ : 'Low Priority'}
+
+
+
)}
{/* Manual verification badge */}
{showManualVerification && (
-
-
-
-
-
-
-
-
- Manual verification required
-
-
-
+
+
+
+
+
+
+
+ Manual verification required
+
+
)}
{/* Blocked badge */}
{isBlocked && (
-
-
-
-
-
-
-
-
-
- Blocked by {blockingDependencies.length} incomplete{' '}
- {blockingDependencies.length === 1 ? 'dependency' : 'dependencies'}
-
-
- {blockingDependencies
- .map((depId) => {
- const dep = features.find((f) => f.id === depId);
- return dep?.description || depId;
- })
- .join(', ')}
-
-
-
-
+
+
+
+
+
+
+
+
+ Blocked by {blockingDependencies.length} incomplete{' '}
+ {blockingDependencies.length === 1 ? 'dependency' : 'dependencies'}
+
+
+ {blockingDependencies
+ .map((depId) => {
+ const dep = features.find((f) => f.id === depId);
+ return dep?.description || depId;
+ })
+ .join(', ')}
+
+
+
)}
{/* Just Finished badge */}
{isJustFinished && (
-
-
-
-
-
-
-
-
- Agent just finished working on this feature
-
-
-
+
+
+
+
+
+
+
+ Agent just finished working on this feature
+
+
)}
{/* Pipeline exclusion badge */}
{hasPipelineExclusions && (
-
-
-
-
-
-
-
-
-
- {allPipelinesExcluded
- ? 'All pipelines skipped'
- : `${excludedStepCount} of ${totalPipelineSteps} pipeline${totalPipelineSteps !== 1 ? 's' : ''} skipped`}
-
-
- {allPipelinesExcluded
- ? 'This feature will skip all custom pipeline steps'
- : 'Some custom pipeline steps will be skipped for this feature'}
-
-
-
-
+
+
+
+
+
+
+
+
+ {allPipelinesExcluded
+ ? 'All pipelines skipped'
+ : `${excludedStepCount} of ${totalPipelineSteps} pipeline${totalPipelineSteps !== 1 ? 's' : ''} skipped`}
+
+
+ {allPipelinesExcluded
+ ? 'This feature will skip all custom pipeline steps'
+ : 'Some custom pipeline steps will be skipped for this feature'}
+
+
+
)}
);
diff --git a/apps/ui/src/components/views/board-view/components/kanban-column.tsx b/apps/ui/src/components/views/board-view/components/kanban-column.tsx
index 1fc1029b..a280c955 100644
--- a/apps/ui/src/components/views/board-view/components/kanban-column.tsx
+++ b/apps/ui/src/components/views/board-view/components/kanban-column.tsx
@@ -78,7 +78,9 @@ export const KanbanColumn = memo(function KanbanColumn({
)}
>
-
{title}
+
+ {title}
+
{headerAction}
{count}
diff --git a/apps/ui/src/components/views/board-view/components/list-view/list-header.tsx b/apps/ui/src/components/views/board-view/components/list-view/list-header.tsx
index aad969b6..1c874caf 100644
--- a/apps/ui/src/components/views/board-view/components/list-view/list-header.tsx
+++ b/apps/ui/src/components/views/board-view/components/list-view/list-header.tsx
@@ -132,7 +132,7 @@ const SortableColumnHeader = memo(function SortableColumnHeader({
)}
data-testid={`list-header-${column.id}`}
>
- {column.label}
+ {column.label}
);
@@ -156,7 +156,7 @@ const StaticColumnHeader = memo(function StaticColumnHeader({ column }: { column
)}
data-testid={`list-header-${column.id}`}
>
-
{column.label}
+
{column.label}
);
});
diff --git a/apps/ui/src/components/views/board-view/components/list-view/list-row.tsx b/apps/ui/src/components/views/board-view/components/list-view/list-row.tsx
index a3d10eb7..32b0f445 100644
--- a/apps/ui/src/components/views/board-view/components/list-view/list-row.tsx
+++ b/apps/ui/src/components/views/board-view/components/list-view/list-row.tsx
@@ -3,7 +3,7 @@
// @ts-nocheck
import { memo, useCallback, useState, useEffect } from 'react';
import { cn } from '@/lib/utils';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { AlertCircle, Lock, Hand, Sparkles, FileText } from 'lucide-react';
import type { Feature } from '@/store/app-store';
import { RowActions, type RowActionHandlers } from './row-actions';
@@ -149,29 +149,27 @@ const IndicatorBadges = memo(function IndicatorBadges({
return (
-
- {badges.map((badge) => (
-
-
-
-
-
-
-
- {badge.tooltip}
-
-
- ))}
-
+ {badges.map((badge) => (
+
+
+
+
+
+
+
+ {badge.tooltip}
+
+
+ ))}
);
});
diff --git a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
index 490f6a5b..6fa66061 100644
--- a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
+++ b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
@@ -50,7 +50,7 @@ import {
} from '../shared';
import type { WorkMode } from '../shared';
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
getAncestors,
formatAncestorContextForPrompt,
@@ -532,26 +532,24 @@ export function AddFeatureDialog({
AI & Execution
-
-
-
- {
- onOpenChange(false);
- navigate({ to: '/settings', search: { view: 'defaults' } });
- }}
- className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
- >
-
- Edit Defaults
-
-
-
- Change default model and planning settings for new features
-
-
-
+
+
+ {
+ onOpenChange(false);
+ navigate({ to: '/settings', search: { view: 'defaults' } });
+ }}
+ className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
+ >
+
+ Edit Defaults
+
+
+
+ Change default model and planning settings for new features
+
+
diff --git a/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx
index 34baae2e..61a20878 100644
--- a/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx
+++ b/apps/ui/src/components/views/board-view/dialogs/edit-feature-dialog.tsx
@@ -41,7 +41,7 @@ import {
} from '../shared';
import type { WorkMode } from '../shared';
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { DependencyTreeDialog } from './dependency-tree-dialog';
import { supportsReasoningEffort } from '@automaker/types';
@@ -424,26 +424,24 @@ export function EditFeatureDialog({
AI & Execution
-
-
-
- {
- onClose();
- navigate({ to: '/settings', search: { view: 'defaults' } });
- }}
- className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
- >
-
- Edit Defaults
-
-
-
- Change default model and planning settings for new features
-
-
-
+
+
+ {
+ onClose();
+ navigate({ to: '/settings', search: { view: 'defaults' } });
+ }}
+ className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
+ >
+
+ Edit Defaults
+
+
+
+ Change default model and planning settings for new features
+
+
diff --git a/apps/ui/src/components/views/board-view/kanban-board.tsx b/apps/ui/src/components/views/board-view/kanban-board.tsx
index 8314e74f..9da06723 100644
--- a/apps/ui/src/components/views/board-view/kanban-board.tsx
+++ b/apps/ui/src/components/views/board-view/kanban-board.tsx
@@ -12,7 +12,8 @@ import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
import { Button } from '@/components/ui/button';
import { KanbanColumn, KanbanCard, EmptyStateCard } from './components';
import { Feature, useAppStore, formatShortcut } from '@/store/app-store';
-import { Archive, Settings2, CheckSquare, GripVertical, Plus } from 'lucide-react';
+import { Archive, Settings2, CheckSquare, GripVertical, Plus, CheckCircle2 } from 'lucide-react';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useResponsiveKanban } from '@/hooks/use-responsive-kanban';
import { getColumnsWithPipeline, type ColumnId } from './constants';
import type { PipelineConfig } from '@automaker/types';
@@ -359,32 +360,44 @@ export function KanbanBoard({
column.id === 'verified' ? (
{columnFeatures.length > 0 && (
-
-
- Complete All
-
+
+
+
+
+
+
+
+ Complete All
+
+
)}
-
-
- {completedCount > 0 && (
-
- {completedCount > 99 ? '99+' : completedCount}
-
- )}
-
+
+
+
+
+ {completedCount > 0 && (
+
+ {completedCount > 99 ? '99+' : completedCount}
+
+ )}
+
+
+
+ Completed Features ({completedCount})
+
+
) : column.id === 'backlog' ? (
diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/tooltip-wrapper.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/tooltip-wrapper.tsx
index b1d9d8e7..40988894 100644
--- a/apps/ui/src/components/views/board-view/worktree-panel/components/tooltip-wrapper.tsx
+++ b/apps/ui/src/components/views/board-view/worktree-panel/components/tooltip-wrapper.tsx
@@ -1,5 +1,5 @@
import type { ReactElement, ReactNode } from 'react';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
interface TooltipWrapperProps {
/** The element to wrap with a tooltip */
@@ -29,16 +29,14 @@ export function TooltipWrapper({
}
return (
-
-
-
- {/* The div wrapper is necessary for tooltips to work on disabled elements */}
- {children}
-
-
- {tooltipContent}
-
-
-
+
+
+ {/* The div wrapper is necessary for tooltips to work on disabled elements */}
+ {children}
+
+
+ {tooltipContent}
+
+
);
}
diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown-item.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown-item.tsx
index 8549c40b..2fb79aed 100644
--- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown-item.tsx
+++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown-item.tsx
@@ -1,5 +1,5 @@
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Check, CircleDot, Globe, GitPullRequest, FlaskConical } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
@@ -101,14 +101,12 @@ export function WorktreeDropdownItem({
{/* Branch name with optional tooltip */}
{isBranchNameTruncated ? (
-
-
- {branchNameElement}
-
- {worktree.branch}
-
-
-
+
+ {branchNameElement}
+
+ {worktree.branch}
+
+
) : (
branchNameElement
)}
diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx
index c2bf8ac5..fd1c2ba3 100644
--- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx
+++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx
@@ -8,7 +8,7 @@ import {
DropdownMenuTrigger,
DropdownMenuGroup,
} from '@/components/ui/dropdown-menu';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
GitBranch,
ChevronDown,
@@ -335,14 +335,12 @@ export function WorktreeDropdown({
const dropdownTrigger =
{triggerButton} ;
const triggerWithTooltip = isBranchNameTruncated ? (
-
-
- {dropdownTrigger}
-
- {displayBranch}
-
-
-
+
+ {dropdownTrigger}
+
+ {displayBranch}
+
+
) : (
dropdownTrigger
);
diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
index a4722406..9d508ed3 100644
--- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
+++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
@@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button';
import { Globe, CircleDot, GitPullRequest } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useDroppable } from '@dnd-kit/core';
import type {
WorktreeInfo,
@@ -271,29 +271,27 @@ export function WorktreeTab({
)}
{hasChanges && (
-
-
-
-
-
- {changedFilesCount ?? '!'}
-
-
-
-
- {changedFilesCount ?? 'Some'} uncommitted file
- {changedFilesCount !== 1 ? 's' : ''}
-
-
-
-
+
+
+
+
+ {changedFilesCount ?? '!'}
+
+
+
+
+ {changedFilesCount ?? 'Some'} uncommitted file
+ {changedFilesCount !== 1 ? 's' : ''}
+
+
+
)}
{prBadge}
@@ -340,78 +338,72 @@ export function WorktreeTab({
)}
{hasChanges && (
-
-
-
-
-
- {changedFilesCount ?? '!'}
-
-
-
-
- {changedFilesCount ?? 'Some'} uncommitted file
- {changedFilesCount !== 1 ? 's' : ''}
-
-
-
-
+
+
+
+
+ {changedFilesCount ?? '!'}
+
+
+
+
+ {changedFilesCount ?? 'Some'} uncommitted file
+ {changedFilesCount !== 1 ? 's' : ''}
+
+
+
)}
{prBadge}
)}
{isDevServerRunning && (
-
-
-
- onOpenDevServerUrl(worktree)}
- aria-label={`Open dev server on port ${devServerInfo?.port} in browser`}
- >
-
-
-
-
- Open dev server (:{devServerInfo?.port})
-
-
-
+
+
+ onOpenDevServerUrl(worktree)}
+ aria-label={`Open dev server on port ${devServerInfo?.port} in browser`}
+ >
+
+
+
+
+ Open dev server (:{devServerInfo?.port})
+
+
)}
{isAutoModeRunning && (
-
-
-
-
-
-
-
-
- Auto Mode Running
-
-
-
+
+
+
+
+
+
+
+ Auto Mode Running
+
+
)}
-
-
- {/* Zoom controls */}
-
-
- zoomIn({ duration: 200 })}
- >
-
-
-
- Zoom In
-
+
+ {/* Zoom controls */}
+
+
+ zoomIn({ duration: 200 })}
+ >
+
+
+
+ Zoom In
+
-
-
- zoomOut({ duration: 200 })}
- >
-
-
-
- Zoom Out
-
+
+
+ zoomOut({ duration: 200 })}
+ >
+
+
+
+ Zoom Out
+
-
-
- fitView({ padding: 0.2, duration: 300 })}
- >
-
-
-
- Fit View
-
+
+
+ fitView({ padding: 0.2, duration: 300 })}
+ >
+
+
+
+ Fit View
+
-
+
- {/* Layout controls */}
-
-
- onRunLayout('LR')}
- >
-
-
-
- Horizontal Layout
-
+ {/* Layout controls */}
+
+
+ onRunLayout('LR')}
+ >
+
+
+
+ Horizontal Layout
+
-
-
- onRunLayout('TB')}
- >
-
-
-
- Vertical Layout
-
+
+
+ onRunLayout('TB')}
+ >
+
+
+
+ Vertical Layout
+
-
+
- {/* Lock toggle */}
-
-
-
- {isLocked ? : }
-
-
- {isLocked ? 'Unlock Nodes' : 'Lock Nodes'}
-
-
-
+ {/* Lock toggle */}
+
+
+
+ {isLocked ? : }
+
+
+ {isLocked ? 'Unlock Nodes' : 'Lock Nodes'}
+
+
);
}
diff --git a/apps/ui/src/components/views/graph-view/components/graph-filter-controls.tsx b/apps/ui/src/components/views/graph-view/components/graph-filter-controls.tsx
index 35af041b..16c9abf6 100644
--- a/apps/ui/src/components/views/graph-view/components/graph-filter-controls.tsx
+++ b/apps/ui/src/components/views/graph-view/components/graph-filter-controls.tsx
@@ -4,7 +4,7 @@ import { Checkbox } from '@/components/ui/checkbox';
import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/input';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
Filter,
X,
@@ -115,248 +115,252 @@ export function GraphFilterControls({
return (
-
-
- {/* Search Input */}
-
-
- onSearchQueryChange(e.target.value)}
- className="h-8 w-48 pl-8 pr-8 text-sm bg-background/50"
- />
- {searchQuery && (
- onSearchQueryChange('')}
- className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
- aria-label="Clear search"
- >
-
-
- )}
-
-
- {/* Divider */}
-
-
- {/* Category Filter Dropdown */}
-
-
-
-
- 0 && 'bg-brand-500/20 text-brand-500'
- )}
- >
-
- {categoryButtonLabel}
-
-
-
-
- Filter by Category
-
-
-
-
- Categories
-
-
- {/* Select All option */}
-
- 0
- }
- onCheckedChange={handleSelectAllCategories}
- />
-
- {selectedCategories.length === availableCategories.length
- ? 'Deselect All'
- : 'Select All'}
-
-
-
-
-
- {/* Category list */}
-
- {availableCategories.length === 0 ? (
-
- No categories available
-
- ) : (
- availableCategories.map((category) => (
-
handleCategoryToggle(category)}
- >
- handleCategoryToggle(category)}
- />
- {category}
-
- ))
- )}
-
-
-
-
-
- {/* Status Filter Dropdown */}
-
-
-
-
- 0 && 'bg-brand-500/20 text-brand-500'
- )}
- >
-
- {statusButtonLabel}
-
-
-
-
- Filter by Status
-
-
-
-
Status
-
- {/* Select All option */}
-
-
-
- {selectedStatuses.length === STATUS_FILTER_OPTIONS.length
- ? 'Deselect All'
- : 'Select All'}
-
-
-
-
-
- {/* Status list */}
-
- {STATUS_FILTER_OPTIONS.map((status) => {
- const config = statusDisplayConfig[status];
- const StatusIcon = config.icon;
- return (
-
handleStatusToggle(status)}
- >
- handleStatusToggle(status)}
- />
-
- {config.label}
-
- );
- })}
-
-
-
-
-
- {/* Divider */}
-
-
- {/* Positive/Negative Filter Toggle */}
-
-
-
- onNegativeFilterChange(!isNegativeFilter)}
- aria-label={
- isNegativeFilter
- ? 'Switch to show matching nodes'
- : 'Switch to hide matching nodes'
- }
- aria-pressed={isNegativeFilter}
- className={cn(
- 'flex items-center gap-1.5 px-2 py-1 rounded text-xs transition-colors',
- isNegativeFilter
- ? 'bg-orange-500/20 text-orange-500'
- : 'hover:bg-accent text-muted-foreground hover:text-foreground'
- )}
- >
- {isNegativeFilter ? (
- <>
-
- Hide
- >
- ) : (
- <>
-
- Show
- >
- )}
-
-
-
-
-
- {isNegativeFilter
- ? 'Negative filter: Highlighting non-matching nodes'
- : 'Positive filter: Highlighting matching nodes'}
-
-
-
- {/* Clear Filters Button - only show when filters are active */}
- {hasActiveFilter && (
- <>
-
-
-
-
-
-
-
- Clear All Filters
-
- >
+
+ {/* Search Input */}
+
+
+ onSearchQueryChange(e.target.value)}
+ className="h-8 w-48 pl-8 pr-8 text-sm bg-background/50"
+ />
+ {searchQuery && (
+ onSearchQueryChange('')}
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
+ aria-label="Clear search"
+ >
+
+
)}
-
+
+ {/* Divider */}
+
+
+ {/* Category Filter Dropdown */}
+
+
+
+
+ 0 && 'bg-brand-500/20 text-brand-500'
+ )}
+ >
+
+ {categoryButtonLabel}
+
+
+
+
+ Filter by Category
+
+ e.preventDefault()}
+ >
+
+
Categories
+
+ {/* Select All option */}
+
+ 0
+ }
+ onCheckedChange={handleSelectAllCategories}
+ />
+
+ {selectedCategories.length === availableCategories.length
+ ? 'Deselect All'
+ : 'Select All'}
+
+
+
+
+
+ {/* Category list */}
+
+ {availableCategories.length === 0 ? (
+
+ No categories available
+
+ ) : (
+ availableCategories.map((category) => (
+
handleCategoryToggle(category)}
+ >
+ handleCategoryToggle(category)}
+ />
+ {category}
+
+ ))
+ )}
+
+
+
+
+
+ {/* Status Filter Dropdown */}
+
+
+
+
+ 0 && 'bg-brand-500/20 text-brand-500'
+ )}
+ >
+
+ {statusButtonLabel}
+
+
+
+
+ Filter by Status
+
+ e.preventDefault()}
+ >
+
+
Status
+
+ {/* Select All option */}
+
+
+
+ {selectedStatuses.length === STATUS_FILTER_OPTIONS.length
+ ? 'Deselect All'
+ : 'Select All'}
+
+
+
+
+
+ {/* Status list */}
+
+ {STATUS_FILTER_OPTIONS.map((status) => {
+ const config = statusDisplayConfig[status];
+ const StatusIcon = config.icon;
+ return (
+
handleStatusToggle(status)}
+ >
+ handleStatusToggle(status)}
+ />
+
+ {config.label}
+
+ );
+ })}
+
+
+
+
+
+ {/* Divider */}
+
+
+ {/* Positive/Negative Filter Toggle */}
+
+
+
+ onNegativeFilterChange(!isNegativeFilter)}
+ aria-label={
+ isNegativeFilter
+ ? 'Switch to show matching nodes'
+ : 'Switch to hide matching nodes'
+ }
+ aria-pressed={isNegativeFilter}
+ className={cn(
+ 'flex items-center gap-1.5 px-2 py-1 rounded text-xs transition-colors',
+ isNegativeFilter
+ ? 'bg-orange-500/20 text-orange-500'
+ : 'hover:bg-accent text-muted-foreground hover:text-foreground'
+ )}
+ >
+ {isNegativeFilter ? (
+ <>
+
+ Hide
+ >
+ ) : (
+ <>
+
+ Show
+ >
+ )}
+
+
+
+
+
+ {isNegativeFilter
+ ? 'Negative filter: Highlighting non-matching nodes'
+ : 'Positive filter: Highlighting matching nodes'}
+
+
+
+ {/* Clear Filters Button - only show when filters are active */}
+ {hasActiveFilter && (
+ <>
+
+
+
+
+
+
+
+ Clear All Filters
+
+ >
+ )}
+
);
}
diff --git a/apps/ui/src/components/views/graph-view/components/task-node.tsx b/apps/ui/src/components/views/graph-view/components/task-node.tsx
index 16cf6817..98b95c46 100644
--- a/apps/ui/src/components/views/graph-view/components/task-node.tsx
+++ b/apps/ui/src/components/views/graph-view/components/task-node.tsx
@@ -26,7 +26,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
type TaskNodeProps = NodeProps & {
data: TaskNodeData;
@@ -286,50 +286,44 @@ export const TaskNode = memo(function TaskNode({ data, selected }: TaskNodeProps
{/* Blocked indicator */}
{data.isBlocked && !data.error && data.status === 'backlog' && (
-
-
-
-
-
-
-
-
- Blocked by {data.blockingDependencies.length} dependencies
-
-
-
+
+
+
+
+
+
+
+ Blocked by {data.blockingDependencies.length} dependencies
+
+
)}
{/* Error indicator */}
{data.error && (
-
-
-
-
-
-
- {data.error}
-
-
-
+
+
+
+
+
+ {data.error}
+
+
)}
{/* Stopped indicator - task is in_progress but not actively running */}
{isStopped && (
-
-
-
-
-
-
- Task paused - click menu to resume
-
-
-
+
+
+
+
+
+ Task paused - click menu to resume
+
+
)}
{/* Actions dropdown */}
diff --git a/apps/ui/src/components/views/settings-view/account/account-section.tsx b/apps/ui/src/components/views/settings-view/account/account-section.tsx
index d10049fc..abacd8ee 100644
--- a/apps/ui/src/components/views/settings-view/account/account-section.tsx
+++ b/apps/ui/src/components/views/settings-view/account/account-section.tsx
@@ -8,7 +8,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { toast } from 'sonner';
import { LogOut, User, Code2, RefreshCw } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
@@ -134,24 +134,22 @@ export function AccountSection() {
})}
-
-
-
-
- {isRefreshing ? : }
-
-
-
- Refresh available editors
-
-
-
+
+
+
+ {isRefreshing ? : }
+
+
+
+ Refresh available editors
+
+
diff --git a/apps/ui/src/hooks/use-responsive-kanban.ts b/apps/ui/src/hooks/use-responsive-kanban.ts
index 3062e715..3c1c5efb 100644
--- a/apps/ui/src/hooks/use-responsive-kanban.ts
+++ b/apps/ui/src/hooks/use-responsive-kanban.ts
@@ -14,8 +14,8 @@ export interface ResponsiveKanbanConfig {
* Default configuration for responsive Kanban columns
*/
const DEFAULT_CONFIG: ResponsiveKanbanConfig = {
- columnWidth: 288, // 18rem = 288px (w-72)
- columnMinWidth: 280, // Minimum column width - ensures usability
+ columnWidth: 320, // Increased from 288px to accommodate longer column titles
+ columnMinWidth: 320, // Increased from 280px to prevent title overflow
columnMaxWidth: Infinity, // No max width - columns scale evenly to fill viewport
gap: 20, // gap-5 = 20px
padding: 40, // px-5 on both sides = 40px (matches gap between columns)
diff --git a/apps/ui/src/lib/agent-context-parser.ts b/apps/ui/src/lib/agent-context-parser.ts
index 35e2cceb..8313e055 100644
--- a/apps/ui/src/lib/agent-context-parser.ts
+++ b/apps/ui/src/lib/agent-context-parser.ts
@@ -237,39 +237,34 @@ function cleanFragmentedText(content: string): string {
/**
* Extracts a summary from completed feature context
* Looks for content between
and tags
+ * Returns the LAST summary found to ensure we get the most recent/updated one
*/
function extractSummary(content: string): string | undefined {
// First, clean up any fragmented text from streaming
const cleanedContent = cleanFragmentedText(content);
- // Look for
tags - capture everything between opening and closing tags
- const summaryTagMatch = cleanedContent.match(/([\s\S]*?)<\/summary>/i);
- if (summaryTagMatch) {
- // Clean up the extracted summary content as well
- return cleanFragmentedText(summaryTagMatch[1]).trim();
- }
+ // Define regex patterns to try in order of priority
+ // Each pattern specifies which capture group contains the summary content
+ const regexesToTry = [
+ { regex: /([\s\S]*?)<\/summary>/gi, group: 1 },
+ { regex: /## Summary[^\n]*\n([\s\S]*?)(?=\n## [^#]|\n🔧|$)/gi, group: 1 },
+ {
+ regex:
+ /✓ (?:Feature|Verification|Task) (?:successfully|completed|verified)[^\n]*(?:\n[^\n]{1,200})?/gi,
+ group: 0,
+ },
+ {
+ regex: /(?:What was done|Changes made|Implemented)[^\n]*\n([\s\S]*?)(?=\n## [^#]|\n🔧|$)/gi,
+ group: 1,
+ },
+ ];
- // Fallback: Look for summary sections - capture everything including subsections (###)
- // Stop at same-level ## sections (but not ###), or tool markers, or end
- const summaryMatch = cleanedContent.match(/## Summary[^\n]*\n([\s\S]*?)(?=\n## [^#]|\n🔧|$)/i);
- if (summaryMatch) {
- return cleanFragmentedText(summaryMatch[1]).trim();
- }
-
- // Look for completion markers and extract surrounding text
- const completionMatch = cleanedContent.match(
- /✓ (?:Feature|Verification|Task) (?:successfully|completed|verified)[^\n]*(?:\n[^\n]{1,200})?/i
- );
- if (completionMatch) {
- return cleanFragmentedText(completionMatch[0]).trim();
- }
-
- // Look for "What was done" type sections
- const whatWasDoneMatch = cleanedContent.match(
- /(?:What was done|Changes made|Implemented)[^\n]*\n([\s\S]*?)(?=\n## [^#]|\n🔧|$)/i
- );
- if (whatWasDoneMatch) {
- return cleanFragmentedText(whatWasDoneMatch[1]).trim();
+ for (const { regex, group } of regexesToTry) {
+ const matches = [...cleanedContent.matchAll(regex)];
+ if (matches.length > 0) {
+ const lastMatch = matches[matches.length - 1];
+ return cleanFragmentedText(lastMatch[group]).trim();
+ }
}
return undefined;
diff --git a/apps/ui/src/lib/log-parser.ts b/apps/ui/src/lib/log-parser.ts
index 8a873b5f..5228a8fc 100644
--- a/apps/ui/src/lib/log-parser.ts
+++ b/apps/ui/src/lib/log-parser.ts
@@ -1198,46 +1198,48 @@ function mergeConsecutiveEntries(entries: LogEntry[]): LogEntry[] {
/**
* Extracts summary content from raw log output
- * Returns the summary text if found, or null if no summary exists
+ * Returns the LAST summary text if found, or null if no summary exists
+ * This ensures we get the most recent/updated summary when multiple exist
*/
export function extractSummary(rawOutput: string): string | null {
if (!rawOutput || !rawOutput.trim()) {
return null;
}
- // Try to find tags first (preferred format)
- const summaryTagMatch = rawOutput.match(/([\s\S]*?)<\/summary>/);
- if (summaryTagMatch) {
- return summaryTagMatch[1].trim();
- }
+ // First, clean up any fragmented text from streaming
+ // This handles cases where streaming providers send partial text chunks
+ // that got separated by newlines during accumulation (e.g., "")
+ const cleanedOutput = cleanFragmentedText(rawOutput);
- // Try to find markdown ## Summary section
- const summaryHeaderMatch = rawOutput.match(/^##\s+Summary\s*\n([\s\S]*?)(?=\n##\s+|$)/m);
- if (summaryHeaderMatch) {
- return summaryHeaderMatch[1].trim();
- }
+ // Define regex patterns to try in order of priority
+ // Each pattern specifies a processor function to extract the summary from the match
+ const regexesToTry: Array<{
+ regex: RegExp;
+ processor: (m: RegExpMatchArray) => string;
+ }> = [
+ { regex: /([\s\S]*?)<\/summary>/gi, processor: (m) => m[1] },
+ { regex: /^##\s+Summary[^\n]*\n([\s\S]*?)(?=\n##\s+[^#]|\n🔧|$)/gm, processor: (m) => m[1] },
+ {
+ regex: /^##\s+(Feature|Changes|Implementation)[^\n]*\n([\s\S]*?)(?=\n##\s+[^#]|\n🔧|$)/gm,
+ processor: (m) => `## ${m[1]}\n${m[2]}`,
+ },
+ {
+ regex: /(^|\n)(All tasks completed[\s\S]*?)(?=\n🔧|\n📋|\n⚡|\n❌|$)/g,
+ processor: (m) => m[2],
+ },
+ {
+ regex:
+ /(^|\n)((I've|I have) (successfully |now )?(completed|finished|implemented)[\s\S]*?)(?=\n🔧|\n📋|\n⚡|\n❌|$)/g,
+ processor: (m) => m[2],
+ },
+ ];
- // Try other summary formats (Feature, Changes, Implementation)
- const otherHeaderMatch = rawOutput.match(
- /^##\s+(Feature|Changes|Implementation)\s*\n([\s\S]*?)(?=\n##\s+|$)/m
- );
- if (otherHeaderMatch) {
- return `## ${otherHeaderMatch[1]}\n${otherHeaderMatch[2].trim()}`;
- }
-
- // Try to find summary introduction lines
- const introMatch = rawOutput.match(
- /(^|\n)(All tasks completed[\s\S]*?)(?=\n🔧|\n📋|\n⚡|\n❌|$)/
- );
- if (introMatch) {
- return introMatch[2].trim();
- }
-
- const completionMatch = rawOutput.match(
- /(^|\n)((I've|I have) (successfully |now )?(completed|finished|implemented)[\s\S]*?)(?=\n🔧|\n📋|\n⚡|\n❌|$)/
- );
- if (completionMatch) {
- return completionMatch[2].trim();
+ for (const { regex, processor } of regexesToTry) {
+ const matches = [...cleanedOutput.matchAll(regex)];
+ if (matches.length > 0) {
+ const lastMatch = matches[matches.length - 1];
+ return cleanFragmentedText(processor(lastMatch)).trim();
+ }
}
return null;