From 540525e98ea41397b1ea9d0484622d99829e0f80 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 21 May 2026 10:42:18 -0400 Subject: [PATCH 1/7] fix(tui): align wrapped inline tool rows --- .../src/cli/cmd/tui/routes/session/index.tsx | 26 ++- .../inline-tool-wrap-snapshot.test.tsx.snap | 58 ++++++ .../tui/inline-tool-wrap-snapshot.test.tsx | 174 ++++++++++++++++++ 3 files changed, 253 insertions(+), 5 deletions(-) create mode 100644 packages/opencode/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap create mode 100644 packages/opencode/test/cli/tui/inline-tool-wrap-snapshot.test.tsx 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 245a7296b5..f9caef33c1 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1802,11 +1802,27 @@ function InlineTool(props: { - - ~ {props.pending}} when={props.complete}> - {props.icon} {props.children} - - + + ~ {props.pending} + + } + when={props.complete} + > + + + {props.icon} + + + {props.children} + + + 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 new file mode 100644 index 0000000000..664fa4e3f5 --- /dev/null +++ b/packages/opencode/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap @@ -0,0 +1,58 @@ +// 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`] = ` +"CURRENT: measured height adds top margin after wrapped rows + + * 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) + + +STABLE WRAP: no height-coupled margin + * 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) + + +HANGING INDENT: wrap aligns with tool text + * 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) + + +DETAIL ROWS: split identity from metadata + * Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data. + *dir|xdg|APPDATA" + packages/opencode/src - 151 matches + * Glob "**/*db*" + 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 =" + packages/opencode/src - 115 matches + + +COMPACT: truncate middle, never wrap + * Grep "OPENCODE.*DB|d...dir|xdg|APPDATA" - packages/openc...c - 151 + * Glob "**/*db*" - packages/opencode - 6 matches + -> Read packages/openco...rc/storage/db.ts - offset=1, limit=130 + -> Read packages/opencode/src/index.ts - offset=1, limit=100 + * Grep "export const O...th\\.data|data =" - packages/openc...c - 115" +`; 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 new file mode 100644 index 0000000000..0497fc462e --- /dev/null +++ b/packages/opencode/test/cli/tui/inline-tool-wrap-snapshot.test.tsx @@ -0,0 +1,174 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { createSignal, For } from "solid-js" +import { testRender } from "@opentui/solid" + +let testSetup: Awaited> | undefined + +afterEach(() => { + testSetup?.renderer.destroy() + testSetup = undefined +}) + +const tools = [ + { + kind: "grep", + icon: "*", + label: + 'Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.*dir|xdg|APPDATA" in packages/opencode/src (151 matches)', + target: '"OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.*dir|xdg|APPDATA"', + meta: "packages/opencode/src - 151 matches", + }, + { + kind: "glob", + icon: "*", + label: 'Glob "**/*db*" in packages/opencode (6 matches)', + target: '"**/*db*"', + meta: "packages/opencode - 6 matches", + }, + { + kind: "read", + icon: "->", + label: "Read packages/opencode/src/storage/db.ts [offset=1, limit=130]", + target: "packages/opencode/src/storage/db.ts", + meta: "offset=1, limit=130", + }, + { + kind: "read", + icon: "->", + label: "Read packages/opencode/src/index.ts [offset=1, limit=100]", + target: "packages/opencode/src/index.ts", + meta: "offset=1, limit=100", + }, + { + kind: "grep", + icon: "*", + label: + 'Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.Path\\.data|data =" in packages/opencode/src (115 matches)', + target: '"export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.Path\\.data|data ="', + meta: "packages/opencode/src - 115 matches", + }, +] as const + +function CurrentInlineRow(props: { item: (typeof tools)[number]; index: number }) { + const [margin, setMargin] = createSignal(0) + + return ( + 1) { + setMargin(1) + return + } + const previous = parent.getChildren()[parent.getChildren().indexOf(this) - 1] + if (!previous) { + setMargin(0) + return + } + if (previous.height > 1 || previous.id.startsWith("text-")) setMargin(1) + }} + > + + {props.item.icon} {props.item.label} + + + ) +} + +function StableInlineRow(props: { item: (typeof tools)[number] }) { + return ( + + + {props.item.icon} {props.item.label} + + + ) +} + +function HangingIndentRow(props: { item: (typeof tools)[number] }) { + return ( + + {props.item.icon} + {props.item.label} + + ) +} + +function DetailRow(props: { item: (typeof tools)[number] }) { + return ( + + + {props.item.icon} {titlecase(props.item.kind)} {props.item.target} + + {props.item.meta} + + ) +} + +function CompactRow(props: { item: (typeof tools)[number] }) { + return ( + + + {props.item.icon} {titlecase(props.item.kind)} {truncateMiddle(props.item.target, 34)} -{" "} + {truncateMiddle(props.item.meta, 32)} + + + ) +} + +function Fixture() { + return ( + + CURRENT: measured height adds top margin after wrapped rows + + {(item, index) => } + + STABLE WRAP: no height-coupled margin + + {(item) => } + + HANGING INDENT: wrap aligns with tool text + + {(item) => } + + DETAIL ROWS: split identity from metadata + + {(item) => } + + COMPACT: truncate middle, never wrap + + {(item) => } + + + ) +} + +describe("TUI inline tool wrapping", () => { + test("snapshots consecutive grep, glob, and read rows at a narrow width", async () => { + testSetup = await testRender(() => , { width: 72, height: 60 }) + await testSetup.renderOnce() + await testSetup.renderOnce() + + expect( + testSetup + .captureCharFrame() + .split("\n") + .map((line) => line.trimEnd()) + .join("\n") + .trimEnd(), + ).toMatchSnapshot() + }) +}) + +function titlecase(value: string) { + return value.slice(0, 1).toUpperCase() + value.slice(1) +} + +function truncateMiddle(value: string, max: number) { + if (value.length <= max) return value + return value.slice(0, Math.floor((max - 3) / 2)) + "..." + value.slice(value.length - Math.ceil((max - 3) / 2)) +} From 1499daf15f19055e9cc260038550809fc4c43ad3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 21 May 2026 11:00:35 -0400 Subject: [PATCH 2/7] test(tui): focus inline tool wrap snapshot --- .../inline-tool-wrap-snapshot.test.tsx.snap | 51 +------- .../tui/inline-tool-wrap-snapshot.test.tsx | 111 +----------------- 2 files changed, 6 insertions(+), 156 deletions(-) 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 664fa4e3f5..653935e088 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,58 +1,11 @@ // 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`] = ` -"CURRENT: measured height adds top margin after wrapped rows - - * 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) - - -STABLE WRAP: no height-coupled margin - * 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) - - -HANGING INDENT: wrap aligns with tool text - * Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data. +" * 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) - - -DETAIL ROWS: split identity from metadata - * Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data. - *dir|xdg|APPDATA" - packages/opencode/src - 151 matches - * Glob "**/*db*" - 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 =" - packages/opencode/src - 115 matches - - -COMPACT: truncate middle, never wrap - * Grep "OPENCODE.*DB|d...dir|xdg|APPDATA" - packages/openc...c - 151 - * Glob "**/*db*" - packages/opencode - 6 matches - -> Read packages/openco...rc/storage/db.ts - offset=1, limit=130 - -> Read packages/opencode/src/index.ts - offset=1, limit=100 - * Grep "export const O...th\\.data|data =" - packages/openc...c - 115" + Path\\.data|data =" in packages/opencode/src (115 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 0497fc462e..e5aa72d6fb 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 @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test" -import { createSignal, For } from "solid-js" +import { For } from "solid-js" import { testRender } from "@opentui/solid" let testSetup: Awaited> | undefined @@ -11,85 +11,30 @@ afterEach(() => { const tools = [ { - kind: "grep", icon: "*", label: 'Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.*dir|xdg|APPDATA" in packages/opencode/src (151 matches)', - target: '"OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.*dir|xdg|APPDATA"', - meta: "packages/opencode/src - 151 matches", }, { - kind: "glob", icon: "*", label: 'Glob "**/*db*" in packages/opencode (6 matches)', - target: '"**/*db*"', - meta: "packages/opencode - 6 matches", }, { - kind: "read", icon: "->", label: "Read packages/opencode/src/storage/db.ts [offset=1, limit=130]", - target: "packages/opencode/src/storage/db.ts", - meta: "offset=1, limit=130", }, { - kind: "read", icon: "->", label: "Read packages/opencode/src/index.ts [offset=1, limit=100]", - target: "packages/opencode/src/index.ts", - meta: "offset=1, limit=100", }, { - kind: "grep", icon: "*", label: 'Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.Path\\.data|data =" in packages/opencode/src (115 matches)', - target: '"export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.Path\\.data|data ="', - meta: "packages/opencode/src - 115 matches", }, ] as const -function CurrentInlineRow(props: { item: (typeof tools)[number]; index: number }) { - const [margin, setMargin] = createSignal(0) - - return ( - 1) { - setMargin(1) - return - } - const previous = parent.getChildren()[parent.getChildren().indexOf(this) - 1] - if (!previous) { - setMargin(0) - return - } - if (previous.height > 1 || previous.id.startsWith("text-")) setMargin(1) - }} - > - - {props.item.icon} {props.item.label} - - - ) -} - -function StableInlineRow(props: { item: (typeof tools)[number] }) { - return ( - - - {props.item.icon} {props.item.label} - - - ) -} - -function HangingIndentRow(props: { item: (typeof tools)[number] }) { +function InlineToolRow(props: { item: (typeof tools)[number] }) { return ( {props.item.icon} @@ -98,50 +43,11 @@ function HangingIndentRow(props: { item: (typeof tools)[number] }) { ) } -function DetailRow(props: { item: (typeof tools)[number] }) { - return ( - - - {props.item.icon} {titlecase(props.item.kind)} {props.item.target} - - {props.item.meta} - - ) -} - -function CompactRow(props: { item: (typeof tools)[number] }) { - return ( - - - {props.item.icon} {titlecase(props.item.kind)} {truncateMiddle(props.item.target, 34)} -{" "} - {truncateMiddle(props.item.meta, 32)} - - - ) -} - function Fixture() { return ( - CURRENT: measured height adds top margin after wrapped rows - {(item, index) => } - - STABLE WRAP: no height-coupled margin - - {(item) => } - - HANGING INDENT: wrap aligns with tool text - - {(item) => } - - DETAIL ROWS: split identity from metadata - - {(item) => } - - COMPACT: truncate middle, never wrap - - {(item) => } + {(item) => } ) @@ -149,7 +55,7 @@ function Fixture() { describe("TUI inline tool wrapping", () => { test("snapshots consecutive grep, glob, and read rows at a narrow width", async () => { - testSetup = await testRender(() => , { width: 72, height: 60 }) + testSetup = await testRender(() => , { width: 72, height: 12 }) await testSetup.renderOnce() await testSetup.renderOnce() @@ -163,12 +69,3 @@ describe("TUI inline tool wrapping", () => { ).toMatchSnapshot() }) }) - -function titlecase(value: string) { - return value.slice(0, 1).toUpperCase() + value.slice(1) -} - -function truncateMiddle(value: string, max: number) { - if (value.length <= max) return value - return value.slice(0, Math.floor((max - 3) / 2)) + "..." + value.slice(value.length - Math.ceil((max - 3) / 2)) -} From 939426828dde497382d710073b8d498cd153e5f3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 21 May 2026 14:53:03 -0400 Subject: [PATCH 3/7] test(tui): mirror inline tool padding in snapshot --- .../inline-tool-wrap-snapshot.test.tsx.snap | 14 +++++++------- .../cli/tui/inline-tool-wrap-snapshot.test.tsx | 8 +++++--- 2 files changed, 12 insertions(+), 10 deletions(-) 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 653935e088..8662b81348 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,11 +1,11 @@ // 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)" +" * 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)" `; 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 e5aa72d6fb..72388c5560 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 @@ -36,9 +36,11 @@ const tools = [ function InlineToolRow(props: { item: (typeof tools)[number] }) { return ( - - {props.item.icon} - {props.item.label} + + + {props.item.icon} + {props.item.label} + ) } From eb5d30257a1074f481ad98204a1d8b6aab7a0048 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 21 May 2026 15:00:52 -0400 Subject: [PATCH 4/7] fix(tui): preserve inline tool left edge --- .../src/cli/cmd/tui/routes/session/index.tsx | 6 ++++-- .../inline-tool-wrap-snapshot.test.tsx.snap | 15 ++++++++------- .../cli/tui/inline-tool-wrap-snapshot.test.tsx | 14 +++++++++++--- 3 files changed, 23 insertions(+), 12 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 f9caef33c1..43c0e41641 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1810,7 +1810,7 @@ function InlineTool(props: { } when={props.complete} > - + - {error()} + + {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 8662b81348..e2f5d53037 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,11 +1,12 @@ // 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)" +" * 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] + No LSP server available for this file type. + * Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\. + Path\\.data|data =" in packages/opencode/src (115 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 72388c5560..99592e9b9f 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 @@ -9,7 +9,9 @@ afterEach(() => { testSetup = undefined }) -const tools = [ +type ToolFixture = { icon: string; label: string; error?: string } + +const tools: readonly ToolFixture[] = [ { icon: "*", label: @@ -26,6 +28,7 @@ const tools = [ { icon: "->", label: "Read packages/opencode/src/index.ts [offset=1, limit=100]", + error: "No LSP server available for this file type.", }, { icon: "*", @@ -34,13 +37,18 @@ const tools = [ }, ] as const -function InlineToolRow(props: { item: (typeof tools)[number] }) { +function InlineToolRow(props: { item: ToolFixture }) { return ( - + {props.item.icon} {props.item.label} + {props.item.error && ( + + {props.item.error} + + )} ) } From c9426d3a57e2439c4e41feac388271b4576cfe9d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 21 May 2026 15:04:41 -0400 Subject: [PATCH 5/7] fix(tui): keep wrapped tool rows dense --- .../src/cli/cmd/tui/routes/session/index.tsx | 13 +------------ .../cli/tui/inline-tool-wrap-snapshot.test.tsx | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 14 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 43c0e41641..de7395f109 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1780,21 +1780,10 @@ function InlineTool(props: { if (!parent) { return } - if (el.height > 1) { - setMargin(1) - return - } const children = parent.getChildren() const index = children.indexOf(el) const previous = children[index - 1] - if (!previous) { - setMargin(0) - return - } - if (previous.height > 1 || previous.id.startsWith("text-")) { - setMargin(1) - return - } + setMargin(previous?.id.startsWith("text-") ? 1 : 0) }} > 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 99592e9b9f..8e968bc6a5 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 @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test" -import { For } from "solid-js" +import { createSignal, For } from "solid-js" import { testRender } from "@opentui/solid" let testSetup: Awaited> | undefined @@ -38,8 +38,18 @@ const tools: readonly ToolFixture[] = [ ] as const function InlineToolRow(props: { item: ToolFixture }) { + const [margin, setMargin] = createSignal(0) + return ( - + {props.item.icon} {props.item.label} From e03ffb9ed4fd2daa32a3c415886a45aee5c37a34 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 21 May 2026 15:53:49 -0400 Subject: [PATCH 6/7] refactor(tui): use fixed inline tool icon cell --- .../src/cli/cmd/tui/routes/session/index.tsx | 6 ++++-- .../inline-tool-wrap-snapshot.test.tsx.snap | 12 ++++++------ .../cli/tui/inline-tool-wrap-snapshot.test.tsx | 16 +++++++++------- 3 files changed, 19 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 de7395f109..bbd5eac1a2 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1497,6 +1497,8 @@ const PART_MAPPING = { reasoning: ReasoningPart, } +const INLINE_TOOL_ICON_WIDTH = 2 + function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: AssistantMessage }) { const { theme, subtleSyntax } = useTheme() const ctx = use() @@ -1801,7 +1803,7 @@ function InlineTool(props: { > @@ -1815,7 +1817,7 @@ function InlineTool(props: { - + {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 e2f5d53037..89987234dd 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,12 +1,12 @@ // 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. +" ✱ 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] - No LSP server available for this file type. - * Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\. + ✱ 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] + No LSP server available for this file type. + ✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\. Path\\.data|data =" in packages/opencode/src (115 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 8e968bc6a5..318847e946 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 @@ -11,27 +11,29 @@ afterEach(() => { type ToolFixture = { icon: string; label: string; error?: string } +const INLINE_TOOL_ICON_WIDTH = 2 + const tools: readonly ToolFixture[] = [ { - icon: "*", + icon: "✱", label: 'Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.*dir|xdg|APPDATA" in packages/opencode/src (151 matches)', }, { - icon: "*", + icon: "✱", label: 'Glob "**/*db*" in packages/opencode (6 matches)', }, { - icon: "->", + icon: "→", label: "Read packages/opencode/src/storage/db.ts [offset=1, limit=130]", }, { - icon: "->", + icon: "→", label: "Read packages/opencode/src/index.ts [offset=1, limit=100]", error: "No LSP server available for this file type.", }, { - icon: "*", + icon: "✱", label: 'Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.Path\\.data|data =" in packages/opencode/src (115 matches)', }, @@ -51,11 +53,11 @@ function InlineToolRow(props: { item: ToolFixture }) { }} > - {props.item.icon} + {props.item.icon} {props.item.label} {props.item.error && ( - + {props.item.error} )} From dd4aa2ab5a8d4564a73b9413aad17ec9d109afdf Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 21 May 2026 15:56:53 -0400 Subject: [PATCH 7/7] 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() + }) })