From dd4aa2ab5a8d4564a73b9413aad17ec9d109afdf Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 21 May 2026 15:56:53 -0400 Subject: [PATCH] fix(tui): collapse inline tool errors --- .../src/cli/cmd/tui/routes/session/index.tsx | 34 +++++++++++++------ .../inline-tool-wrap-snapshot.test.tsx.snap | 10 ++++++ .../tui/inline-tool-wrap-snapshot.test.tsx | 23 ++++++++++--- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index bbd5eac1a2..6f18f02a98 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1742,6 +1742,7 @@ function InlineTool(props: { const sync = useSync() const renderer = useRenderer() const [hover, setHover] = createSignal(false) + const [errorExpanded, setErrorExpanded] = createSignal(false) const permission = createMemo(() => { const callID = sync.data.permission[ctx.sessionID]?.at(0)?.tool?.callID @@ -1749,13 +1750,6 @@ function InlineTool(props: { return callID === props.part.callID }) - const fg = createMemo(() => { - if (permission()) return theme.warning - if (hover() && props.onClick) return theme.text - if (props.complete) return theme.textMuted - return theme.text - }) - const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error : undefined)) const denied = createMemo( @@ -1766,14 +1760,28 @@ function InlineTool(props: { error()?.includes("user dismissed"), ) + const failed = createMemo(() => Boolean(error() && !denied())) + const clickable = createMemo(() => Boolean(props.onClick || failed())) + const fg = createMemo(() => { + if (permission()) return theme.warning + if (failed()) return theme.error + if (hover() && props.onClick) return theme.text + if (props.complete) return theme.textMuted + return theme.text + }) + return ( props.onClick && setHover(true)} + onMouseOver={() => clickable() && setHover(true)} onMouseOut={() => setHover(false)} onMouseUp={() => { if (renderer.getSelection()?.getSelectedText()) return + if (failed()) { + setErrorExpanded((value) => !value) + return + } props.onClick?.() }} renderBefore={function () { @@ -1804,19 +1812,23 @@ function InlineTool(props: { {props.icon} - + {props.children} - + {error()} diff --git a/packages/opencode/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap b/packages/opencode/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap index 89987234dd..6cb0df1e2e 100644 --- a/packages/opencode/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +++ b/packages/opencode/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap @@ -1,6 +1,16 @@ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`TUI inline tool wrapping snapshots consecutive grep, glob, and read rows at a narrow width 1`] = ` +" ✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data. + *dir|xdg|APPDATA" in packages/opencode/src (151 matches) + ✱ Glob "**/*db*" in packages/opencode (6 matches) + → Read packages/opencode/src/storage/db.ts [offset=1, limit=130] + → Read packages/opencode/src/index.ts [offset=1, limit=100] + ✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\. + Path\\.data|data =" in packages/opencode/src (115 matches)" +`; + +exports[`TUI inline tool wrapping snapshots expanded tool errors under the tool text 1`] = ` " ✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data. *dir|xdg|APPDATA" in packages/opencode/src (151 matches) ✱ Glob "**/*db*" in packages/opencode (6 matches) diff --git a/packages/opencode/test/cli/tui/inline-tool-wrap-snapshot.test.tsx b/packages/opencode/test/cli/tui/inline-tool-wrap-snapshot.test.tsx index 318847e946..c7aa4708ec 100644 --- a/packages/opencode/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +++ b/packages/opencode/test/cli/tui/inline-tool-wrap-snapshot.test.tsx @@ -39,7 +39,7 @@ const tools: readonly ToolFixture[] = [ }, ] as const -function InlineToolRow(props: { item: ToolFixture }) { +function InlineToolRow(props: { item: ToolFixture; errorExpanded?: boolean }) { const [margin, setMargin] = createSignal(0) return ( @@ -56,7 +56,7 @@ function InlineToolRow(props: { item: ToolFixture }) { {props.item.icon} {props.item.label} - {props.item.error && ( + {props.item.error && props.errorExpanded && ( {props.item.error} @@ -65,11 +65,11 @@ function InlineToolRow(props: { item: ToolFixture }) { ) } -function Fixture() { +function Fixture(props: { errorExpanded?: boolean }) { return ( - {(item) => } + {(item) => } ) @@ -90,4 +90,19 @@ describe("TUI inline tool wrapping", () => { .trimEnd(), ).toMatchSnapshot() }) + + test("snapshots expanded tool errors under the tool text", async () => { + testSetup = await testRender(() => , { width: 72, height: 12 }) + await testSetup.renderOnce() + await testSetup.renderOnce() + + expect( + testSetup + .captureCharFrame() + .split("\n") + .map((line) => line.trimEnd()) + .join("\n") + .trimEnd(), + ).toMatchSnapshot() + }) })