diff --git a/src/features/chat/display/agent.ts b/src/features/chat/display/agent.ts index 80dd84c6..53f13ba4 100644 --- a/src/features/chat/display/agent.ts +++ b/src/features/chat/display/agent.ts @@ -27,7 +27,7 @@ export function agentDisplayItem(item: CollabAgentToolCallItem, turnId?: string) model: item.model, reasoningEffort: item.reasoningEffort, agents, - state: collabAgentExecutionState(item.tool, item.status, item.receiverThreadIds, agents), + executionState: collabAgentExecutionState(item.tool, item.status, item.receiverThreadIds, agents), }; } diff --git a/src/features/chat/display/plan.ts b/src/features/chat/display/plan.ts index 93444a4e..3c12ece1 100644 --- a/src/features/chat/display/plan.ts +++ b/src/features/chat/display/plan.ts @@ -25,6 +25,6 @@ export function planProgressDisplayItem(turnId: string, explanation: string | nu explanation: trimmedExplanation !== undefined && trimmedExplanation.length > 0 ? trimmedExplanation : null, steps: plan.map((step) => ({ step: step.step, status: step.status })), status, - state: taskProgressExecutionState(status), + executionState: taskProgressExecutionState(status), }; } diff --git a/src/features/chat/display/review.ts b/src/features/chat/display/review.ts index 286296f0..4bcde741 100644 --- a/src/features/chat/display/review.ts +++ b/src/features/chat/display/review.ts @@ -21,7 +21,7 @@ export function createReviewResultItem(id: string, text: string): DisplayItem { role: "tool", text: parsed.summary, markdown: false, - state: autoReviewExecutionState(parsed.status), + executionState: autoReviewExecutionState(parsed.status), details: [{ title: "Review", rows: parsed.rows }], }; } @@ -55,7 +55,7 @@ export function createAutoReviewResultItem(params: AutoReviewNotification): Disp text, turnId: params.turnId, markdown: false, - state: completed ? autoReviewExecutionState(status) : "running", + executionState: completed ? autoReviewExecutionState(status) : "running", details: [{ title: "Review", rows }], }; } diff --git a/src/features/chat/display/state.ts b/src/features/chat/display/state.ts index 17c78bf9..59b38313 100644 --- a/src/features/chat/display/state.ts +++ b/src/features/chat/display/state.ts @@ -46,13 +46,7 @@ const AGENT_FAILED = ["interrupted", "errored", "notFound"] as const satisfies r const AGENT_FAILED_COMPAT = ["failed"] as const; export function executionState(item: DisplayItem): ExecutionState { - if (item.state) return item.state; - if (item.kind === "command") return commandExecutionState(item.status, item.exitCode); - if (item.kind === "fileChange") return patchApplyExecutionState(item.status); - if (item.kind === "taskProgress") return taskProgressExecutionState(item.status); - if (item.kind === "agent") return collabAgentToolCallExecutionState(item.status); - if (item.kind === "tool" || item.kind === "hook" || item.kind === "reasoning") return toolStatusExecutionState(item.status); - return null; + return item.executionState ?? null; } export function commandExecutionState(status: string, exitCode?: number): ExecutionState { diff --git a/src/features/chat/display/stream-updates.ts b/src/features/chat/display/stream-updates.ts index 16c94dbf..97973d1c 100644 --- a/src/features/chat/display/stream-updates.ts +++ b/src/features/chat/display/stream-updates.ts @@ -64,7 +64,7 @@ export function completeReasoningItems(items: readonly DisplayItem[], turnId: st ? { ...item, status: "completed", - state: "completed", + executionState: "completed", } : item, ); @@ -191,11 +191,13 @@ export function appendItemOutput( ? { status: "inProgress", changes: [], + executionState: "running", } : { command: fallbackText, cwd: "(unknown)", status: "running", + executionState: "running", }), }, ] as DisplayItem[]; diff --git a/src/features/chat/display/thread-items.ts b/src/features/chat/display/thread-items.ts index 67081c9d..978e7a6b 100644 --- a/src/features/chat/display/thread-items.ts +++ b/src/features/chat/display/thread-items.ts @@ -203,7 +203,7 @@ function mcpToolCallDisplayItem(item: McpToolCallItem, turnId?: string): Display ["Error JSON", item.error], ]), output: "", - state: mcpToolCallExecutionState(item.status), + executionState: mcpToolCallExecutionState(item.status), }; } @@ -225,7 +225,7 @@ function dynamicToolCallDisplayItem(item: DynamicToolCallItem, turnId?: string): ["Result JSON", item.contentItems], ]), output: "", - state: dynamicToolCallExecutionState(item.status, item.success), + executionState: dynamicToolCallExecutionState(item.status, item.success), }; } @@ -274,7 +274,7 @@ function imageGenerationDisplayItem(item: ImageGenerationItem, turnId?: string): ...bodyDetail("Result", item.result), ], output: "", - state: imageGenerationExecutionState(item.status), + executionState: imageGenerationExecutionState(item.status), }; } @@ -465,7 +465,7 @@ export function commandDisplayItem(item: CommandExecutionItem, turnId?: string): ...definedProp("exitCode", exitCode), ...definedProp("durationMs", durationMs), output: item.aggregatedOutput ?? "", - state: commandExecutionState(item.status, exitCode), + executionState: commandExecutionState(item.status, exitCode), }; } @@ -481,7 +481,7 @@ export function fileChangeDisplayItem(item: FileChangeItem, turnId?: string): Di itemId: item.id, status: item.status, changes, - state: patchApplyExecutionState(item.status), + executionState: patchApplyExecutionState(item.status), }; } diff --git a/src/features/chat/display/types.ts b/src/features/chat/display/types.ts index 856fea62..f6b6476f 100644 --- a/src/features/chat/display/types.ts +++ b/src/features/chat/display/types.ts @@ -23,7 +23,7 @@ export interface DisplayBase { text: string; turnId?: string; itemId?: string; - state?: ExecutionState; + executionState?: ExecutionState; } export interface DisplayDetailMetaRow { diff --git a/src/features/chat/request-state.ts b/src/features/chat/request-state.ts index 42e47646..924aa2d3 100644 --- a/src/features/chat/request-state.ts +++ b/src/features/chat/request-state.ts @@ -58,7 +58,7 @@ export function createApprovalResultItem(approval: PendingApproval, action: Appr text: approvalResultText(approval, action), ...definedProp("turnId", approvalTurnId(approval)), markdown: false, - state: kind === "accept" || kind === "accept-session" ? "completed" : "failed", + executionState: kind === "accept" || kind === "accept-session" ? "completed" : "failed", details: [ { title: "Approval", @@ -94,7 +94,7 @@ export function createUserInputResultItem( text: status === "submitted" ? `Input submitted for ${label}.` : `Input request cancelled for ${label}.`, ...definedProp("turnId", input.params.turnId), markdown: false, - state: status === "submitted" ? "completed" : "failed", + executionState: status === "submitted" ? "completed" : "failed", details, }; } diff --git a/tests/features/chat/chat-controller.test.ts b/tests/features/chat/chat-controller.test.ts index 334598c7..71769c47 100644 --- a/tests/features/chat/chat-controller.test.ts +++ b/tests/features/chat/chat-controller.test.ts @@ -83,7 +83,7 @@ describe("ChatController", () => { expect(state.displayItems).toEqual( expect.arrayContaining([ - expect.objectContaining({ id: "r1", kind: "reasoning", status: "completed", state: "completed" }), + expect.objectContaining({ id: "r1", kind: "reasoning", status: "completed", executionState: "completed" }), expect.objectContaining({ id: "a1", kind: "message", text: "answer" }), ]), ); @@ -732,7 +732,7 @@ describe("ChatController", () => { role: "tool", text: "Allowed for this session: Need access", turnId: "turn", - state: "completed", + executionState: "completed", details: [ { title: "Approval", @@ -1304,7 +1304,7 @@ describe("ChatController", () => { id: "review-review-1", kind: "reviewResult", text: "Auto-review approved: npm test", - state: "completed", + executionState: "completed", }); const reviewItem = expectPresent(state.displayItems[0]); expect("details" in reviewItem ? reviewItem.details[0] : null).toMatchObject({ diff --git a/tests/features/chat/display/display-model.test.ts b/tests/features/chat/display/display-model.test.ts index 89428a98..9f4a3676 100644 --- a/tests/features/chat/display/display-model.test.ts +++ b/tests/features/chat/display/display-model.test.ts @@ -13,7 +13,7 @@ import { import { normalizeProposedPlanMarkdown, planProgressDisplayItem } from "../../../../src/features/chat/display/plan"; import { pathRelativeToRoot } from "../../../../src/features/chat/display/paths"; import { createAutoReviewResultItem, createReviewResultItem } from "../../../../src/features/chat/display/review"; -import { executionState } from "../../../../src/features/chat/display/state"; +import { commandExecutionState, executionState } from "../../../../src/features/chat/display/state"; import { displayItemFromThreadItem, displayItemsFromTurns } from "../../../../src/features/chat/display/thread-items"; import { referencedThreadPrompt } from "../../../../src/domain/threads/reference"; import type { DisplayItem } from "../../../../src/features/chat/display/types"; @@ -27,7 +27,17 @@ function expectPresent(value: T | null | undefined): T { } function commandItem(id: string, text: string, turnId: string): DisplayItem { - return { id, kind: "command", role: "tool", text, turnId, command: text, cwd: "/vault", status: "completed" }; + return { + id, + kind: "command", + role: "tool", + text, + turnId, + command: text, + cwd: "/vault", + status: "completed", + executionState: "completed", + }; } function fileChangeItem(id: string, turnId: string, path = "src/main.ts"): DisplayItem { @@ -38,6 +48,7 @@ function fileChangeItem(id: string, turnId: string, path = "src/main.ts"): Displ text: "File change completed", turnId, status: "completed", + executionState: "completed", changes: [{ kind: "update", path, diff: "" }], }; } @@ -225,7 +236,7 @@ describe("thread item conversion preserves app-server semantics", () => { model: "gpt-5.5", reasoningEffort: "high", agents: [{ threadId: "child-thread", status: "completed", message: "Done" }], - state: "completed", + executionState: "completed", }); }); @@ -246,7 +257,7 @@ describe("thread item conversion preserves app-server semantics", () => { expect(displayItemFromThreadItem(item, "t1")).toMatchObject({ kind: "agent", status: "completed", - state: "completed", + executionState: "completed", }); }); @@ -269,7 +280,7 @@ describe("thread item conversion preserves app-server semantics", () => { kind: "command", text: "npm run check (exit 1)", output: "stderr with many details", - state: "failed", + executionState: "failed", }); }); @@ -292,7 +303,7 @@ describe("thread item conversion preserves app-server semantics", () => { kind: "command", actionLabel: "read", text: "src/main.ts", - state: "completed", + executionState: "completed", }); }); @@ -317,7 +328,7 @@ describe("thread item conversion preserves app-server semantics", () => { text: "src/main.ts", command: "nl -ba src/main.ts | sed -n '1,20p'", cwd: "/vault", - state: "completed", + executionState: "completed", }); }); @@ -340,7 +351,7 @@ describe("thread item conversion preserves app-server semantics", () => { kind: "command", actionLabel: "read", text: "/vault/src/main.ts", - state: "completed", + executionState: "completed", }); }); @@ -365,7 +376,7 @@ describe("thread item conversion preserves app-server semantics", () => { text: "npm run check", command: "/bin/zsh -lc 'npm run check'", cwd: "/vault", - state: "completed", + executionState: "completed", }); }); @@ -390,7 +401,7 @@ describe("thread item conversion preserves app-server semantics", () => { text: '"command target" in src/display', command: "rg 'command target' src/display", cwd: "/vault", - state: "completed", + executionState: "completed", }); }); @@ -413,7 +424,7 @@ describe("thread item conversion preserves app-server semantics", () => { kind: "command", actionLabel: "search", text: "target in src/display", - state: "completed", + executionState: "completed", }); }); @@ -436,7 +447,7 @@ describe("thread item conversion preserves app-server semantics", () => { kind: "command", actionLabel: "search", text: "target in src/display", - state: "completed", + executionState: "completed", }); }); @@ -459,7 +470,7 @@ describe("thread item conversion preserves app-server semantics", () => { kind: "command", actionLabel: "list files", text: "src/display", - state: "completed", + executionState: "completed", }); }); @@ -485,7 +496,7 @@ describe("thread item conversion preserves app-server semantics", () => { kind: "command", actionLabel: "search", text: "target in src", - state: "completed", + executionState: "completed", }); }); @@ -508,7 +519,7 @@ describe("thread item conversion preserves app-server semantics", () => { kind: "command", actionLabel: "list files", text: "workspace", - state: "completed", + executionState: "completed", }); }); @@ -530,7 +541,7 @@ describe("thread item conversion preserves app-server semantics", () => { expect(displayItemFromThreadItem(item, "t1")).toMatchObject({ kind: "command", text: "rg error src", - state: "completed", + executionState: "completed", }); }); @@ -569,16 +580,16 @@ describe("thread item conversion preserves app-server semantics", () => { expect(displayItemFromThreadItem(command, "t1")).toMatchObject({ text: "npm run check", - state: "running", + executionState: "running", }); expect(displayItemFromThreadItem(fileChange, "t1")).toMatchObject({ text: "src/main.ts", - state: "running", + executionState: "running", }); expect(displayItemFromThreadItem(tool, "t1")).toMatchObject({ text: "123", toolLabel: "github.pull_request_read", - state: "running", + executionState: "running", }); }); @@ -604,7 +615,7 @@ describe("thread item conversion preserves app-server semantics", () => { { title: "Arguments JSON", body: expect.stringContaining('"id": 123') }, { title: "Error JSON", body: expect.stringContaining("Not found") }, ], - state: "failed", + executionState: "failed", }); }); @@ -629,7 +640,7 @@ describe("thread item conversion preserves app-server semantics", () => { { title: "Arguments JSON", body: expect.stringContaining("https://example.com") }, { title: "Result JSON", body: expect.stringContaining("ok") }, ], - state: "completed", + executionState: "completed", }); }); @@ -669,7 +680,7 @@ describe("thread item conversion preserves app-server semantics", () => { { title: "Revised prompt", body: "A precise UI mockup." }, { title: "Result", body: "image result" }, ], - state: "completed", + executionState: "completed", }); }); @@ -711,7 +722,7 @@ describe("thread item conversion preserves app-server semantics", () => { kind: "tool", text: "details", toolLabel: "multi_tool_use.parallel", - state: "completed", + executionState: "completed", }); }); @@ -778,7 +789,7 @@ describe("thread item conversion preserves app-server semantics", () => { ).toMatchObject({ kind: "reviewResult", text: "Auto-review approved: Auto-review returned a low-risk allow decision.", - state: "completed", + executionState: "completed", details: [ { title: "Review", @@ -898,7 +909,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes role: "tool", text: "Auto-review approved: npm test", turnId: "t1", - state: "completed", + executionState: "completed", }, { id: "review-2", @@ -906,7 +917,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes role: "tool", text: "Auto-review approved: npm test", turnId: "t1", - state: "completed", + executionState: "completed", }, { id: "a1", kind: "message", role: "assistant", text: "done", turnId: "t1" }, ]; @@ -926,7 +937,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes it("hides empty completed reasoning items", () => { const items: DisplayItem[] = [ { id: "u1", kind: "message", role: "user", text: "do it", turnId: "t1" }, - { id: "r1", kind: "reasoning", role: "tool", text: "", turnId: "t1", status: "completed" }, + { id: "r1", kind: "reasoning", role: "tool", text: "", turnId: "t1", status: "completed", executionState: "completed" }, { id: "a1", kind: "message", role: "assistant", text: "done", turnId: "t1" }, ]; @@ -1177,7 +1188,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes role: "tool", text: "Auto-review approved: npm test", turnId: "t1", - state: "completed", + executionState: "completed", }, { id: "review-2", @@ -1185,7 +1196,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes role: "tool", text: "Auto-review approved: npm test", turnId: "t1", - state: "completed", + executionState: "completed", }, { id: "a1", kind: "message", role: "assistant", text: "done", turnId: "t1" }, ]; @@ -1205,7 +1216,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes role: "tool", text: "Auto-review approved: npm test", turnId: "t1", - state: "completed", + executionState: "completed", }, { id: "a1", kind: "message", role: "assistant", text: "still working", turnId: "t1" }, ]; @@ -1239,47 +1250,15 @@ describe("workspace path summaries stay readable without hiding audit paths", () describe("execution state uses typed status adapters before rendered text", () => { it("detects failed command state", () => { - expect( - executionState({ - id: "c1", - kind: "command", - role: "tool", - text: "Command", - command: "npm test", - cwd: "/vault", - status: "failed", - exitCode: 1, - }), - ).toBe("failed"); + expect(commandExecutionState("failed", 1)).toBe("failed"); }); it("does not infer command failure from the command text", () => { - expect( - executionState({ - id: "c1", - kind: "command", - role: "tool", - text: "rg error src", - command: "rg error src", - cwd: "/vault", - status: "completed", - exitCode: 0, - }), - ).toBe("completed"); + expect(commandExecutionState("completed", 0)).toBe("completed"); }); it("uses typed command status before command text", () => { - expect( - executionState({ - id: "c1", - kind: "command", - role: "tool", - text: "rg error src", - command: "rg error src", - cwd: "/vault", - status: "inProgress", - }), - ).toBe("running"); + expect(commandExecutionState("inProgress")).toBe("running"); }); it("does not infer unknown status strings with broad matching", () => { diff --git a/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx b/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx index fcef437c..e3cf45aa 100644 --- a/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx +++ b/tests/features/chat/ui/message-stream/blocks-and-messages.test.tsx @@ -218,7 +218,7 @@ describe("message stream block identity and message actions", () => { text: "Auto-review approved: npm test", turnId: "turn", markdown: false, - state: "completed", + executionState: "completed", details: [ { title: "Review", @@ -276,7 +276,7 @@ describe("message stream block identity and message actions", () => { cwd: "/vault", status: "completed", output: "ok", - state: "completed", + executionState: "completed", }, ], openDetails: new Set(), diff --git a/tests/features/chat/ui/message-stream/pending-requests.test.tsx b/tests/features/chat/ui/message-stream/pending-requests.test.tsx index 41d06076..27b88a7c 100644 --- a/tests/features/chat/ui/message-stream/pending-requests.test.tsx +++ b/tests/features/chat/ui/message-stream/pending-requests.test.tsx @@ -362,7 +362,7 @@ describe("pending request renderer decisions", () => { text: "Input submitted for 1 question.", turnId: "turn", markdown: false, - state: "completed", + executionState: "completed", details: [{ title: "Question: Scope", rows: [{ key: "Answer", value: "Narrow" }] }], }, ], @@ -394,7 +394,7 @@ describe("pending request renderer decisions", () => { text: "Allowed for this session: Need access", turnId: "turn", markdown: false, - state: "completed", + executionState: "completed", details: [ { title: "Approval",