From c400746dd5bf5525469e773df5f54d5e35fea6d9 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:03:06 +1000 Subject: [PATCH] fix(app): render code mode executions (#42949) --- .../src/components/message-part.tsx | 154 +++++++++++++----- .../src/components/part-default-open.test.ts | 3 +- .../src/components/part-default-open.ts | 2 +- .../src/components/tool-error-card.tsx | 1 + packages/ui/src/i18n/en.ts | 1 + 5 files changed, 117 insertions(+), 44 deletions(-) diff --git a/packages/session-ui/src/components/message-part.tsx b/packages/session-ui/src/components/message-part.tsx index e1e37be752a..7b61353b504 100644 --- a/packages/session-ui/src/components/message-part.tsx +++ b/packages/session-ui/src/components/message-part.tsx @@ -547,6 +547,12 @@ export function getToolInfo( title: i18n.t("ui.tool.shell"), subtitle: input.command, } + case "execute": + return { + icon: "console", + title: i18n.t("ui.tool.execute"), + subtitle: input.code, + } case "edit": return { icon: "code-lines", @@ -1574,6 +1580,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { if (typeof value === "string" && value) return value return taskId() }) + const toolError = createMemo(() => partError(part(), i18n.t("ui.toolErrorCard.failed"))) const render = createMemo(() => ToolRegistry.render(part().tool) ?? GenericTool) const controlledOpen = () => (props.onToolOpenChange ? (props.toolOpen ?? props.defaultOpen) : undefined) @@ -1583,7 +1590,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
- + {(error) => { const cleaned = error().replace("Error: ", "") if (part().tool === "question" && cleaned.includes("dismissed this question")) { @@ -1644,6 +1651,26 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { ) } +function partError(part: ToolPart, fallback: string) { + if (part.state.status === "error") return part.state.error + if (part.tool !== "execute" || !("metadata" in part.state)) return undefined + const calls = part.state.metadata?.toolCalls + const failed = + part.state.metadata?.error === true || + (Array.isArray(calls) && + calls.some( + (call) => + call !== null && + typeof call === "object" && + !Array.isArray(call) && + "status" in call && + call.status === "error", + )) + if (!failed) return undefined + if ("output" in part.state && typeof part.state.output === "string" && part.state.output) return part.state.output + return fallback +} + export function MessageDivider(props: { label: string }) { return (
@@ -2104,6 +2131,84 @@ ToolRegistry.register({ ToolRegistry.register({ name: "subagent", render: ToolRegistry.render("task") }) +function ConsoleOutput(props: { copy: string; children: JSX.Element }) { + const i18n = useI18n() + const [copied, setCopied] = createSignal(false) + + const copy = async () => { + if (!props.copy) return + if (!(await writeClipboard(props.copy))) return + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( +
+
+ + } + size="normal" + variant="ghost-muted" + onMouseDown={(event) => event.preventDefault()} + onClick={copy} + aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} + /> + +
+
+
+          {props.children}
+        
+
+
+ ) +} + +ToolRegistry.register({ + name: "execute", + render(props) { + const i18n = useI18n() + const pending = () => props.status === "pending" || props.status === "streaming" || props.status === "running" + const code = createMemo(() => (typeof props.input.code === "string" ? props.input.code : "")) + const text = createMemo(() => { + const output = stripAnsi(props.output ?? "").replace(/\r\n?/g, "\n") + return `${code()}${output ? "\n\n" + output : ""}` + }) + const sawPending = pending() + return ( + ( +
+ + + +
+ + + + + + +
+
+ )} + > + {text()} +
+ ) + }, +}) + ToolRegistry.register({ name: "shell", render(props) { @@ -2116,17 +2221,6 @@ ToolRegistry.register({ const out = stripAnsi(props.output || props.metadata.output || "").replace(/\r\n?/g, "\n") return `${command()}${out ? "\n\n" + out : ""}` }) - const [copied, setCopied] = createSignal(false) - - const handleCopy = async () => { - const content = command() - if (!content) return - if (await writeClipboard(content)) { - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } - } - return ( )} > -
-
- - } - size="normal" - variant="ghost-muted" - onMouseDown={(e) => e.preventDefault()} - onClick={handleCopy} - aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} - /> - -
-
-
-              
-                
-                {text()}
-              
-            
-
-
+ + + {text()} +
) }, diff --git a/packages/session-ui/src/components/part-default-open.test.ts b/packages/session-ui/src/components/part-default-open.test.ts index a122561dbec..0ea671100b9 100644 --- a/packages/session-ui/src/components/part-default-open.test.ts +++ b/packages/session-ui/src/components/part-default-open.test.ts @@ -71,8 +71,9 @@ describe("partDefaultOpen", () => { ).toBe(true) }) - test("preserves shell defaults", () => { + test("applies shell defaults to console tools", () => { expect(partDefaultOpen(tool("shell", {}), true, false)).toBe(true) + expect(partDefaultOpen(tool("execute", {}), true, false)).toBe(true) }) }) diff --git a/packages/session-ui/src/components/part-default-open.ts b/packages/session-ui/src/components/part-default-open.ts index 13097c483df..18e31d31195 100644 --- a/packages/session-ui/src/components/part-default-open.ts +++ b/packages/session-ui/src/components/part-default-open.ts @@ -23,7 +23,7 @@ function deletionOnly(part: ToolPart) { export function partDefaultOpen(part: PartType, shell = false, edit = false): boolean | undefined { if (part.type !== "tool") return undefined - if (part.tool === "bash" || part.tool === "shell") return shell + if (part.tool === "bash" || part.tool === "shell" || part.tool === "execute") return shell if (part.tool === "edit" || part.tool === "write" || part.tool === "patch" || part.tool === "apply_patch") { if (!edit) return false return !deletionOnly(part) diff --git a/packages/session-ui/src/components/tool-error-card.tsx b/packages/session-ui/src/components/tool-error-card.tsx index 2251641857a..e03b65141c4 100644 --- a/packages/session-ui/src/components/tool-error-card.tsx +++ b/packages/session-ui/src/components/tool-error-card.tsx @@ -54,6 +54,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) { websearch: "ui.tool.websearch", bash: "ui.tool.shell", shell: "ui.tool.shell", + execute: "ui.tool.execute", patch: "ui.tool.patch", apply_patch: "ui.tool.patch", question: "ui.tool.questions", diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 34f6aed2bfc..abaaa059cc6 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -158,6 +158,7 @@ const source = { "ui.tool.websearch": "Web Search", "ui.tool.websearch.provider": "{{provider}} Web Search", "ui.tool.shell": "Shell", + "ui.tool.execute": "Execute", "ui.tool.patch": "Patch", "ui.tool.questions": "Questions", "ui.tool.questions.numbered": "Questions {{number}}",