"use client"; import { useState, useEffect, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { ChevronDown, ChevronRight, CheckCircle2, XCircle, Loader2, Terminal, FileText, FileEdit, FileSearch, Search, Globe, BookOpen, ClipboardList, Bot, Wrench, } from "lucide-react"; import { cn } from "@/lib/utils"; // ─── Tool icon mapping ──────────────────────────────────────────────────────── const TOOL_ICONS: Record = { bash: Terminal, read: FileText, write: FileText, edit: FileEdit, glob: FileSearch, grep: Search, webfetch: Globe, websearch: Globe, notebookedit: BookOpen, todowrite: ClipboardList, agent: Bot, }; function getToolIcon(name: string): React.ElementType { return TOOL_ICONS[name.toLowerCase()] ?? Wrench; } const TOOL_LABELS: Record = { bash: "Bash", read: "Read File", write: "Write File", edit: "Edit File", glob: "Glob", grep: "Grep", webfetch: "Web Fetch", websearch: "Web Search", notebookedit: "Notebook Edit", todowrite: "Todo", agent: "Agent", }; function getToolLabel(name: string): string { return TOOL_LABELS[name.toLowerCase()] ?? name; } // ─── Elapsed timer ──────────────────────────────────────────────────────────── function ElapsedTimer({ startMs }: { startMs: number }) { const [elapsed, setElapsed] = useState(0); useEffect(() => { const interval = setInterval(() => { setElapsed(Date.now() - startMs); }, 100); return () => clearInterval(interval); }, [startMs]); if (elapsed < 1000) return {elapsed}ms; return {(elapsed / 1000).toFixed(1)}s; } // ─── Status badge ───────────────────────────────────────────────────────────── interface StatusBadgeProps { isRunning: boolean; isError: boolean; startedAt: number; completedAt?: number; } function StatusBadge({ isRunning, isError, startedAt, completedAt, }: StatusBadgeProps) { if (isRunning) { return ( ); } const duration = completedAt ? completedAt - startedAt : null; const durationStr = duration ? duration < 1000 ? `${duration}ms` : `${(duration / 1000).toFixed(1)}s` : null; if (isError) { return ( {durationStr && {durationStr}} Error ); } return ( {durationStr && {durationStr}} ); } // ─── Main component ─────────────────────────────────────────────────────────── export interface ToolUseBlockProps { toolName: string; toolInput: Record; toolResult?: string | null; isError?: boolean; isRunning?: boolean; startedAt?: number; completedAt?: number; children?: React.ReactNode; defaultExpanded?: boolean; } export function ToolUseBlock({ toolName, toolInput: _toolInput, toolResult: _toolResult, isError = false, isRunning = false, startedAt, completedAt, children, defaultExpanded = false, }: ToolUseBlockProps) { const [isExpanded, setIsExpanded] = useState(defaultExpanded || isRunning); const startRef = useRef(startedAt ?? Date.now()); // Auto-expand while running, retain state after completion useEffect(() => { if (isRunning) setIsExpanded(true); }, [isRunning]); const Icon = getToolIcon(toolName); const label = getToolLabel(toolName); const borderColor = isRunning ? "border-brand-600/40" : isError ? "border-red-800/50" : "border-surface-700"; const headerBg = isRunning ? "bg-brand-950/30" : isError ? "bg-red-950/20" : "bg-surface-850"; return (
{/* Header row */} {/* Expandable body */} {isExpanded && (
{children}
)}
); }