Spaces:
Sleeping
Sleeping
| import { useState, useMemo } from "react"; | |
| import { cn } from "@/lib/utils"; | |
| import { | |
| Terminal, | |
| FileText, | |
| FileEdit, | |
| Search, | |
| FolderSearch, | |
| Globe, | |
| Link, | |
| ListTodo, | |
| Bot, | |
| ChevronDown, | |
| ChevronRight, | |
| Loader2, | |
| Check, | |
| X, | |
| Clock, | |
| Copy, | |
| GitBranch, | |
| MessageCircle, | |
| Wrench, | |
| Settings, | |
| Send, | |
| Server, | |
| KeyRound, | |
| Timer, | |
| Zap, | |
| Code2, | |
| BookOpen, | |
| MonitorPlay, | |
| Moon, | |
| Play, | |
| Braces, | |
| ListChecks, | |
| CalendarClock, | |
| Trash2, | |
| List, | |
| Eye, | |
| StopCircle, | |
| RefreshCw, | |
| Languages, | |
| Map, | |
| MapPin, | |
| Users, | |
| UserMinus, | |
| Webhook, | |
| FlaskConical, | |
| GitFork, | |
| } from "lucide-react"; | |
| import type { ToolCallInfo } from "@/hooks/useChat"; | |
| /** | |
| * Tool icon map — EXACT parity with original claw-code tool names. | |
| * Supports both original names and legacy aliases. | |
| */ | |
| const TOOL_ICONS: Record<string, React.ElementType> = { | |
| // ── Core 19 tools (original names) ── | |
| bash: Terminal, | |
| PowerShell: MonitorPlay, | |
| read_file: FileText, | |
| write_file: FileEdit, | |
| edit_file: FileEdit, | |
| glob_search: FolderSearch, | |
| grep_search: Search, | |
| NotebookEdit: BookOpen, | |
| WebSearch: Globe, | |
| WebFetch: Link, | |
| TodoWrite: ListTodo, | |
| Agent: Bot, | |
| SendUserMessage: MessageCircle, | |
| Brief: MessageCircle, | |
| TestingPermission: FlaskConical, | |
| ToolSearch: Wrench, | |
| Config: Settings, | |
| Skill: Zap, | |
| Sleep: Moon, | |
| REPL: Play, | |
| StructuredOutput: Braces, | |
| // ── Extended tools (full parity) ── | |
| TaskCreate: ListChecks, | |
| TaskGet: Eye, | |
| TaskList: List, | |
| TaskOutput: Terminal, | |
| TaskStop: StopCircle, | |
| TaskUpdate: RefreshCw, | |
| CronCreate: CalendarClock, | |
| CronDelete: Trash2, | |
| CronList: CalendarClock, | |
| LSP: Languages, | |
| EnterPlanMode: Map, | |
| ExitPlanMode: MapPin, | |
| EnterWorktree: GitFork, | |
| ExitWorktree: GitBranch, | |
| TeamCreate: Users, | |
| TeamDelete: UserMinus, | |
| RemoteTrigger: Webhook, | |
| SyntheticOutput: FlaskConical, | |
| // ── MCP ── | |
| mcp_tool: Server, | |
| list_mcp_resources: Server, | |
| read_mcp_resource: Server, | |
| mcp_auth: KeyRound, | |
| // ── Legacy aliases ── | |
| powershell: MonitorPlay, | |
| grep: Search, | |
| glob: FolderSearch, | |
| web_search: Globe, | |
| web_fetch: Link, | |
| todo_read: ListTodo, | |
| todo_write: ListTodo, | |
| sub_agent: Bot, | |
| send_message: Send, | |
| ask_user: MessageCircle, | |
| tool_search: Wrench, | |
| config_read: Settings, | |
| config_write: Settings, | |
| notebook_edit: BookOpen, | |
| skill: Zap, | |
| }; | |
| /** | |
| * Tool color map — EXACT parity with original claw-code tool names. | |
| */ | |
| const TOOL_COLORS: Record<string, string> = { | |
| // ── Core 19 tools ── | |
| bash: "text-green-400", | |
| PowerShell: "text-blue-500", | |
| read_file: "text-blue-400", | |
| write_file: "text-yellow-400", | |
| edit_file: "text-orange-400", | |
| glob_search: "text-cyan-400", | |
| grep_search: "text-purple-400", | |
| NotebookEdit: "text-emerald-400", | |
| WebSearch: "text-pink-400", | |
| WebFetch: "text-indigo-400", | |
| TodoWrite: "text-teal-400", | |
| Agent: "text-amber-400", | |
| SendUserMessage: "text-sky-400", | |
| Brief: "text-sky-400", | |
| TestingPermission: "text-gray-400", | |
| ToolSearch: "text-violet-400", | |
| Config: "text-slate-400", | |
| Skill: "text-yellow-300", | |
| Sleep: "text-indigo-300", | |
| REPL: "text-lime-400", | |
| StructuredOutput: "text-cyan-300", | |
| // ── Extended tools (full parity) ── | |
| TaskCreate: "text-emerald-500", | |
| TaskGet: "text-emerald-400", | |
| TaskList: "text-emerald-300", | |
| TaskOutput: "text-emerald-400", | |
| TaskStop: "text-red-400", | |
| TaskUpdate: "text-emerald-300", | |
| CronCreate: "text-amber-500", | |
| CronDelete: "text-red-400", | |
| CronList: "text-amber-400", | |
| LSP: "text-blue-500", | |
| EnterPlanMode: "text-violet-500", | |
| ExitPlanMode: "text-violet-400", | |
| EnterWorktree: "text-orange-500", | |
| ExitWorktree: "text-orange-400", | |
| TeamCreate: "text-cyan-500", | |
| TeamDelete: "text-red-400", | |
| RemoteTrigger: "text-pink-500", | |
| SyntheticOutput: "text-lime-500", | |
| // ── MCP ── | |
| mcp_tool: "text-rose-400", | |
| list_mcp_resources: "text-rose-300", | |
| read_mcp_resource: "text-rose-300", | |
| mcp_auth: "text-rose-500", | |
| // ── Legacy aliases ── | |
| powershell: "text-blue-500", | |
| grep: "text-purple-400", | |
| glob: "text-cyan-400", | |
| web_search: "text-pink-400", | |
| web_fetch: "text-indigo-400", | |
| todo_read: "text-teal-400", | |
| todo_write: "text-teal-400", | |
| sub_agent: "text-amber-400", | |
| send_message: "text-sky-400", | |
| ask_user: "text-sky-400", | |
| tool_search: "text-violet-400", | |
| config_read: "text-slate-400", | |
| config_write: "text-slate-400", | |
| notebook_edit: "text-emerald-400", | |
| skill: "text-yellow-300", | |
| }; | |
| /** Simple diff line renderer for edit_file results */ | |
| function DiffView({ content }: { content: string }) { | |
| const lines = content.split("\n"); | |
| return ( | |
| <div className="text-xs font-mono"> | |
| {lines.map((line, i) => { | |
| let cls = "text-foreground/70"; | |
| if (line.startsWith("+ ") || line.startsWith("+\t") || line === "+") { | |
| cls = "text-green-400 bg-green-400/10"; | |
| } else if ( | |
| line.startsWith("- ") || | |
| line.startsWith("-\t") || | |
| line === "-" | |
| ) { | |
| cls = "text-red-400 bg-red-400/10"; | |
| } else if (line.startsWith("@@ ")) { | |
| cls = "text-cyan-400/60"; | |
| } else if (line.startsWith("--- ") || line.startsWith("+++ ")) { | |
| cls = "text-muted-foreground"; | |
| } | |
| return ( | |
| <div key={i} className={cn("px-2 py-px whitespace-pre-wrap", cls)}> | |
| {line} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| ); | |
| } | |
| /** Detect if output looks like a diff */ | |
| function isDiffLike(content: string): boolean { | |
| if (!content) return false; | |
| const lines = content.split("\n").slice(0, 20); | |
| let diffIndicators = 0; | |
| for (const l of lines) { | |
| if ( | |
| l.startsWith("+ ") || | |
| l.startsWith("- ") || | |
| l.startsWith("@@ ") || | |
| l.startsWith("--- ") || | |
| l.startsWith("+++ ") | |
| ) | |
| diffIndicators++; | |
| } | |
| return diffIndicators >= 2; | |
| } | |
| export function ToolCallCard({ tool }: { tool: ToolCallInfo }) { | |
| const [expanded, setExpanded] = useState(false); | |
| const [copied, setCopied] = useState(false); | |
| const Icon = TOOL_ICONS[tool.name] || Terminal; | |
| const colorClass = TOOL_COLORS[tool.name] || "text-muted-foreground"; | |
| let parsedArgs: Record<string, unknown> = {}; | |
| try { | |
| parsedArgs = JSON.parse(tool.arguments || "{}"); | |
| } catch { | |
| parsedArgs = { raw: tool.arguments }; | |
| } | |
| const showDiff = useMemo( | |
| () => | |
| (tool.name === "edit_file" || tool.name === "bash") && | |
| tool.result && | |
| isDiffLike(tool.result), | |
| [tool.name, tool.result] | |
| ); | |
| // Format the tool call summary — supports both original and legacy names | |
| const getSummary = () => { | |
| switch (tool.name) { | |
| case "bash": | |
| case "PowerShell": | |
| case "powershell": | |
| return String(parsedArgs.command || "").substring(0, 80); | |
| case "read_file": | |
| case "write_file": | |
| case "edit_file": | |
| case "NotebookEdit": | |
| case "notebook_edit": | |
| return String(parsedArgs.path || parsedArgs.notebook_path || ""); | |
| case "grep_search": | |
| case "grep": | |
| return `"${parsedArgs.pattern}" in ${parsedArgs.path || "."}`; | |
| case "glob_search": | |
| case "glob": | |
| return String(parsedArgs.pattern || ""); | |
| case "WebSearch": | |
| case "web_search": | |
| return String(parsedArgs.query || ""); | |
| case "WebFetch": | |
| case "web_fetch": | |
| return String(parsedArgs.url || "").substring(0, 60); | |
| case "Agent": | |
| case "sub_agent": | |
| return String(parsedArgs.description || parsedArgs.task || "").substring(0, 60); | |
| case "SendUserMessage": | |
| case "Brief": | |
| case "ask_user": | |
| return String(parsedArgs.message || parsedArgs.question || "").substring(0, 60); | |
| case "TestingPermission": | |
| return `check: ${parsedArgs.tool || "unknown"}`; | |
| case "ToolSearch": | |
| case "tool_search": | |
| return String(parsedArgs.query || ""); | |
| case "Config": | |
| return parsedArgs.value !== undefined | |
| ? `${parsedArgs.setting} = ${parsedArgs.value}` | |
| : String(parsedArgs.setting || "all"); | |
| case "config_read": | |
| return String(parsedArgs.key || "all"); | |
| case "config_write": | |
| return `${parsedArgs.key} = ${parsedArgs.value}`; | |
| case "Skill": | |
| case "skill": | |
| return String(parsedArgs.skill || parsedArgs.name || ""); | |
| case "Sleep": | |
| return `${parsedArgs.duration_ms || 0}ms`; | |
| case "REPL": | |
| return `${parsedArgs.language || "python"}: ${String(parsedArgs.code || "").substring(0, 50)}`; | |
| case "TodoWrite": | |
| return "Update task list"; | |
| // Extended tools | |
| case "TaskCreate": | |
| return String(parsedArgs.description || "").substring(0, 60); | |
| case "TaskGet": | |
| case "TaskOutput": | |
| case "TaskStop": | |
| case "TaskUpdate": | |
| return String(parsedArgs.id || ""); | |
| case "TaskList": | |
| return "List background tasks"; | |
| case "CronCreate": | |
| return `${parsedArgs.schedule} — ${String(parsedArgs.command || "").substring(0, 40)}`; | |
| case "CronDelete": | |
| return String(parsedArgs.id || ""); | |
| case "CronList": | |
| return "List cron jobs"; | |
| case "LSP": | |
| return `${parsedArgs.action || ""} ${parsedArgs.path || ""}`; | |
| case "EnterPlanMode": | |
| return "Enter plan mode"; | |
| case "ExitPlanMode": | |
| return "Exit plan mode"; | |
| case "EnterWorktree": | |
| return String(parsedArgs.branch || ""); | |
| case "ExitWorktree": | |
| return "Exit worktree"; | |
| case "TeamCreate": | |
| return String(parsedArgs.name || ""); | |
| case "TeamDelete": | |
| return String(parsedArgs.id || ""); | |
| case "RemoteTrigger": | |
| return `${parsedArgs.method || "POST"} ${String(parsedArgs.url || "").substring(0, 50)}`; | |
| case "SyntheticOutput": | |
| return String(parsedArgs.format || "json"); | |
| default: | |
| return tool.name; | |
| } | |
| }; | |
| const copyResult = () => { | |
| if (tool.result) { | |
| navigator.clipboard.writeText(tool.result); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| } | |
| }; | |
| return ( | |
| <div className="tool-card rounded-lg border border-border bg-secondary/30 my-2 overflow-hidden"> | |
| {/* Header */} | |
| <button | |
| onClick={() => setExpanded(!expanded)} | |
| className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-accent/30 transition-colors" | |
| > | |
| <Icon className={cn("size-4 shrink-0", colorClass)} /> | |
| <span className="font-mono text-xs font-medium text-foreground/80"> | |
| {tool.name} | |
| </span> | |
| <span className="text-xs text-muted-foreground truncate flex-1 font-mono"> | |
| {getSummary()} | |
| </span> | |
| {/* Status indicator */} | |
| <div className="flex items-center gap-1.5 shrink-0"> | |
| {tool.isExecuting && ( | |
| <Loader2 className="size-3.5 animate-spin text-primary" /> | |
| )} | |
| {!tool.isExecuting && tool.result !== undefined && !tool.isError && ( | |
| <Check className="size-3.5 text-green-400" /> | |
| )} | |
| {!tool.isExecuting && tool.isError && ( | |
| <X className="size-3.5 text-destructive" /> | |
| )} | |
| {tool.durationMs !== undefined && tool.durationMs > 0 && ( | |
| <span className="text-[10px] text-muted-foreground flex items-center gap-0.5"> | |
| <Clock className="size-2.5" /> | |
| {tool.durationMs < 1000 | |
| ? `${tool.durationMs}ms` | |
| : `${(tool.durationMs / 1000).toFixed(1)}s`} | |
| </span> | |
| )} | |
| {showDiff && ( | |
| <GitBranch className="size-3 text-orange-400" /> | |
| )} | |
| {expanded ? ( | |
| <ChevronDown className="size-3.5 text-muted-foreground" /> | |
| ) : ( | |
| <ChevronRight className="size-3.5 text-muted-foreground" /> | |
| )} | |
| </div> | |
| </button> | |
| {/* Expanded content */} | |
| {expanded && ( | |
| <div className="border-t border-border"> | |
| {/* Arguments */} | |
| <div className="px-3 py-2"> | |
| <div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1"> | |
| Input | |
| </div> | |
| <pre className="text-xs font-mono text-foreground/80 whitespace-pre-wrap break-all bg-background/50 rounded p-2 max-h-48 overflow-auto"> | |
| {tool.name === "bash" || tool.name === "PowerShell" || tool.name === "powershell" | |
| ? String(parsedArgs.command || "") | |
| : JSON.stringify(parsedArgs, null, 2)} | |
| </pre> | |
| </div> | |
| {/* Result */} | |
| {tool.result !== undefined && ( | |
| <div className="px-3 py-2 border-t border-border"> | |
| <div className="flex items-center justify-between mb-1"> | |
| <div className="text-[10px] uppercase tracking-wider text-muted-foreground"> | |
| Output | |
| {tool.isError && ( | |
| <span className="text-destructive ml-1">(error)</span> | |
| )} | |
| {showDiff && ( | |
| <span className="text-orange-400 ml-1">(diff)</span> | |
| )} | |
| </div> | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| copyResult(); | |
| }} | |
| className="text-muted-foreground hover:text-foreground transition-colors" | |
| > | |
| {copied ? ( | |
| <Check className="size-3" /> | |
| ) : ( | |
| <Copy className="size-3" /> | |
| )} | |
| </button> | |
| </div> | |
| {showDiff ? ( | |
| <div className="bg-background/50 rounded max-h-64 overflow-auto"> | |
| <DiffView content={tool.result} /> | |
| </div> | |
| ) : ( | |
| <pre | |
| className={cn( | |
| "text-xs font-mono whitespace-pre-wrap break-all rounded p-2 max-h-64 overflow-auto", | |
| tool.isError | |
| ? "bg-destructive/10 text-destructive" | |
| : "bg-background/50 text-foreground/80" | |
| )} | |
| > | |
| {tool.result} | |
| </pre> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |