/** * ToolCallCard shows one tool call the agent is running, in the spirit of * Zed's agent panel: a collapsible card with the tool kind, a live status, * touched locations, and the call's output (text, or a file diff). * * @example * */ import type { ToolCall } from "../reducer"; import { InlineText } from "./InlineText"; import type { AcpToolCallContent } from "../protocol"; import { Markdown } from "./Markdown"; import { CopyButton } from "./CopyButton"; const kindIcons: Record = { read: "📖", edit: "✏️", delete: "🗑️", move: "📦", search: "🔍", execute: "⚡", think: "💭", fetch: "🌐", switch_mode: "🔀", other: "🔧", }; const statusLabels: Record = { pending: "pending", in_progress: "running", completed: "done", failed: "failed", }; /** * contentText flattens one tool-call content block into plain text for the * copy button: a "content" block's text, or a diff's path / old / new * sections. Anything else (terminal handles, …) yields "". * * @example * contentText({ type: "content", content: { type: "text", text: "hi" } }) // "hi" */ export function contentText(content: AcpToolCallContent): string { switch (content.type) { case "content": return content.content?.text ?? ""; case "diff": return diffText(content); default: return ""; } } function diffText(diff: { path?: string; oldText?: string | null; newText?: string | null; }): string { const parts: string[] = []; if (diff.path) parts.push(`Path: ${diff.path}`); if (diff.oldText) parts.push(`Old:\n${diff.oldText}`); if (diff.newText) parts.push(`New:\n${diff.newText}`); return parts.join("\n\n"); } /** * toolContentText joins the textual content of a whole tool call, skipping * empty blocks, so the copy button copies exactly what the card shows. * * @example * toolContentText([]) // "" */ export function toolContentText(contents: AcpToolCallContent[]): string { return contents.map(contentText).filter(Boolean).join("\n\n"); } export function ToolCallCard({ call }: { call: ToolCall }) { const copyText = toolContentText(call.contents); return (
{kindIcons[call.toolKind] ?? kindIcons.other} {statusLabels[call.status] ?? call.status} {copyText && }
{call.locations.length > 0 && (
    {call.locations.map((location, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: tool-call locations carry no id and the list is replaced wholesale on every update.
  • {location.path} {location.line != null ? `:${location.line}` : ""}
  • ))}
)} {call.contents.map((content, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: tool-call contents carry no id and the list is replaced wholesale on every update. ))}
); } function ToolContent({ content, toolKind, }: { content: AcpToolCallContent; toolKind: string }) { switch (content.type) { case "content": return ( ); case "diff": return (
{content.path} {content.oldText != null && (
							{prefixLines(content.oldText, "- ")}
						
)}
						{prefixLines(content.newText ?? "", "+ ")}
					
); case "terminal": return

terminal: {content.terminalId}

; default: return null; } } /** * TextToolContent renders a tool call's textual output. System-command * output (kind "execute") is verbatim text: it goes into a monospace block, * never through the markdown pipeline. */ function TextToolContent({ text, toolKind, }: { text: string | undefined; toolKind: string }) { if (!text) { return null; } if (toolKind === "execute") { return
{text}
; } return ; } function prefixLines(text: string, prefix: string): string { return text .split("\n") .map((line) => prefix + line) .join("\n"); }