diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index aa08ab51b7a..9362c472a20 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -5,6 +5,7 @@ import { createMemo, createSignal, For, + Index, Match, on, onCleanup, @@ -12,6 +13,7 @@ import { Show, Switch, useContext, + type Accessor, } from "solid-js" import path from "node:path" import { EOL, tmpdir } from "node:os" @@ -3019,6 +3021,63 @@ function executeCalls(value: unknown): ExecuteCall[] { }) } +export function executeCallSummary(call: ExecuteCall) { + const args = primitiveInputSummary(call.input ?? {}).replace(/\s+/g, " ") + return `↳ ${call.tool}${call.status === "error" ? " (failed)" : ""}${args ? ` ${args}` : ""}` +} + +function ExecuteCallView(props: { call: Accessor }) { + const theme = useTheme() + const renderer = useRenderer() + const [expanded, setExpanded] = createSignal(false) + const [hover, setHover] = createSignal(false) + const input = createMemo(() => Object.entries(props.call().input ?? {})) + const expandable = createMemo(() => input().length > 0) + const title = createMemo(() => `↳ ${props.call().tool}${props.call().status === "error" ? " (failed)" : ""}`) + + return ( + expandable() && setHover(true)} + onMouseOut={() => setHover(false)} + onMouseUp={() => { + if (!expandable() || renderer.getSelection()?.getSelectedText()) return + setExpanded((value) => !value) + }} + > + + {expanded() ? title() : executeCallSummary(props.call())} + + + + + {([key, value]) => ( + + + {key}:{" "} + + + {typeof value === "string" ? value : JSON.stringify(value, null, 2)} + + + )} + + + + + ) +} + // The `execute` tool streams child tool calls through metadata, not a child session like Task. function Execute(props: ToolProps) { const ctx = use() @@ -3029,14 +3088,6 @@ function Execute(props: ToolProps) { const hasRuntimeError = createMemo(() => props.metadata.error === true || props.part.state.status === "error") const outputPreview = createMemo(() => collapseToolOutput(output(), 4, 4 * Math.max(20, ctx.width - 6)).output) const showOutput = createMemo(() => output() && hasRuntimeError()) - const content = createMemo(() => { - const lines = ["execute"] - for (const call of calls()) { - const args = primitiveInputSummary(call.input ?? {}) - lines.push(`↳ ${call.tool}${args ? ` ${args}` : ""}${call.status === "error" ? " (failed)" : ""}`) - } - return lines.join("\n") - }) return ( <> @@ -3048,8 +3099,9 @@ function Execute(props: ToolProps) { complete={true} part={props.part} > - {content()} + execute + {(call) => } 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 675008b400a..e6958334c92 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,6 +3,7 @@ import { For } from "solid-js" import { testRender, type JSX } from "@opentui/solid" import { InlineToolRow, + executeCallSummary, isBackgroundSubagent, parseApplyPatchFiles, parseDiagnostics, @@ -182,6 +183,22 @@ describe("TUI inline tool wrapping", () => { expect(parseQuestionAnswers({})).toBeUndefined() }) + test("summarizes execute calls on one line", () => { + expect( + executeCallSummary({ + tool: "session.prompt", + status: "completed", + input: { sessionID: "ses_example", notify: true }, + }), + ).toBe("↳ session.prompt [sessionID=ses_example, notify=true]") + expect(executeCallSummary({ tool: "session.get", status: "error", input: { nested: { hidden: true } } })).toBe( + "↳ session.get (failed)", + ) + expect( + executeCallSummary({ tool: "session.prompt", status: "completed", input: { text: "first line\nsecond line" } }), + ).toBe("↳ session.prompt [text=first line second line]") + }) + test("ignores diagnostics with malformed nested ranges", () => { expect( parseDiagnostics(