diff --git a/packages/tui/src/component/spinner.tsx b/packages/tui/src/component/spinner.tsx index 5ba9bc31334b..4a57308ba542 100644 --- a/packages/tui/src/component/spinner.tsx +++ b/packages/tui/src/component/spinner.tsx @@ -14,7 +14,10 @@ export function Spinner(props: { children?: JSX.Element; color?: RGBA }) { const config = useConfig().data const color = () => props.color ?? theme.textMuted return ( - ⋯ {props.children}}> + {props.children ? <>⋯ {props.children} : "⋯"}} + > diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 788bbbe8e1f6..5168de2729ee 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 + status?: 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} + status={props.status} onMouseOver={() => clickable() && setHover(true)} onMouseOut={() => setHover(false)} onMouseUp={() => { @@ -2057,6 +2059,7 @@ export function InlineToolRow(props: { pending: string failure?: string spinner?: boolean + status?: JSX.Element children: JSX.Element onMouseOver?: () => void onMouseOut?: () => void @@ -2066,7 +2069,16 @@ export function InlineToolRow(props: { - + }> + {(status) => ( + + + + {props.children} + + + )} + {props.pending}} when={props.complete || props.failed}> @@ -2078,13 +2090,28 @@ 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} - + {(status) => ( + + {props.failed && !props.complete ? (props.failure ?? props.children) : props.children} + + )} + @@ -2098,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 } @@ -2241,9 +2294,7 @@ function Shell(props: ToolProps) { - - Background - + Background {expanded() ? "Click to collapse" : "Click to expand"} @@ -2386,20 +2437,17 @@ function Subagent(props: ToolProps) { const id = sessionID() if (id) navigate({ type: "session", sessionID: id }) }} + 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", - props.input.background === true || props.metadata.status === "running", - )} + {`${Locale.titlecase(stringValue(props.input.agent) ?? stringValue(props.input.subagent_type) ?? "General")} Subagent — ${description() ?? "Subagent"}`} ) } -export function formatSubagentTitle(agent: string, description: string, background: boolean) { - return `${agent} Subagent — ${description}${background ? " [background]" : ""}` -} - 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 2ff61fa1c392..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, @@ -99,7 +98,16 @@ function ReminderAlignmentFixture() { ) } +function TrailingStatusFixture() { + return ( + 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() @@ -142,6 +150,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([ @@ -180,13 +197,6 @@ 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 retry status ahead of wrapping messages", () => { expect(formatSubagentRetry(2, "Rate limited by provider")).toBe("Retrying (attempt 2) · Rate limited by provider") })