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 60eba50f..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 @@ -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,22 +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 - // This fixes the issue where Kanban cards show "0/0 tasks" when the agent uses planSpec - // instead of emitting TodoWrite tool calls in the output log + // 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 (feature.planSpec?.tasks && feature.planSpec.tasks.length > 0) { - return feature.planSpec.tasks.map((task: ParsedTask) => ({ - content: task.description, - // Map 'failed' status to 'pending' since todo display doesn't support 'failed' - status: task.status === 'failed' ? 'pending' : task.status, - })); + 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 || []; - }, [feature.planSpec?.tasks, agentInfo?.todos]); + }, [ + freshPlanSpec, + feature.planSpec?.tasks, + feature.planSpec?.tasksCompleted, + feature.planSpec?.currentTaskId, + agentInfo?.todos, + taskStatusMap, + ]); useEffect(() => { const loadContext = async () => { @@ -98,6 +143,7 @@ export function AgentInfoPanel({ if (feature.status === 'backlog') { setAgentInfo(null); + setFreshPlanSpec(null); return; } @@ -107,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) { @@ -129,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); @@ -174,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 ( <>
@@ -187,7 +299,7 @@ export function AgentInfoPanel({ })()} {formatModelName(feature.model ?? DEFAULT_MODEL)}
- {agentInfo.currentPhase && ( + {agentInfo?.currentPhase && (
- {(feature.summary || summary || agentInfo.summary) && ( + {(feature.summary || summary || agentInfo?.summary) && (
@@ -289,18 +401,18 @@ 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 {effectiveTodos.length > 0 && (