diff --git a/apps/desktop/src/main/services/orchestrator/aiOrchestratorService.ts b/apps/desktop/src/main/services/orchestrator/aiOrchestratorService.ts index 08a9f320f..287200954 100644 --- a/apps/desktop/src/main/services/orchestrator/aiOrchestratorService.ts +++ b/apps/desktop/src/main/services/orchestrator/aiOrchestratorService.ts @@ -2682,13 +2682,29 @@ Check all worker statuses and continue managing the mission from here. Read work const buildMissionStateProgressFromGraph = (graph: OrchestratorRunGraph): MissionStateProgress => { const relevantSteps = filterExecutionSteps(graph.steps); + let completedSteps = 0; + const activeWorkers: string[] = []; + const blockedSteps: string[] = []; + const failedSteps: string[] = []; + for (const step of relevantSteps) { + if (MISSION_STATE_TERMINAL_STEP_STATUSES.has(step.status)) { + completedSteps += 1; + } + if (step.status === "running") { + activeWorkers.push(step.stepKey); + } else if (step.status === "blocked") { + blockedSteps.push(step.stepKey); + } else if (step.status === "failed") { + failedSteps.push(step.stepKey); + } + } return { currentPhase: currentPhaseFromGraph(graph), - completedSteps: relevantSteps.filter((step) => MISSION_STATE_TERMINAL_STEP_STATUSES.has(step.status)).length, + completedSteps, totalSteps: relevantSteps.length, - activeWorkers: relevantSteps.filter((step) => step.status === "running").map((step) => step.stepKey), - blockedSteps: relevantSteps.filter((step) => step.status === "blocked").map((step) => step.stepKey), - failedSteps: relevantSteps.filter((step) => step.status === "failed").map((step) => step.stepKey), + activeWorkers, + blockedSteps, + failedSteps, }; }; diff --git a/apps/desktop/src/main/services/orchestrator/coordinatorTools.ts b/apps/desktop/src/main/services/orchestrator/coordinatorTools.ts index 5767f5483..2642e1626 100644 --- a/apps/desktop/src/main/services/orchestrator/coordinatorTools.ts +++ b/apps/desktop/src/main/services/orchestrator/coordinatorTools.ts @@ -391,14 +391,29 @@ function resolveCurrentPhase(graph: OrchestratorRunGraph): string { function buildMissionStateProgress(graph: OrchestratorRunGraph): MissionStateProgress { const relevantSteps = filterExecutionSteps(graph.steps); - const completedSteps = relevantSteps.filter((step) => TERMINAL_STEP_STATUSES.has(step.status)).length; + let completedSteps = 0; + const activeWorkers: string[] = []; + const blockedSteps: string[] = []; + const failedSteps: string[] = []; + for (const step of relevantSteps) { + if (TERMINAL_STEP_STATUSES.has(step.status)) { + completedSteps += 1; + } + if (step.status === "running") { + activeWorkers.push(step.stepKey); + } else if (step.status === "blocked") { + blockedSteps.push(step.stepKey); + } else if (step.status === "failed") { + failedSteps.push(step.stepKey); + } + } return { currentPhase: resolveCurrentPhase(graph), completedSteps, totalSteps: relevantSteps.length, - activeWorkers: relevantSteps.filter((step) => step.status === "running").map((step) => step.stepKey), - blockedSteps: relevantSteps.filter((step) => step.status === "blocked").map((step) => step.stepKey), - failedSteps: relevantSteps.filter((step) => step.status === "failed").map((step) => step.stepKey), + activeWorkers, + blockedSteps, + failedSteps, }; } diff --git a/apps/desktop/src/main/services/orchestrator/orchestratorService.ts b/apps/desktop/src/main/services/orchestrator/orchestratorService.ts index e76a565a4..eb0a993e8 100644 --- a/apps/desktop/src/main/services/orchestrator/orchestratorService.ts +++ b/apps/desktop/src/main/services/orchestrator/orchestratorService.ts @@ -4125,12 +4125,26 @@ export function createOrchestratorService({ const runSteps = listStepRows(args.run.id).map(toStep); const frontier = { - pending: runSteps.filter((step) => step.status === "pending").length, - ready: runSteps.filter((step) => step.status === "ready").length, - running: runSteps.filter((step) => step.status === "running").length, - blocked: runSteps.filter((step) => step.status === "blocked").length, - terminal: runSteps.filter((step) => TERMINAL_STEP_STATUSES.has(step.status)).length + pending: 0, + ready: 0, + running: 0, + blocked: 0, + terminal: 0 }; + for (const step of runSteps) { + if (TERMINAL_STEP_STATUSES.has(step.status)) { + frontier.terminal += 1; + } + if (step.status === "pending") { + frontier.pending += 1; + } else if (step.status === "ready") { + frontier.ready += 1; + } else if (step.status === "running") { + frontier.running += 1; + } else if (step.status === "blocked") { + frontier.blocked += 1; + } + } const openQuestions = Number( db.get<{ count: number }>( ` @@ -11350,7 +11364,13 @@ export function createOrchestratorService({ checkFanOutCompletion(args: { runId: string; completedStepKey: string }): boolean { const { runId, completedStepKey } = args; const allSteps = listStepRows(runId).map(toStep); - const completedStep = allSteps.find((s) => s.stepKey === completedStepKey); + const stepByKey = new Map(); + for (const step of allSteps) { + if (!stepByKey.has(step.stepKey)) { + stepByKey.set(step.stepKey, step); + } + } + const completedStep = stepByKey.get(completedStepKey); if (!completedStep) return false; // Find the parent via fanOutParent in metadata @@ -11358,7 +11378,7 @@ export function createOrchestratorService({ const parentStepKey = meta.fanOutParent as string | undefined; if (!parentStepKey) return false; - const parentStep = allSteps.find((s) => s.stepKey === parentStepKey); + const parentStep = stepByKey.get(parentStepKey); if (!parentStep) return false; const parentMeta = (parentStep.metadata ?? {}) as Record; @@ -11368,15 +11388,23 @@ export function createOrchestratorService({ // Check if all children are in a terminal state const terminalStatuses = new Set(["succeeded", "failed", "skipped", "superseded", "canceled"]); const allDone = childKeys.every((key) => { - const child = allSteps.find((s) => s.stepKey === key); + const child = stepByKey.get(key); return child && terminalStatuses.has(child.status); }); if (allDone && parentMeta.fanOutComplete !== true) { const updatedMeta = { ...parentMeta, fanOutComplete: true }; const now = nowIso(); - const succeededCount = childKeys.filter((key) => allSteps.find((s) => s.stepKey === key)?.status === "succeeded").length; - const failedCount = childKeys.filter((key) => allSteps.find((s) => s.stepKey === key)?.status === "failed").length; + let succeededCount = 0; + let failedCount = 0; + for (const key of childKeys) { + const child = stepByKey.get(key); + if (child?.status === "succeeded") { + succeededCount += 1; + } else if (child?.status === "failed") { + failedCount += 1; + } + } // VAL-STATE-002: Update parent step status to reflect variant outcomes. // If any child succeeded → parent succeeded; if all failed → parent failed. diff --git a/apps/desktop/src/renderer/components/chat/AgentChatMessageList.tsx b/apps/desktop/src/renderer/components/chat/AgentChatMessageList.tsx index 093b02ac5..13440619d 100644 --- a/apps/desktop/src/renderer/components/chat/AgentChatMessageList.tsx +++ b/apps/desktop/src/renderer/components/chat/AgentChatMessageList.tsx @@ -47,7 +47,7 @@ import type { import { getModelById, resolveModelDescriptor, type ModelDescriptor } from "../../../shared/modelRegistry"; import { cn } from "../ui/cn"; import { formatTime } from "../../lib/format"; -import { openExternalUrl, openUrlInAdeBrowser } from "../../lib/openExternal"; +import { openUrlInAdeBrowser } from "../../lib/openExternal"; import { isPathEqualOrDescendant, isWindowsAbsolutePath, normalizePath } from "../../lib/pathUtils"; import { describeToolIdentifier, replaceInternalToolNames } from "./toolPresentation"; import { chatChipToneClass } from "./chatSurfaceTheme"; @@ -554,13 +554,6 @@ function StatusIcon({ status }: { status: "running" | "completed" | "failed" | " return ; } -function PlanStepIcon({ status }: { status: string }) { - if (status === "completed") return ; - if (status === "failed") return ; - if (status === "in_progress") return ; - return ; -} - function todoItemStatusClass(status: string): string { switch (status) { case "completed": @@ -3176,26 +3169,11 @@ function renderEvent( ); } -type TurnSummaryTask = { - id: string; - description: string; - status: string; -}; - -type TurnSummaryFile = { - path: string; - kind: Extract["kind"]; - status?: Extract["status"]; - additions: number; - deletions: number; -}; - type TurnSummary = { turnId: string; - tasks: TurnSummaryTask[]; - files: TurnSummaryFile[]; - totalAdditions: number; - totalDeletions: number; + taskCount: number; + completedTaskCount: number; + changedFileCount: number; backgroundAgentCount: number; activeBackgroundAgentCount: number; turnModel: { label: string; modelId?: string; model?: string } | null; @@ -3207,10 +3185,11 @@ function deriveTurnSummary( events: AgentChatEventEnvelope[], turnModelState: DerivedTurnModelState | null, ): TurnSummary | null { - const latestTurnId = [...events] - .reverse() - .map((envelope) => getEventTurnId(envelope.event)) - .find((turnId): turnId is string => Boolean(turnId)); + let latestTurnId: string | null = null; + for (let i = events.length - 1; i >= 0; i -= 1) { + latestTurnId = getEventTurnId(events[i]!.event); + if (latestTurnId) break; + } if (!latestTurnId) return null; let latestTodoUpdate: Extract | null = null; @@ -3218,7 +3197,7 @@ function deriveTurnSummary( let turnStartedAt: number | null = null; let turnEndedAt: number | null = null; let ended = false; - const files = new Map(); + const changedFilePaths = new Set(); const subagents = new Map(); for (const envelope of events) { @@ -3245,14 +3224,7 @@ function deriveTurnSummary( } if (event.type === "file_change") { - const stats = summarizeDiffStats(event.diff); - files.set(event.path, { - path: event.path, - kind: event.kind, - status: event.status, - additions: stats.additions, - deletions: stats.deletions, - }); + changedFilePaths.add(event.path); continue; } @@ -3283,26 +3255,25 @@ function deriveTurnSummary( } } - const tasks: TurnSummaryTask[] = latestTodoUpdate - ? latestTodoUpdate.items.map((item) => ({ - id: item.id, - description: item.description, - status: item.status, - })) - : latestPlan - ? latestPlan.steps.map((step, index) => ({ - id: `plan-${index}`, - description: step.text, - status: step.status, - })) - : []; - const changedFiles = [...files.values()]; - const totalAdditions = changedFiles.reduce((sum, file) => sum + file.additions, 0); - const totalDeletions = changedFiles.reduce((sum, file) => sum + file.deletions, 0); - const backgroundAgentCount = [...subagents.values()].filter((entry) => entry.background).length; - const activeBackgroundAgentCount = [...subagents.values()].filter((entry) => entry.background && entry.status === "running").length; + let taskCount = 0; + let completedTaskCount = 0; + const taskSource = latestTodoUpdate?.items ?? latestPlan?.steps ?? []; + for (const task of taskSource) { + taskCount += 1; + if (task.status === "completed") completedTaskCount += 1; + } + const changedFileCount = changedFilePaths.size; + let backgroundAgentCount = 0; + let activeBackgroundAgentCount = 0; + for (const entry of subagents.values()) { + if (!entry.background) continue; + backgroundAgentCount += 1; + if (entry.status === "running") { + activeBackgroundAgentCount += 1; + } + } - if (!tasks.length && !changedFiles.length && !backgroundAgentCount) { + if (!taskCount && !changedFileCount && !backgroundAgentCount) { return null; } @@ -3313,12 +3284,11 @@ function deriveTurnSummary( return { turnId: latestTurnId, - tasks, - files: changedFiles, + taskCount, + completedTaskCount, + changedFileCount, durationMs, ended, - totalAdditions, - totalDeletions, backgroundAgentCount, activeBackgroundAgentCount, turnModel: turnModelState?.map.get(latestTurnId) ?? null, @@ -3336,9 +3306,7 @@ function formatTurnDuration(durationMs: number): string { function TurnDivider({ summary }: { summary: TurnSummary }) { if (!summary.ended) return null; - const completedCount = summary.tasks.filter((task) => task.status === "completed").length; - const totalCount = summary.tasks.length; - const taskLine = totalCount ? `${completedCount}/${totalCount} tasks complete` : null; + const taskLine = summary.taskCount ? `${summary.completedTaskCount}/${summary.taskCount} tasks complete` : null; const agentLine = summary.backgroundAgentCount ? `${summary.backgroundAgentCount} background ${summary.backgroundAgentCount === 1 ? "agent" : "agents"}${ summary.activeBackgroundAgentCount > 0 ? ` · ${summary.activeBackgroundAgentCount} still running` : " finished" @@ -3831,7 +3799,7 @@ export function AgentChatMessageList({ const turnSummary = useMemo(() => deriveTurnSummary(events, turnModelState), [events, turnModelState]); const handleReviewChanges = useCallback(() => { - if (!turnSummary?.files.length) return; + if (!turnSummary?.changedFileCount) return; const state = currentLaneId ? { laneId: currentLaneId } : undefined; navigate("/files", state ? { state } : undefined); }, [currentLaneId, navigate, turnSummary]);