From 948047a8ca382380d9e1c9b2cbc342ee04c74e7d Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:10:32 -0400 Subject: [PATCH] fix(tui): use disclosure icon for low activity errors (#52097) Co-authored-by: thdxr <826656+thdxr@users.noreply.github.com> --- .../src/routes/session/activity-summary.ts | 9 ---- .../tui/src/routes/session/group-view.tsx | 2 +- packages/tui/test/cli/tui/group-view.test.tsx | 51 +++++++++++++++++++ .../test/cli/tui/session-verbosity.test.ts | 11 ++-- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/packages/tui/src/routes/session/activity-summary.ts b/packages/tui/src/routes/session/activity-summary.ts index b85f954161e..6578f50d632 100644 --- a/packages/tui/src/routes/session/activity-summary.ts +++ b/packages/tui/src/routes/session/activity-summary.ts @@ -91,7 +91,6 @@ export function activitySummary(items: readonly Item[], instructions: number, cl .map(([name, count]) => `${count} ${name}${count === 1 ? "" : "s"}`) .join(", "), active: items.some((item) => isActive(item, closed)), - failed: items.some(failed), } } @@ -100,11 +99,3 @@ function isActive(item: Item, closed: boolean) { return !closed && item.part.time?.completed === undefined && item.message.time.completed === undefined return item.part.state.status === "streaming" || item.part.state.status === "running" } - -function failed(item: Item) { - if (item.part.type !== "tool") return false - if (item.part.state.status === "error") return true - if (canonicalToolName(item.part.name) !== "execute" || item.part.state.status === "streaming") return false - const metadata = item.part.state.metadata - return metadata?.error === true || executeCalls(metadata?.toolCalls).some((call) => call.status === "error") -} diff --git a/packages/tui/src/routes/session/group-view.tsx b/packages/tui/src/routes/session/group-view.tsx index ec56198a799..6bb543b2e5a 100644 --- a/packages/tui/src/routes/session/group-view.tsx +++ b/packages/tui/src/routes/session/group-view.tsx @@ -211,7 +211,7 @@ function ActivityGroup(props: GroupProps) { { + const config = createTuiResolvedConfig({ animations: false }) + const anchors = createTimelineAnchors() + const [expanded, setExpanded] = createStore>({}) + const message: SessionMessageAssistant = { + id: "a", + type: "assistant", + agent: "build", + model: { providerID: "fixture", id: "fixture" }, + time: { created: 0, completed: 2 }, + content: [ + { + type: "tool", + id: "failed-shell", + name: "shell", + time: { created: 0, completed: 2 }, + state: { status: "error", input: {}, error: { type: "Fixture", message: "command failed" } }, + }, + ], + } + const row: SessionGroup = { + type: "group", + kind: "activity", + size: 1, + completed: true, + pending: [], + children: [{ type: "entry", size: 1, entry: { type: "part", ref: { messageID: "a", partID: "failed-shell" } } }], + } + const app = await mount({ + row, + anchors, + config, + expanded: (id) => expanded[id], + setExpanded: (id, value) => setExpanded(id, value), + message: () => message, + entry: () => command failed, + }) + try { + app.renderer.start() + await app.waitForFrame((frame) => frame.includes("1 command")) + expect(app.captureCharFrame()).toContain("+ 1 command") + expect(app.captureCharFrame()).not.toContain("command failed") + await app.mockMouse.click(4, anchors.get({ type: "group", groupID: groupID(row, 0)! })?.node.y ?? -1) + await app.renderOnce() + expect(app.captureCharFrame()).toContain("− 1 command") + expect(app.captureCharFrame()).toContain("command failed") + } finally { + app.renderer.destroy() + } +}) + function mount(input: { row: SessionGroup anchors: ReturnType diff --git a/packages/tui/test/cli/tui/session-verbosity.test.ts b/packages/tui/test/cli/tui/session-verbosity.test.ts index 48b65fc01ec..c6034bd03a9 100644 --- a/packages/tui/test/cli/tui/session-verbosity.test.ts +++ b/packages/tui/test/cli/tui/session-verbosity.test.ts @@ -180,11 +180,10 @@ test("activity summary counts finished work by category", () => { expect(activitySummary(items, 2)).toEqual({ label: "2 commands, 2 edits, 1 thought, 1 read, 1 tool, 2 instructions", active: false, - failed: false, }) }) -test("running work is active but not counted, and failures are reported", () => { +test("running work is active but not counted, and failed tools are counted", () => { const open = { ...assistant("a", []), time: { created: 1 } } const summary = activitySummary( [ @@ -195,7 +194,7 @@ test("running work is active but not counted, and failures are reported", () => ], 0, ) - expect(summary).toEqual({ label: "1 read, 1 tool", active: true, failed: true }) + expect(summary).toEqual({ label: "1 read, 1 tool", active: true }) }) test("execute counts its finished nested calls instead of itself", () => { @@ -219,7 +218,6 @@ test("execute counts its finished nested calls instead of itself", () => { expect(activitySummary([{ message, part: execute }], 0)).toEqual({ label: "2 reads, 1 tool", active: false, - failed: true, }) }) @@ -257,8 +255,8 @@ test("a thought still streaming counts as finished once a later row closes its g { message: open, part: { type: "reasoning" as const, text: "Planning", time: { created: 1 } } }, { message: open, part: tool("r1", "read") }, ] - expect(activitySummary(items, 0)).toEqual({ label: "1 read", active: true, failed: false }) - expect(activitySummary(items, 0, true)).toEqual({ label: "1 thought, 1 read", active: false, failed: false }) + expect(activitySummary(items, 0)).toEqual({ label: "1 read", active: true }) + expect(activitySummary(items, 0, true)).toEqual({ label: "1 thought, 1 read", active: false }) }) test("until something finishes, the label is the first running item's status", () => { @@ -271,7 +269,6 @@ test("until something finishes, the label is the first running item's status", ( expect(summarizeActivity(activity, () => open, [], false)).toEqual({ label: "Running edit…", active: true, - failed: false, }) })