/** * ActionTree — Manus-style right panel showing execution tree. * Displays tool calls, file operations, and terminal output in a * hierarchical timeline view instead of inline in chat messages. */ import { useState, useRef, useEffect } from "react"; import { ChevronRight, ChevronDown, Terminal, FileText, Search, Globe, Code, CheckCircle2, XCircle, Loader2, Clock, Eye, Copy, Maximize2, Minimize2, } from "lucide-react"; import type { ChatMessage } from "@/hooks/useChat"; import { CodeDiffViewer } from "./CodeDiffViewer"; // Tool category mapping for icons and colors const TOOL_CATEGORIES: Record = { bash: { icon: Terminal, color: "text-green-400", label: "Shell" }, PowerShell: { icon: Terminal, color: "text-blue-400", label: "PowerShell" }, read_file: { icon: FileText, color: "text-sky-400", label: "Read File" }, write_file: { icon: FileText, color: "text-amber-400", label: "Write File" }, edit_file: { icon: FileText, color: "text-orange-400", label: "Edit File" }, multi_edit_file: { icon: FileText, color: "text-orange-400", label: "Multi Edit" }, glob_search: { icon: Search, color: "text-purple-400", label: "Glob Search" }, grep_search: { icon: Search, color: "text-purple-400", label: "Grep Search" }, WebSearch: { icon: Globe, color: "text-cyan-400", label: "Web Search" }, WebFetch: { icon: Globe, color: "text-cyan-400", label: "Fetch URL" }, TodoWrite: { icon: Code, color: "text-primary", label: "Plan Update" }, TodoRead: { icon: Code, color: "text-primary", label: "Plan Read" }, StructuredOutput: { icon: Code, color: "text-indigo-400", label: "Output" }, SendUserMessage: { icon: Code, color: "text-yellow-400", label: "Ask User" }, }; function getToolMeta(name: string) { return TOOL_CATEGORIES[name] || { icon: Code, color: "text-muted-foreground", label: name }; } interface ToolCall { id: string; name: string; arguments: string; result?: string; isError?: boolean; isExecuting?: boolean; durationMs?: number; } // Extract a short summary from tool arguments function getToolSummary(name: string, args: string): string { try { const parsed = JSON.parse(args); switch (name) { case "bash": case "PowerShell": return parsed.command?.slice(0, 80) || "..."; case "read_file": return parsed.path?.split("/").pop() || parsed.path || "..."; case "write_file": case "edit_file": case "multi_edit_file": return parsed.path?.split("/").pop() || parsed.path || "..."; case "glob_search": return parsed.pattern || "..."; case "grep_search": return parsed.pattern || parsed.query || "..."; case "WebSearch": return parsed.query || "..."; case "WebFetch": return parsed.url?.slice(0, 60) || "..."; case "TodoWrite": return `${(parsed.todos || []).length} items`; default: return Object.values(parsed).filter(v => typeof v === "string").join(", ").slice(0, 60) || "..."; } } catch { return args.slice(0, 60) || "..."; } } interface ActionNodeProps { tool: ToolCall; index: number; isLast: boolean; } function ActionNode({ tool, index, isLast }: ActionNodeProps) { const [expanded, setExpanded] = useState(false); const meta = getToolMeta(tool.name); const Icon = meta.icon; const summary = getToolSummary(tool.name, tool.arguments); const [copied, setCopied] = useState(false); const statusIcon = tool.isExecuting ? ( ) : tool.isError ? ( ) : ( ); const handleCopy = (text: string) => { navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 1500); }; return (
{/* Timeline connector */} {!isLast && (
)} {/* Node header */} {/* Expanded content */} {expanded && (
{/* Input */}
Input
              {(() => {
                try {
                  return JSON.stringify(JSON.parse(tool.arguments), null, 2);
                } catch {
                  return tool.arguments;
                }
              })()}
            
{/* Output — with CodeDiffViewer for edit/write operations */} {tool.result && (
{(tool.name === "edit_file" || tool.name === "multi_edit_file") && tool.result && !tool.isError ? ( (() => { // Try to extract old/new content from edit result try { const parsed = JSON.parse(tool.arguments); const fileName = parsed.path?.split("/").pop() || parsed.path; // Show diff viewer with result as the "after" content return ( ); } catch { return null; } })() || (
                    {tool.result.length > 5000 ? tool.result.slice(0, 5000) + "\n\n[...truncated]" : tool.result}
                  
) ) : ( <>
Output {tool.isError && ( ERROR )}
                    {tool.result.length > 5000
                      ? tool.result.slice(0, 5000) + "\n\n[...truncated]"
                      : tool.result}
                  
)}
)}
)}
); } interface ActionTreeProps { messages: ChatMessage[]; isStreaming: boolean; } export function ActionTree({ messages, isStreaming }: ActionTreeProps) { const scrollRef = useRef(null); const [autoScroll, setAutoScroll] = useState(true); // Collect all tool calls from all assistant messages const allToolCalls: ToolCall[] = []; for (const msg of messages) { if (msg.role === "assistant" && msg.toolCalls) { for (const tc of msg.toolCalls) { allToolCalls.push(tc); } } } // Auto-scroll to bottom when new tool calls appear useEffect(() => { if (autoScroll && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [allToolCalls.length, autoScroll]); const handleScroll = () => { const el = scrollRef.current; if (!el) return; const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50; setAutoScroll(isAtBottom); }; if (allToolCalls.length === 0) { return (

Actions will appear here as the agent works

); } return (
{/* Header with progress indicators */} {(() => { const completed = allToolCalls.filter(tc => tc.result && !tc.isError && !tc.isExecuting).length; const errors = allToolCalls.filter(tc => tc.isError).length; const running = allToolCalls.filter(tc => tc.isExecuting).length; const total = allToolCalls.length; const pct = total > 0 ? Math.round((completed / total) * 100) : 0; return (
Actions {total}
{completed > 0 && ( {completed} )} {errors > 0 && ( {errors} )} {running > 0 && ( {running} )} {isStreaming && running === 0 && ( thinking )}
{/* Progress bar */} {total > 0 && (
0 ? 'bg-destructive/60' : 'bg-green-500/60'}`} style={{ width: `${pct}%` }} />
)}
); })()} {/* Jump to live button */} {!autoScroll && ( )} {/* Timeline */}
{allToolCalls.map((tc, i) => ( ))}
); }