From 5979c61ca0de5868ecc88af990031f311a0fe9d1 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 13 Jul 2026 16:19:45 -0400 Subject: [PATCH 1/5] feat(tui): style background subagent indicator --- packages/tui/src/routes/session/index.tsx | 10 +++++++--- .../test/cli/tui/inline-tool-wrap-snapshot.test.tsx | 7 ++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 788bbbe8e1f6..e4c7e0182504 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -2367,9 +2367,11 @@ function WebSearch(props: ToolProps) { function Subagent(props: ToolProps) { const { navigate } = useRoute() + const { theme } = useTheme() const data = useData() const sessionID = createMemo(() => stringValue(props.metadata.sessionID) ?? stringValue(props.metadata.sessionId)) const description = createMemo(() => stringValue(props.input.description)) + const background = createMemo(() => props.input.background === true || props.metadata.status === "running") const isRunning = createMemo(() => { const id = sessionID() return props.part.state.status === "running" || Boolean(id && data.session.status(id) === "running") @@ -2390,14 +2392,16 @@ function Subagent(props: ToolProps) { {formatSubagentTitle( Locale.titlecase(stringValue(props.input.agent) ?? stringValue(props.input.subagent_type) ?? "General"), description() ?? "Subagent", - props.input.background === true || props.metadata.status === "running", )} + + Background + ) } -export function formatSubagentTitle(agent: string, description: string, background: boolean) { - return `${agent} Subagent — ${description}${background ? " [background]" : ""}` +export function formatSubagentTitle(agent: string, description: string) { + return `${agent} Subagent — ${description}` } export function formatSubagentRetry(attempt: number, message: string) { diff --git a/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx b/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx index 2ff61fa1c392..f2d5f74b9daa 100644 --- a/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +++ b/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx @@ -180,11 +180,8 @@ describe("TUI inline tool wrapping", () => { ).toEqual([{ message: "valid", range: { start: { line: 2, character: 3 } } }]) }) - test("keeps background state attached to the subagent identity", () => { - expect(formatSubagentTitle("Explore", "Inspect renderer", false)).toBe("Explore Subagent — Inspect renderer") - expect(formatSubagentTitle("Explore", "Inspect renderer", true)).toBe( - "Explore Subagent — Inspect renderer [background]", - ) + test("keeps status styling separate from the subagent title", () => { + expect(formatSubagentTitle("Explore", "Inspect renderer")).toBe("Explore Subagent — Inspect renderer") }) test("keeps retry status ahead of wrapping messages", () => { From 0a35c2f37f2f1a07eabb22232b83ce8b00649989 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 13 Jul 2026 16:34:32 -0400 Subject: [PATCH 2/5] fix(tui): separate background subagent status --- packages/tui/src/routes/session/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index e4c7e0182504..3cbdb1f9d883 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -2394,7 +2394,8 @@ function Subagent(props: ToolProps) { description() ?? "Subagent", )} - Background + {" "} + Background ) From a988946476ac8757ca05eb76b3458c7d507ba077 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 13 Jul 2026 17:18:40 -0400 Subject: [PATCH 3/5] fix(tui): wrap trailing tool status atomically --- packages/tui/src/component/spinner.tsx | 21 ++++++--- packages/tui/src/routes/session/index.tsx | 47 ++++++++++++++----- .../tui/inline-tool-wrap-snapshot.test.tsx | 26 ++++++++++ 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/packages/tui/src/component/spinner.tsx b/packages/tui/src/component/spinner.tsx index 5ba9bc31334b..04bed120ad6c 100644 --- a/packages/tui/src/component/spinner.tsx +++ b/packages/tui/src/component/spinner.tsx @@ -9,18 +9,25 @@ registerOpencodeSpinner() export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] -export function Spinner(props: { children?: JSX.Element; color?: RGBA }) { +export function Spinner(props: { children?: JSX.Element; color?: RGBA; trailing?: JSX.Element }) { const { theme } = useTheme() const config = useConfig().data const color = () => props.color ?? theme.textMuted return ( - ⋯ {props.children}}> - + + ⋯}> - - {props.children} + + + {props.children}}> + {(trailing) => ( + + {props.children} + {trailing()} + + )} - - + + ) } diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 3cbdb1f9d883..0073d50df93f 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1978,6 +1978,7 @@ function InlineTool(props: { pending: string failure?: string spinner?: boolean + trailing?: JSX.Element children: JSX.Element part: SessionMessageAssistantTool onClick?: () => void @@ -2028,6 +2029,7 @@ function InlineTool(props: { pending={props.pending} failure={props.failure} spinner={props.spinner} + trailing={props.trailing} onMouseOver={() => clickable() && setHover(true)} onMouseOut={() => setHover(false)} onMouseUp={() => { @@ -2057,6 +2059,7 @@ export function InlineToolRow(props: { pending: string failure?: string spinner?: boolean + trailing?: JSX.Element children: JSX.Element onMouseOver?: () => void onMouseOut?: () => void @@ -2066,7 +2069,7 @@ export function InlineToolRow(props: { - + {props.pending}} when={props.complete || props.failed}> @@ -2078,13 +2081,32 @@ export function InlineToolRow(props: { > {props.icon} - + {props.failed && !props.complete ? (props.failure ?? props.children) : props.children} + + } > - {props.failed && !props.complete ? (props.failure ?? props.children) : props.children} - + {(trailing) => ( + + + {props.failed && !props.complete ? (props.failure ?? props.children) : props.children} + + {trailing()} + + )} + @@ -2388,15 +2410,18 @@ function Subagent(props: ToolProps) { const id = sessionID() if (id) navigate({ type: "session", sessionID: id }) }} + trailing={ + background() ? ( + + Background + + ) : undefined + } > {formatSubagentTitle( Locale.titlecase(stringValue(props.input.agent) ?? stringValue(props.input.subagent_type) ?? "General"), description() ?? "Subagent", )} - - {" "} - Background - ) } diff --git a/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx b/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx index f2d5f74b9daa..1338b8e68f87 100644 --- a/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +++ b/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx @@ -99,6 +99,23 @@ function ReminderAlignmentFixture() { ) } +function TrailingStatusFixture() { + return ( + + Background + + } + > + Explore Subagent — Inspect renderer status styling + + ) +} + async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) { testSetup = await testRender(component, options) await testSetup.renderOnce() @@ -142,6 +159,15 @@ describe("TUI inline tool wrapping", () => { ) }) + test("wraps a trailing status as one padded item", async () => { + expect(await renderFrame(() => , { width: 70, height: 2 })).toBe( + " : Explore Subagent — Inspect renderer status styling Background", + ) + expect(await renderFrame(() => , { width: 62, height: 2 })).toBe( + " : Explore Subagent — Inspect renderer status styling\n Background", + ) + }) + test("filters malformed nested tool wire data", () => { expect( parseApplyPatchFiles([ From 4f899b117edc484a131d7365ffe41b8d5f3ee5ba Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 13 Jul 2026 17:27:29 -0400 Subject: [PATCH 4/5] fix(tui): paint status pill padding --- packages/tui/src/routes/session/index.tsx | 5 +++-- .../cli/tui/inline-tool-wrap-snapshot.test.tsx | 15 +++------------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 0073d50df93f..3e663049e210 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -2412,8 +2412,9 @@ function Subagent(props: ToolProps) { }} trailing={ background() ? ( - - Background + + {" "} + Background{" "} ) : undefined } diff --git a/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx b/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx index 1338b8e68f87..7fc12c084c7d 100644 --- a/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +++ b/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx @@ -101,16 +101,7 @@ function ReminderAlignmentFixture() { function TrailingStatusFixture() { return ( - - Background - - } - > + Background }> Explore Subagent — Inspect renderer status styling ) @@ -161,10 +152,10 @@ describe("TUI inline tool wrapping", () => { test("wraps a trailing status as one padded item", async () => { expect(await renderFrame(() => , { width: 70, height: 2 })).toBe( - " : Explore Subagent — Inspect renderer status styling Background", + " : Explore Subagent — Inspect renderer status styling Background", ) expect(await renderFrame(() => , { width: 62, height: 2 })).toBe( - " : Explore Subagent — Inspect renderer status styling\n Background", + " : Explore Subagent — Inspect renderer status styling\n Background", ) }) From 00286c1b4e0dc4af395f756adc6f7fd639490ab2 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 13 Jul 2026 17:47:34 -0400 Subject: [PATCH 5/5] refactor(tui): simplify inline status layout --- packages/tui/src/component/spinner.tsx | 24 +++-- packages/tui/src/routes/session/index.tsx | 89 +++++++++++-------- .../tui/inline-tool-wrap-snapshot.test.tsx | 8 +- 3 files changed, 65 insertions(+), 56 deletions(-) diff --git a/packages/tui/src/component/spinner.tsx b/packages/tui/src/component/spinner.tsx index 04bed120ad6c..4a57308ba542 100644 --- a/packages/tui/src/component/spinner.tsx +++ b/packages/tui/src/component/spinner.tsx @@ -9,25 +9,21 @@ registerOpencodeSpinner() export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] -export function Spinner(props: { children?: JSX.Element; color?: RGBA; trailing?: JSX.Element }) { +export function Spinner(props: { children?: JSX.Element; color?: RGBA }) { const { theme } = useTheme() const config = useConfig().data const color = () => props.color ?? theme.textMuted return ( - - ⋯}> + {props.children ? <>⋯ {props.children} : "⋯"}} + > + - - - {props.children}}> - {(trailing) => ( - - {props.children} - {trailing()} - - )} + + {props.children} - - + + ) } diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 3e663049e210..5168de2729ee 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1978,7 +1978,7 @@ function InlineTool(props: { pending: string failure?: string spinner?: boolean - trailing?: JSX.Element + status?: JSX.Element children: JSX.Element part: SessionMessageAssistantTool onClick?: () => void @@ -2029,7 +2029,7 @@ function InlineTool(props: { pending={props.pending} failure={props.failure} spinner={props.spinner} - trailing={props.trailing} + status={props.status} onMouseOver={() => clickable() && setHover(true)} onMouseOut={() => setHover(false)} onMouseUp={() => { @@ -2059,7 +2059,7 @@ export function InlineToolRow(props: { pending: string failure?: string spinner?: boolean - trailing?: JSX.Element + status?: JSX.Element children: JSX.Element onMouseOver?: () => void onMouseOut?: () => void @@ -2069,7 +2069,16 @@ export function InlineToolRow(props: { - + }> + {(status) => ( + + + + {props.children} + + + )} + {props.pending}} when={props.complete || props.failed}> @@ -2082,7 +2091,7 @@ export function InlineToolRow(props: { {props.icon} } > - {(trailing) => ( - - - {props.failed && !props.complete ? (props.failure ?? props.children) : props.children} - - {trailing()} - + {(status) => ( + + {props.failed && !props.complete ? (props.failure ?? props.children) : props.children} + )} @@ -2120,6 +2125,32 @@ export function InlineToolRow(props: { ) } +function InlineToolLabel(props: { color?: RGBA; denied?: boolean; status: JSX.Element; children: JSX.Element }) { + return ( + + + {props.children} + + {props.status} + + ) +} + +function StatusBadge(props: { children: string }) { + const { theme } = useTheme() + return ( + + {" "} + {props.children}{" "} + + ) +} + function BlockTool(props: { title?: string path?: { label: string; value: string } @@ -2263,9 +2294,7 @@ function Shell(props: ToolProps) { - - Background - + Background {expanded() ? "Click to collapse" : "Click to expand"} @@ -2389,11 +2418,9 @@ function WebSearch(props: ToolProps) { function Subagent(props: ToolProps) { const { navigate } = useRoute() - const { theme } = useTheme() const data = useData() const sessionID = createMemo(() => stringValue(props.metadata.sessionID) ?? stringValue(props.metadata.sessionId)) const description = createMemo(() => stringValue(props.input.description)) - const background = createMemo(() => props.input.background === true || props.metadata.status === "running") const isRunning = createMemo(() => { const id = sessionID() return props.part.state.status === "running" || Boolean(id && data.session.status(id) === "running") @@ -2410,27 +2437,17 @@ function Subagent(props: ToolProps) { const id = sessionID() if (id) navigate({ type: "session", sessionID: id }) }} - trailing={ - background() ? ( - - {" "} - Background{" "} - + status={ + props.input.background === true || props.metadata.status === "running" ? ( + Background ) : undefined } > - {formatSubagentTitle( - Locale.titlecase(stringValue(props.input.agent) ?? stringValue(props.input.subagent_type) ?? "General"), - description() ?? "Subagent", - )} + {`${Locale.titlecase(stringValue(props.input.agent) ?? stringValue(props.input.subagent_type) ?? "General")} Subagent — ${description() ?? "Subagent"}`} ) } -export function formatSubagentTitle(agent: string, description: string) { - return `${agent} Subagent — ${description}` -} - export function formatSubagentRetry(attempt: number, message: string) { return `Retrying (attempt ${attempt}) · ${message}` } diff --git a/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx b/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx index 7fc12c084c7d..24b741eb8de9 100644 --- a/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +++ b/packages/tui/test/cli/tui/inline-tool-wrap-snapshot.test.tsx @@ -3,7 +3,6 @@ import { For } from "solid-js" import { testRender, type JSX } from "@opentui/solid" import { formatSubagentRetry, - formatSubagentTitle, InlineToolRow, parseApplyPatchFiles, parseDiagnostics, @@ -101,13 +100,14 @@ function ReminderAlignmentFixture() { function TrailingStatusFixture() { return ( - Background }> + Background }> Explore Subagent — Inspect renderer status styling ) } async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) { + testSetup?.renderer.destroy() testSetup = await testRender(component, options) await testSetup.renderOnce() await testSetup.renderOnce() @@ -197,10 +197,6 @@ describe("TUI inline tool wrapping", () => { ).toEqual([{ message: "valid", range: { start: { line: 2, character: 3 } } }]) }) - test("keeps status styling separate from the subagent title", () => { - expect(formatSubagentTitle("Explore", "Inspect renderer")).toBe("Explore Subagent — Inspect renderer") - }) - test("keeps retry status ahead of wrapping messages", () => { expect(formatSubagentRetry(2, "Rate limited by provider")).toBe("Retrying (attempt 2) · Rate limited by provider") })