diff --git a/apps/server/src/services/spec-parser.ts b/apps/server/src/services/spec-parser.ts index 1c9f527e..295246fb 100644 --- a/apps/server/src/services/spec-parser.ts +++ b/apps/server/src/services/spec-parser.ts @@ -214,10 +214,13 @@ export function extractSummary(text: string): string | null { } // Check for ## Summary section (use last match) - const sectionMatches = text.matchAll(/##\s*Summary\s*\n+([\s\S]*?)(?=\n##|\n\*\*|$)/gi); + // Use \n## [^#] to stop at same-level headers (## Foo) but NOT subsections (### Root Cause) + const sectionMatches = text.matchAll(/##\s*Summary\s*\n+([\s\S]*?)(?=\n## [^#]|\n\*\*|$)/gi); const sectionMatch = getLastMatch(sectionMatches); if (sectionMatch) { - return truncate(sectionMatch[1].trim(), 500); + const content = sectionMatch[1].trim(); + // Keep full content (including ### subsections) up to max length + return content.length > 500 ? `${content.substring(0, 500)}...` : content; } // Check for **Goal**: section (lite mode, use last match) diff --git a/apps/server/tests/unit/services/spec-parser.test.ts b/apps/server/tests/unit/services/spec-parser.test.ts index 411c9290..205e92c3 100644 --- a/apps/server/tests/unit/services/spec-parser.test.ts +++ b/apps/server/tests/unit/services/spec-parser.test.ts @@ -573,6 +573,55 @@ Implementation details. `; expect(extractSummary(text)).toBe('Summary content here.'); }); + + it('should include ### subsections within the summary (not cut off at ### Root Cause)', () => { + const text = ` +## Summary + +Overview of changes. + +### Root Cause +The bug was caused by X. + +### Fix Applied +Changed Y to Z. + +## Other Section +More content. +`; + const result = extractSummary(text); + expect(result).not.toBeNull(); + expect(result).toContain('Overview of changes.'); + expect(result).toContain('### Root Cause'); + expect(result).toContain('The bug was caused by X.'); + expect(result).toContain('### Fix Applied'); + expect(result).toContain('Changed Y to Z.'); + expect(result).not.toContain('## Other Section'); + }); + + it('should include ### subsections and stop at next ## header', () => { + const text = ` +## Summary + +Brief intro. + +### Changes +- File A modified +- File B added + +### Notes +Important context. + +## Implementation +Details here. +`; + const result = extractSummary(text); + expect(result).not.toBeNull(); + expect(result).toContain('Brief intro.'); + expect(result).toContain('### Changes'); + expect(result).toContain('### Notes'); + expect(result).not.toContain('## Implementation'); + }); }); describe('**Goal**: section (lite planning mode)', () => { @@ -692,7 +741,7 @@ Summary section content. expect(extractSummary('Random text without any summary patterns')).toBeNull(); }); - it('should handle multiple paragraph summaries (return first paragraph)', () => { + it('should include all paragraphs in ## Summary section', () => { const text = ` ## Summary @@ -702,7 +751,9 @@ Second paragraph of summary. ## Other `; - expect(extractSummary(text)).toBe('First paragraph of summary.'); + const result = extractSummary(text); + expect(result).toContain('First paragraph of summary.'); + expect(result).toContain('Second paragraph of summary.'); }); }); diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 9b525edb..f0c92784 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -1905,7 +1905,15 @@ export function BoardView({ initialFeatureId }: BoardViewProps) { selectedFeatureIds={selectedFeatureIds} onToggleFeatureSelection={toggleFeatureSelection} onRowClick={(feature) => { - if (feature.status === 'backlog') { + // Running features should always show logs, even if status is + // stale (still 'backlog'/'ready'/'interrupted' during race window) + const isRunning = runningAutoTasksAllWorktrees.includes(feature.id); + const isBacklogLike = + feature.status === 'backlog' || + feature.status === 'merge_conflict' || + feature.status === 'ready' || + feature.status === 'interrupted'; + if (isBacklogLike && !isRunning) { setEditingFeature(feature); } else { handleViewOutput(feature); diff --git a/apps/ui/src/components/views/board-view/components/list-view/row-actions.tsx b/apps/ui/src/components/views/board-view/components/list-view/row-actions.tsx index fcf938d1..173e014b 100644 --- a/apps/ui/src/components/views/board-view/components/list-view/row-actions.tsx +++ b/apps/ui/src/components/views/board-view/components/list-view/row-actions.tsx @@ -431,6 +431,45 @@ export const RowActions = memo(function RowActions({ )} + {/* Running task with stale status (backlog/ready/interrupted but tracked as running). + These features are placed in the in_progress column by useBoardColumnFeatures + but their actual status hasn't updated yet, so no other menu block matches. */} + {!isCurrentAutoTask && + isRunningTask && + (feature.status === 'backlog' || + feature.status === 'ready' || + feature.status === 'interrupted' || + feature.status === 'merge_conflict') && ( + <> + {handlers.onViewOutput && ( + + )} + + {handlers.onSpawnTask && ( + + )} + {handlers.onForceStop && ( + <> + + + + )} + + )} + {/* Backlog actions */} {!isCurrentAutoTask && !isRunningTask && diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-features.ts b/apps/ui/src/components/views/board-view/hooks/use-board-features.ts index 40fb30be..9068af89 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-features.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-features.ts @@ -115,16 +115,14 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) { // Board view only reacts to events for the currently selected project const eventProjectId = ('projectId' in event && event.projectId) || projectId; - if (event.type === 'auto_mode_feature_start') { - // Reload features when a feature starts to ensure status update (backlog -> in_progress) is reflected - logger.info( - `[BoardFeatures] Feature ${event.featureId} started for project ${projectPath}, reloading features to update status...` - ); - loadFeatures(); - } else if (event.type === 'auto_mode_feature_complete') { - // Reload features when a feature is completed - logger.info('Feature completed, reloading features...'); - loadFeatures(); + // NOTE: auto_mode_feature_start and auto_mode_feature_complete are NOT handled here + // for feature list reloading. That is handled by useAutoModeQueryInvalidation which + // invalidates the features.all query on those events. Duplicate invalidation here + // caused a re-render cascade through DndContext that triggered React error #185 + // (maximum update depth exceeded), crashing the board view with an infinite spinner + // when a new feature was added and moved to in_progress. + + if (event.type === 'auto_mode_feature_complete') { // Play ding sound when feature is done (unless muted) const { muteDoneSound } = useAppStore.getState(); if (!muteDoneSound) { 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 0d053ea8..446ac08f 100644 --- a/apps/ui/src/components/views/board-view/kanban-board.tsx +++ b/apps/ui/src/components/views/board-view/kanban-board.tsx @@ -1,4 +1,5 @@ import { + memo, useMemo, useRef, useState, @@ -280,7 +281,7 @@ function VirtualizedList({ ); } -export function KanbanBoard({ +export const KanbanBoard = memo(function KanbanBoard({ activeFeature, getColumnFeatures, backgroundImageStyle, @@ -719,4 +720,4 @@ export function KanbanBoard({ ); -} +});