diff --git a/apps/ui/src/components/ui/task-progress-panel.tsx b/apps/ui/src/components/ui/task-progress-panel.tsx index 5cb5826f..414be1e7 100644 --- a/apps/ui/src/components/ui/task-progress-panel.tsx +++ b/apps/ui/src/components/ui/task-progress-panel.tsx @@ -230,7 +230,7 @@ export function TaskProgressPanel({ )} >
-
+
{/* Vertical Connector Line */}
diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx index 87268652..6916222e 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx @@ -1,6 +1,6 @@ // @ts-nocheck -import { useEffect, useState } from 'react'; -import { Feature, ThinkingLevel } from '@/store/app-store'; +import { useEffect, useState, useMemo } from 'react'; +import { Feature, ThinkingLevel, ParsedTask } from '@/store/app-store'; import type { ReasoningEffort } from '@automaker/types'; import { getProviderFromModel } from '@/lib/utils'; import { @@ -10,6 +10,7 @@ import { DEFAULT_MODEL, } from '@/lib/agent-context-parser'; import { cn } from '@/lib/utils'; +import type { AutoModeEvent } from '@/types/electron'; import { Brain, ListTodo, @@ -71,6 +72,66 @@ export function AgentInfoPanel({ const [agentInfo, setAgentInfo] = useState(null); const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false); const [isTodosExpanded, setIsTodosExpanded] = useState(false); + // Track real-time task status updates from WebSocket events + const [taskStatusMap, setTaskStatusMap] = useState< + Map + >(new Map()); + // Fresh planSpec data fetched from API (store data is stale for task progress) + const [freshPlanSpec, setFreshPlanSpec] = useState<{ + tasks?: ParsedTask[]; + tasksCompleted?: number; + currentTaskId?: string; + } | null>(null); + + // Derive effective todos from planSpec.tasks when available, fallback to agentInfo.todos + // Uses freshPlanSpec (from API) for accurate progress, with taskStatusMap for real-time updates + const effectiveTodos = useMemo(() => { + // Use freshPlanSpec if available (fetched from API), fallback to store's feature.planSpec + const planSpec = freshPlanSpec?.tasks?.length ? freshPlanSpec : feature.planSpec; + + // First priority: use planSpec.tasks if available (modern approach) + if (planSpec?.tasks && planSpec.tasks.length > 0) { + const completedCount = planSpec.tasksCompleted || 0; + const currentTaskId = planSpec.currentTaskId; + + return planSpec.tasks.map((task: ParsedTask, index: number) => { + // Use real-time status from WebSocket events if available + const realtimeStatus = taskStatusMap.get(task.id); + + // Calculate status: WebSocket status > index-based status > task.status + let effectiveStatus: 'pending' | 'in_progress' | 'completed'; + if (realtimeStatus) { + effectiveStatus = realtimeStatus; + } else if (index < completedCount) { + effectiveStatus = 'completed'; + } else if (task.id === currentTaskId) { + effectiveStatus = 'in_progress'; + } else { + // Fallback to task.status if available, otherwise pending + effectiveStatus = + task.status === 'completed' + ? 'completed' + : task.status === 'in_progress' + ? 'in_progress' + : 'pending'; + } + + return { + content: task.description, + status: effectiveStatus, + }; + }); + } + // Fallback: use parsed agentInfo.todos from agent-output.md + return agentInfo?.todos || []; + }, [ + freshPlanSpec, + feature.planSpec?.tasks, + feature.planSpec?.tasksCompleted, + feature.planSpec?.currentTaskId, + agentInfo?.todos, + taskStatusMap, + ]); useEffect(() => { const loadContext = async () => { @@ -82,6 +143,7 @@ export function AgentInfoPanel({ if (feature.status === 'backlog') { setAgentInfo(null); + setFreshPlanSpec(null); return; } @@ -91,6 +153,21 @@ export function AgentInfoPanel({ if (!currentProject?.path) return; if (api.features) { + // Fetch fresh feature data to get up-to-date planSpec (store data is stale) + try { + const featureResult = await api.features.get(currentProject.path, feature.id); + const freshFeature: any = (featureResult as any).feature; + if (featureResult.success && freshFeature?.planSpec) { + setFreshPlanSpec({ + tasks: freshFeature.planSpec.tasks, + tasksCompleted: freshFeature.planSpec.tasksCompleted || 0, + currentTaskId: freshFeature.planSpec.currentTaskId, + }); + } + } catch { + // Ignore errors fetching fresh planSpec + } + const result = await api.features.getAgentOutput(currentProject.path, feature.id); if (result.success && result.content) { @@ -113,13 +190,62 @@ export function AgentInfoPanel({ loadContext(); - if (isCurrentAutoTask) { + // Poll for updates when feature is in_progress (not just isCurrentAutoTask) + // This ensures planSpec progress stays in sync + if (isCurrentAutoTask || feature.status === 'in_progress') { const interval = setInterval(loadContext, 3000); return () => { clearInterval(interval); }; } }, [feature.id, feature.status, contextContent, isCurrentAutoTask]); + + // Listen to WebSocket events for real-time task status updates + // This ensures the Kanban card shows the same progress as the Agent Output modal + // Listen for ANY in-progress feature with planSpec tasks, not just isCurrentAutoTask + const hasPlanSpecTasks = + (freshPlanSpec?.tasks?.length ?? 0) > 0 || (feature.planSpec?.tasks?.length ?? 0) > 0; + const shouldListenToEvents = feature.status === 'in_progress' && hasPlanSpecTasks; + + useEffect(() => { + if (!shouldListenToEvents) return; + + const api = getElectronAPI(); + if (!api?.autoMode) return; + + const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => { + // Only handle events for this feature + if (!('featureId' in event) || event.featureId !== feature.id) return; + + switch (event.type) { + case 'auto_mode_task_started': + if ('taskId' in event) { + const taskEvent = event as Extract; + setTaskStatusMap((prev) => { + const newMap = new Map(prev); + // Mark current task as in_progress + newMap.set(taskEvent.taskId, 'in_progress'); + return newMap; + }); + } + break; + + case 'auto_mode_task_complete': + if ('taskId' in event) { + const taskEvent = event as Extract; + setTaskStatusMap((prev) => { + const newMap = new Map(prev); + newMap.set(taskEvent.taskId, 'completed'); + return newMap; + }); + } + break; + } + }); + + return unsubscribe; + }, [feature.id, shouldListenToEvents]); + // Model/Preset Info for Backlog Cards if (feature.status === 'backlog') { const provider = getProviderFromModel(feature.model); @@ -158,7 +284,9 @@ export function AgentInfoPanel({ } // Agent Info Panel for non-backlog cards - if (feature.status !== 'backlog' && agentInfo) { + // Show panel if we have agentInfo OR planSpec.tasks (for spec/full mode) + // Note: hasPlanSpecTasks is already defined above and includes freshPlanSpec + if (feature.status !== 'backlog' && (agentInfo || hasPlanSpecTasks)) { return ( <>
@@ -171,7 +299,7 @@ export function AgentInfoPanel({ })()} {formatModelName(feature.model ?? DEFAULT_MODEL)}
- {agentInfo.currentPhase && ( + {agentInfo?.currentPhase && (
{/* Task List Progress */} - {agentInfo.todos.length > 0 && ( + {effectiveTodos.length > 0 && (
- {agentInfo.todos.filter((t) => t.status === 'completed').length}/ - {agentInfo.todos.length} tasks + {effectiveTodos.filter((t) => t.status === 'completed').length}/ + {effectiveTodos.length} tasks
- {(isTodosExpanded ? agentInfo.todos : agentInfo.todos.slice(0, 3)).map( + {(isTodosExpanded ? effectiveTodos : effectiveTodos.slice(0, 3)).map( (todo, idx) => (
{todo.status === 'completed' ? ( @@ -227,7 +355,7 @@ export function AgentInfoPanel({
) )} - {agentInfo.todos.length > 3 && ( + {effectiveTodos.length > 3 && ( )}
@@ -247,7 +375,7 @@ export function AgentInfoPanel({ {/* Summary for waiting_approval and verified */} {(feature.status === 'waiting_approval' || feature.status === 'verified') && ( <> - {(feature.summary || summary || agentInfo.summary) && ( + {(feature.summary || summary || agentInfo?.summary) && (
@@ -273,23 +401,23 @@ export function AgentInfoPanel({ onPointerDown={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} > - {feature.summary || summary || agentInfo.summary} + {feature.summary || summary || agentInfo?.summary}

)} {!feature.summary && !summary && - !agentInfo.summary && - agentInfo.toolCallCount > 0 && ( + !agentInfo?.summary && + (agentInfo?.toolCallCount ?? 0) > 0 && (
- {agentInfo.toolCallCount} tool calls + {agentInfo?.toolCallCount ?? 0} tool calls - {agentInfo.todos.length > 0 && ( + {effectiveTodos.length > 0 && ( - {agentInfo.todos.filter((t) => t.status === 'completed').length} tasks done + {effectiveTodos.filter((t) => t.status === 'completed').length} tasks done )}
diff --git a/apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx b/apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx index 7b900409..ab2b0732 100644 --- a/apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx @@ -380,11 +380,11 @@ export function AgentOutputModal({ {effectiveViewMode === 'changes' ? ( -
+
{projectPath ? ( ) : effectiveViewMode === 'summary' && summary ? ( -
+
{summary}
) : ( @@ -409,7 +409,7 @@ export function AgentOutputModal({
{isLoading && !output ? (
diff --git a/package-lock.json b/package-lock.json index 0d26140a..00e0d253 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11595,7 +11595,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11617,7 +11616,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11660,7 +11658,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11682,7 +11679,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11704,7 +11700,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11726,7 +11721,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11748,7 +11742,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11770,7 +11763,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11792,7 +11784,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -17015,7 +17006,6 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" },