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)) +}