import { useState } from "react"; import { IconChevronDown, IconChevronRight, IconFile, IconTerminal2, IconFileText, IconReplace, IconFolderSearch, IconSubtask, IconListCheck, IconSquareCheck, IconSquare, IconSquareChevronRight, } from "@tabler/icons-react"; import { cn } from "@/lib/utils"; import { FileLink, Markdown } from "./Markdown"; import { DisplayBlocks } from "./DisplayBlocks"; import { formatContentOutput } from "shared/legacy-sdk"; import { cleanSystemTags } from "shared/utils"; import { ThinkingBlock } from "./ThinkingBlock"; import type { UIToolCall, UIStep, UIStepItem } from "@/stores/chat.store"; import type { ToolResult, DisplayBlock, TodoBlock } from "shared/legacy-sdk"; type ToolResultValue = ToolResult["return_value"]; interface ToolRendererProps { call: UIToolCall; result?: ToolResultValue; subagentSteps?: UIStep[]; } function parseArgs(args: string | null): Record { if (!args) { return {}; } try { return JSON.parse(args); } catch { return { raw: args }; } } function formatOutput(output: string | object | object[]): string { const raw = formatContentOutput(output as string); return cleanSystemTags(raw); } function getTodoBlock(display?: DisplayBlock[]): TodoBlock | null { if (!display) { return null; } return (display.find((b) => b.type === "todo") as TodoBlock) || null; } function getRichDisplayBlocks(display?: DisplayBlock[]): DisplayBlock[] { if (!display) { return []; } return display.filter((b) => b.type === "diff"); } function CodeBlock({ content, maxLines = 10 }: { content: string; maxLines?: number }) { const [expanded, setExpanded] = useState(false); const lines = content.split("\n"); const shouldCollapse = lines.length > maxLines; const displayContent = shouldCollapse && !expanded ? lines.slice(0, maxLines).join("\n") : content; return (
        {displayContent}
        {shouldCollapse && !expanded && {"\n"}…}
      
{shouldCollapse && ( )}
); } function StatusIndicator({ status }: { status: "pending" | "success" | "error" }) { if (status === "pending") { return ( ); } return ; } function ToolIcon({ name }: { name: string }) { const iconClass = "size-3.5 text-muted-foreground"; switch (name) { case "Shell": return ; case "ReadFile": return ; case "WriteFile": return ; case "StrReplaceFile": return ; case "Glob": return ; case "Task": return ; case "SetTodoList": return ; default: return ; } } function IORow({ label, children }: { label: "IN" | "OUT"; children: React.ReactNode }) { return (
{label}
{children}
); } function TodoStatusIcon({ status }: { status: string }) { if (status === "done") { return (
); } if (status === "in_progress") { return ; } return ; } function SetTodoListTool({ result }: ToolRendererProps) { const todoBlock = getTodoBlock(result?.display); if (!todoBlock || !todoBlock.items || todoBlock.items.length === 0) { return
{!result?.is_error && "Todo list updated"}
; } return (
{todoBlock.items.map((item, idx) => (
{item.title}
))}
); } function ShellTool({ call, result }: ToolRendererProps) { const args = parseArgs(call.arguments); const command = (args.command as string) || ""; const output = result ? formatOutput(result.output) : ""; return (
{command} {result && output && ( )}
); } function ReadFileTool({ call, result }: ToolRendererProps) { const args = parseArgs(call.arguments); const filePath = (args.path as string) || ""; const lineOffset = args.line_offset as number | undefined; const output = result ? formatOutput(result.output) : ""; return (
{lineOffset && lineOffset > 1 && :L{lineOffset}} {result && output && ( )}
); } function WriteFileTool({ call, result }: ToolRendererProps) { const args = parseArgs(call.arguments); const filePath = (args.path as string) || ""; const mode = (args.mode as string) || "overwrite"; const richDisplay = getRichDisplayBlocks(result?.display); const hasRichDisplay = richDisplay.length > 0; return (
({mode}) {result && ( {hasRichDisplay ? ( ) : ( {!result.is_error ? "✓ Written" : formatOutput(result.output)} )} )}
); } function StrReplaceFileTool({ call, result }: ToolRendererProps) { const args = parseArgs(call.arguments); const filePath = (args.path as string) || ""; const richDisplay = getRichDisplayBlocks(result?.display); const hasRichDisplay = richDisplay.length > 0; return (
{result && ( {hasRichDisplay ? ( ) : ( {!result.is_error ? "✓ Replaced successfully" : formatOutput(result.output)} )} )}
); } function GlobTool({ call, result }: ToolRendererProps) { const args = parseArgs(call.arguments); const pattern = (args.pattern as string) || ""; const directory = args.directory as string | undefined; const output = result ? formatOutput(result.output) : ""; return (
{pattern} {directory && in {directory}} {result && output && ( )}
); } function GenericTool({ call, result }: ToolRendererProps) { const args = parseArgs(call.arguments); const output = result ? formatOutput(result.output) : ""; const richDisplay = getRichDisplayBlocks(result?.display); const hasRichDisplay = richDisplay.length > 0; return (
{result && ( {hasRichDisplay ? ( ) : output ? ( ) : ( {!result.is_error ? "✓ Done" : "✗ Failed"} )} )}
); } function SubagentStepItemRenderer({ item }: { item: UIStepItem }) { if (item.type === "thinking") { return ; } if (item.type === "text") { return ; } if (item.type === "tool_use") { return ; } return null; } function TaskTool({ call, result, subagentSteps }: ToolRendererProps) { const [showProcess, setShowProcess] = useState(false); const args = parseArgs(call.arguments); const description = (args.description as string) || ""; const subagentName = (args.subagent_name as string) || (args.subagent_type as string) || "coder"; const prompt = (args.prompt as string) || ""; const hasSubagentSteps = subagentSteps && subagentSteps.length > 0; const finalOutput = (() => { if (!hasSubagentSteps) { return result ? formatOutput(result.output) : ""; } const lastStep = subagentSteps[subagentSteps.length - 1]; const textItems = lastStep.items.filter((i) => i.type === "text"); if (textItems.length > 0) { return textItems.map((i) => (i as { type: "text"; content: string }).content).join("\n"); } return result ? formatOutput(result.output) : ""; })(); return (
{subagentName} {description}
{prompt &&
{prompt}
}
{hasSubagentSteps && (
{showProcess && (
{subagentSteps.map((step) => (
Step {step.n}
{step.items.map((item, idx) => ( ))}
))}
)}
)} {result && finalOutput && ( )}
); } function getToolLabel(call: UIToolCall): string { const args = parseArgs(call.arguments); switch (call.name) { case "Shell": return (args.command as string) || "command"; case "ReadFile": return (args.path as string)?.split("/").pop() || "file"; case "WriteFile": return (args.path as string)?.split("/").pop() || "file"; case "StrReplaceFile": return (args.path as string)?.split("/").pop() || "file"; case "Glob": return (args.pattern as string) || "pattern"; case "Task": return (args.description as string) || "subagent task"; case "SetTodoList": return "Update Todos"; default: return ""; } } export function ToolCallCard({ call, result, subagentSteps }: ToolRendererProps) { const [expanded, setExpanded] = useState(false); const status = !result ? "pending" : !result.is_error ? "success" : "error"; const renderContent = () => { const props = { call, result, subagentSteps }; switch (call.name) { case "Shell": return ; case "ReadFile": return ; case "WriteFile": return ; case "StrReplaceFile": return ; case "Glob": return ; case "Task": case "Agent": return ; case "SetTodoList": return ; default: return ; } }; return (
{expanded &&
{renderContent()}
}
); }