refactor(tui): simplify inline tool spacing (#33097)

This commit is contained in:
Dax 2026-06-20 15:35:20 +02:00 committed by GitHub
commit 009f3799cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 62 additions and 72 deletions

View file

@ -3,6 +3,7 @@
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]
@ -13,10 +14,12 @@ exports[`TUI inline tool wrapping snapshots consecutive grep, glob, and read row
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)
→ 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)"
`;
@ -32,6 +35,7 @@ exports[`TUI inline tool wrapping keeps separation after a shell output block 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]
@ -46,6 +50,7 @@ exports[`TUI inline tool wrapping keeps separation after a padded user message 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]
@ -53,9 +58,8 @@ exports[`TUI inline tool wrapping keeps separation after a padded user message 1
Path\\.data|data =" in packages/opencode/src (115 matches)"
`;
exports[`TUI inline tool wrapping separates a contiguous subagent group from inline tools 1`] = `
exports[`TUI inline tool wrapping separates after a multi-line task row 1`] = `
" ✱ Grep "Task" (2 matches)
⠙ Explore Task — Inspect active task spacing
✓ General Task — Confirm completed task spacing
↳ 1 toolcall · 501ms
@ -63,22 +67,21 @@ exports[`TUI inline tool wrapping separates a contiguous subagent group from inl
→ Read src/cli/cmd/tui/routes/session/index.tsx"
`;
exports[`TUI inline tool wrapping separates a subagent group after an expanded read 1`] = `
exports[`TUI inline tool wrapping does not treat task rows differently from other inline rows 1`] = `
" → Read src/cli/cmd/tui/routes/session/index.tsx
↳ Loaded src/cli/cmd/tui/routes/session/tools.tsx
✓ Explore Task — Inspect active task spacing
↳ 1 toolcall · 501ms"
`;
exports[`TUI inline tool wrapping separates a subagent from the previous assistant summary 1`] = `
exports[`TUI inline tool wrapping separates an inline row from the previous assistant summary 1`] = `
" ▣ Build · Little Frank · 53.1s
✓ Build Task — Review changes
↳ 48 toolcalls · 1m 40s"
`;
exports[`TUI inline tool wrapping separates a subagent from the previous assistant error 1`] = `
exports[`TUI inline tool wrapping separates an inline row from the previous assistant error 1`] = `
"│
│ Managed inference requires an active Member plan

View file

@ -1,6 +1,6 @@
import { afterEach, describe, expect, test } from "bun:test"
import { createSignal, For, Show } from "solid-js"
import type { ScrollBoxRenderable } from "@opentui/core"
import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core"
import { testRender, type JSX } from "@opentui/solid"
import {
formatCompletedSubagentDetail,
@ -13,6 +13,7 @@ import {
parseQuestionAnswers,
parseQuestions,
parseTodos,
alwaysSeparate,
toolDisplay,
} from "../../../src/routes/session"
@ -53,7 +54,14 @@ const tools: readonly ToolFixture[] = [
function ShellOutput() {
return (
<box id="tool-block-shell" marginTop={1} paddingTop={1} paddingBottom={1} paddingLeft={2} gap={1}>
<box
ref={(el: BoxRenderable) => alwaysSeparate.add(el)}
marginTop={1}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
gap={1}
>
<text paddingLeft={3}># List files</text>
<box gap={1}>
<text>$ ls</text>
@ -65,7 +73,7 @@ function ShellOutput() {
function UserMessage() {
return (
<box id="message-user">
<box ref={(el: BoxRenderable) => alwaysSeparate.add(el)}>
<box paddingTop={1} paddingBottom={1} paddingLeft={2}>
<text>Check whether the next tool remains separated.</text>
</box>
@ -88,7 +96,6 @@ function Fixture(props: { errorExpanded?: boolean; before?: "shell" | "user" })
failed={Boolean(item.error)}
error={item.error}
errorExpanded={props.errorExpanded}
separateAfter={(id) => id === "message-user"}
>
{item.label}
</InlineToolRow>
@ -99,61 +106,67 @@ function Fixture(props: { errorExpanded?: boolean; before?: "shell" | "user" })
)
}
function SubagentGroupFixture() {
function TaskRowsFixture() {
return (
<box flexDirection="column" width={72}>
<InlineToolRow id="tool-inline-before" icon="✱" complete={true} pending="">
<InlineToolRow icon="✱" complete={true} pending="">
Grep "Task" (2 matches)
</InlineToolRow>
<InlineToolRow id="tool-inline-subagent-one" icon="⠙" complete={true} pending="" subagent={true}>
<InlineToolRow icon="⠙" complete={true} pending="">
Explore Task Inspect active task spacing
</InlineToolRow>
<InlineToolRow id="tool-inline-subagent-two" icon="✓" complete={true} pending="" subagent={true}>
<InlineToolRow icon="✓" complete={true} pending="">
{"General Task — Confirm completed task spacing\n↳ 1 toolcall · 501ms"}
</InlineToolRow>
<InlineToolRow id="tool-inline-after" icon="→" complete={true} pending="">
<InlineToolRow icon="→" complete={true} pending="">
Read src/cli/cmd/tui/routes/session/index.tsx
</InlineToolRow>
</box>
)
}
function LoadedReadBeforeSubagentFixture() {
function LoadedReadBeforeTaskFixture() {
return (
<box flexDirection="column" width={72}>
<InlineToolRow id="tool-inline-read" icon="→" complete={true} pending="">
<InlineToolRow icon="→" complete={true} pending="">
Read src/cli/cmd/tui/routes/session/index.tsx
</InlineToolRow>
<box id="tool-inline-loaded-read-child" paddingLeft={3}>
<box paddingLeft={3}>
<text paddingLeft={3}> Loaded src/cli/cmd/tui/routes/session/tools.tsx</text>
</box>
<InlineToolRow id="tool-inline-subagent-after-read" icon="✓" complete={true} pending="" subagent={true}>
<InlineToolRow icon="✓" complete={true} pending="">
{"Explore Task — Inspect active task spacing\n↳ 1 toolcall · 501ms"}
</InlineToolRow>
</box>
)
}
function AssistantSummaryBeforeSubagentFixture() {
function AssistantSummaryBeforeInlineFixture() {
return (
<box flexDirection="column" width={72}>
<box id="assistant-summary-message-one" paddingLeft={3}>
<box ref={(el: BoxRenderable) => alwaysSeparate.add(el)} paddingLeft={3}>
<text> Build · Little Frank · 53.1s</text>
</box>
<InlineToolRow id="tool-inline-subagent-one" icon="✓" complete={true} pending="" subagent={true}>
<InlineToolRow icon="✓" complete={true} pending="">
{"Build Task — Review changes\n↳ 48 toolcalls · 1m 40s"}
</InlineToolRow>
</box>
)
}
function AssistantErrorBeforeSubagentFixture() {
function AssistantErrorBeforeInlineFixture() {
return (
<box flexDirection="column" width={72}>
<box id="assistant-error-message-one" border={["left"]} paddingTop={1} paddingBottom={1} paddingLeft={2}>
<box
ref={(el: BoxRenderable) => alwaysSeparate.add(el)}
border={["left"]}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
>
<text>Managed inference requires an active Member plan</text>
</box>
<InlineToolRow id="tool-inline-subagent-one" icon="✓" complete={true} pending="" subagent={true}>
<InlineToolRow icon="✓" complete={true} pending="">
{"Build Task — Review changes\n↳ 48 toolcalls · 1m 40s"}
</InlineToolRow>
</box>
@ -170,7 +183,7 @@ function StickyScrollFixture(props: { separated: boolean; scroll: (scroll: Scrol
<text>Second row</text>
</box>
<Show when={props.separated}>
<box id="text-before-tool">
<box ref={(el: BoxRenderable) => alwaysSeparate.add(el)}>
<text>Assistant text</text>
</box>
</Show>
@ -200,6 +213,7 @@ function FailedCompleteToolFixture() {
async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) {
testSetup = await testRender(component, options)
await testSetup.renderOnce()
await testSetup.renderOnce()
return testSetup
.captureCharFrame()
@ -294,22 +308,20 @@ describe("TUI inline tool wrapping", () => {
expect(await renderFrame(() => <Fixture before="user" />, { width: 72, height: 14 })).toMatchSnapshot()
})
test("separates a contiguous subagent group from inline tools", async () => {
expect(await renderFrame(() => <SubagentGroupFixture />, { width: 72, height: 10 })).toMatchSnapshot()
test("separates after a multi-line task row", async () => {
expect(await renderFrame(() => <TaskRowsFixture />, { width: 72, height: 10 })).toMatchSnapshot()
})
test("separates a subagent group after an expanded read", async () => {
expect(await renderFrame(() => <LoadedReadBeforeSubagentFixture />, { width: 72, height: 8 })).toMatchSnapshot()
test("does not treat task rows differently from other inline rows", async () => {
expect(await renderFrame(() => <LoadedReadBeforeTaskFixture />, { width: 72, height: 8 })).toMatchSnapshot()
})
test("separates a subagent from the previous assistant summary", async () => {
expect(
await renderFrame(() => <AssistantSummaryBeforeSubagentFixture />, { width: 72, height: 5 }),
).toMatchSnapshot()
test("separates an inline row from the previous assistant summary", async () => {
expect(await renderFrame(() => <AssistantSummaryBeforeInlineFixture />, { width: 72, height: 5 })).toMatchSnapshot()
})
test("separates a subagent from the previous assistant error", async () => {
expect(await renderFrame(() => <AssistantErrorBeforeSubagentFixture />, { width: 72, height: 7 })).toMatchSnapshot()
test("separates an inline row from the previous assistant error", async () => {
expect(await renderFrame(() => <AssistantErrorBeforeInlineFixture />, { width: 72, height: 7 })).toMatchSnapshot()
})
test("updates sticky-bottom geometry when a text separator mounts and unmounts", async () => {