From 0ccfc4da065f6a631df64b0d98423c3c46266028 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 4 Mar 2026 18:04:34 -0500 Subject: [PATCH] feat(site): add specialized renderer for process_output tool in agent chats (#22628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `ProcessOutputTool` component that renders `process_output` tool calls with a clean terminal-style output block instead of falling through to the generic JSON renderer. ## Changes **New file:** `ProcessOutputTool.tsx` - Output shown directly with no header - Copy button and status indicators float top-right on hover - Collapsible output with the same expand/collapse chevron bar used by `ExecuteTool` - Exit code badge shown only for non-zero exits - Spinner shown while process is still running **Modified files:** - `Tool.tsx` — `ProcessOutputRenderer` + registered in `toolRenderers` map - `ToolIcon.tsx` — `process_output` falls through to `TerminalIcon` - `ToolLabel.tsx` — shows "Reading process output" label --- .../ai-elements/tool/ProcessOutputTool.tsx | 112 ++++++++++++++++++ site/src/components/ai-elements/tool/Tool.tsx | 29 ++++- .../components/ai-elements/tool/ToolIcon.tsx | 1 + .../components/ai-elements/tool/ToolLabel.tsx | 6 + 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 site/src/components/ai-elements/tool/ProcessOutputTool.tsx diff --git a/site/src/components/ai-elements/tool/ProcessOutputTool.tsx b/site/src/components/ai-elements/tool/ProcessOutputTool.tsx new file mode 100644 index 0000000000..46cbcc4888 --- /dev/null +++ b/site/src/components/ai-elements/tool/ProcessOutputTool.tsx @@ -0,0 +1,112 @@ +import { CopyButton } from "components/CopyButton/CopyButton"; +import { ScrollArea } from "components/ScrollArea/ScrollArea"; +import { ChevronDownIcon, LoaderIcon } from "lucide-react"; +import type React from "react"; +import { useRef, useState } from "react"; +import { cn } from "utils/cn"; +import { COLLAPSED_OUTPUT_HEIGHT } from "./utils"; + +/** + * Specialized rendering for `process_output` tool calls. Shows + * process output directly in a terminal-style block with a + * collapsible preview and an expand chevron at the bottom. + */ +export const ProcessOutputTool: React.FC<{ + output: string; + isRunning: boolean; + exitCode: number | null; + isError: boolean; +}> = ({ output, isRunning, exitCode, isError }) => { + const [expanded, setExpanded] = useState(false); + const outputRef = useRef(null); + const hasOutput = output.length > 0; + + const [overflows, setOverflows] = useState(false); + const measureRef = (node: HTMLPreElement | null) => { + outputRef.current = node; + if (node) { + setOverflows(node.scrollHeight > COLLAPSED_OUTPUT_HEIGHT); + } + }; + + const showExitCode = exitCode !== null && exitCode !== 0; + + return ( +
+ {hasOutput ? ( + <> +
+ +
+								{output}
+							
+
+
+ {isRunning && ( + + )} + {showExitCode && ( + + exit {exitCode} + + )} + +
+
+ + {/* Expand / collapse toggle at the bottom */} + {overflows && ( +
setExpanded((v) => !v)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + setExpanded((v) => !v); + } + }} + className="flex w-full cursor-pointer items-center justify-center py-0.5 text-content-secondary transition-colors hover:bg-surface-secondary hover:text-content-primary" + aria-label={expanded ? "Collapse output" : "Expand output"} + > + +
+ )} + + ) : ( +
+ {isRunning && ( + + )} + {showExitCode && ( + + exit {exitCode} + + )} +
+ )} +
+ ); +}; diff --git a/site/src/components/ai-elements/tool/Tool.tsx b/site/src/components/ai-elements/tool/Tool.tsx index 29592a3edc..a4da9f5de1 100644 --- a/site/src/components/ai-elements/tool/Tool.tsx +++ b/site/src/components/ai-elements/tool/Tool.tsx @@ -13,6 +13,7 @@ import { WaitForExternalAuthTool, } from "./ExecuteTool"; import { ListTemplatesTool } from "./ListTemplatesTool"; +import { ProcessOutputTool } from "./ProcessOutputTool"; import { ReadFileTool } from "./ReadFileTool"; import { ReadTemplateTool } from "./ReadTemplateTool"; import { SubagentTool } from "./SubagentTool"; @@ -105,6 +106,29 @@ const ExecuteRenderer: FC = ({ ); }; +const ProcessOutputRenderer: FC = ({ + status, + result, + isError, +}) => { + const rec = asRecord(result); + const output = rec ? asString(rec.output).trim() : ""; + const exitCode = rec + ? rec.exit_code !== undefined && rec.exit_code !== null + ? Number(rec.exit_code) + : null + : null; + + return ( + + ); +}; + const WaitForExternalAuthRenderer: FC = ({ status, result, @@ -424,6 +448,7 @@ const GenericToolRenderer: FC = ({ const toolRenderers: Record> = { execute: ExecuteRenderer, + process_output: ProcessOutputRenderer, wait_for_external_auth: WaitForExternalAuthRenderer, read_file: ReadFileRenderer, write_file: WriteFileRenderer, @@ -461,7 +486,9 @@ export const Tool = memo(
= ({ const base = cn("h-4 w-4 shrink-0", color); switch (name) { case "execute": + case "process_output": return ; case "read_file": case "list_templates": diff --git a/site/src/components/ai-elements/tool/ToolLabel.tsx b/site/src/components/ai-elements/tool/ToolLabel.tsx index c5a8b4250f..e0a9b9d865 100644 --- a/site/src/components/ai-elements/tool/ToolLabel.tsx +++ b/site/src/components/ai-elements/tool/ToolLabel.tsx @@ -25,6 +25,12 @@ export const ToolLabel: React.FC<{ ); } + case "process_output": + return ( + + Reading process output + + ); case "read_file": return (