"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { Bot, ChevronRight, Loader2, ArrowDown, Brain, AlertCircle, Clock, CheckCircle2, XCircle, Square } from "lucide-react"; import { api } from "@/shared/api"; import { useWSEvent } from "@/features/realtime"; import type { TaskMessagePayload, TaskCompletedPayload, TaskFailedPayload, TaskCancelledPayload } from "@/shared/types/events"; import type { AgentTask } from "@/shared/types/agent"; import { cn } from "@/lib/utils"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { useActorName } from "@/features/workspace"; import { redactSecrets } from "../utils/redact"; // ─── Shared types & helpers ───────────────────────────────────────────────── /** A unified timeline entry: tool calls, thinking, text, and errors in chronological order. */ interface TimelineItem { seq: number; type: "tool_use" | "tool_result" | "thinking" | "text" | "error"; tool?: string; content?: string; input?: Record; output?: string; } function formatElapsed(startedAt: string): string { const elapsed = Date.now() - new Date(startedAt).getTime(); const seconds = Math.floor(elapsed / 1000); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const secs = seconds % 60; return `${minutes}m ${secs}s`; } function formatDuration(start: string, end: string): string { const ms = new Date(end).getTime() - new Date(start).getTime(); const seconds = Math.floor(ms / 1000); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const secs = seconds % 60; return `${minutes}m ${secs}s`; } function shortenPath(p: string): string { const parts = p.split("/"); if (parts.length <= 3) return p; return ".../" + parts.slice(-2).join("/"); } function getToolSummary(item: TimelineItem): string { if (!item.input) return ""; const inp = item.input as Record; // WebSearch / web search if (inp.query) return inp.query; // File operations if (inp.file_path) return shortenPath(inp.file_path); if (inp.path) return shortenPath(inp.path); if (inp.pattern) return inp.pattern; // Bash if (inp.description) return String(inp.description); if (inp.command) { const cmd = String(inp.command); return cmd.length > 100 ? cmd.slice(0, 100) + "..." : cmd; } // Agent if (inp.prompt) { const p = String(inp.prompt); return p.length > 100 ? p.slice(0, 100) + "..." : p; } // Skill if (inp.skill) return String(inp.skill); // Fallback: show first string value for (const v of Object.values(inp)) { if (typeof v === "string" && v.length > 0 && v.length < 120) return v; } return ""; } /** Build a chronologically ordered timeline from raw messages. */ function buildTimeline(msgs: TaskMessagePayload[]): TimelineItem[] { const items: TimelineItem[] = []; for (const msg of msgs) { items.push({ seq: msg.seq, type: msg.type, tool: msg.tool, content: msg.content ? redactSecrets(msg.content) : msg.content, input: msg.input, output: msg.output ? redactSecrets(msg.output) : msg.output, }); } return items.sort((a, b) => a.seq - b.seq); } // ─── AgentLiveCard (real-time view) ──────────────────────────────────────── interface AgentLiveCardProps { issueId: string; agentName?: string; } export function AgentLiveCard({ issueId, agentName }: AgentLiveCardProps) { const { getActorName } = useActorName(); const [activeTask, setActiveTask] = useState(null); const [items, setItems] = useState([]); const [elapsed, setElapsed] = useState(""); const [autoScroll, setAutoScroll] = useState(true); const [cancelling, setCancelling] = useState(false); const scrollRef = useRef(null); const seenSeqs = useRef(new Set()); // Check for active task on mount useEffect(() => { let cancelled = false; api.getActiveTaskForIssue(issueId).then(({ task }) => { if (!cancelled) { setActiveTask(task); if (task) { api.listTaskMessages(task.id).then((msgs) => { if (!cancelled) { const timeline = buildTimeline(msgs); setItems(timeline); for (const m of msgs) seenSeqs.current.add(`${m.task_id}:${m.seq}`); } }).catch(() => {}); } } }).catch(() => {}); return () => { cancelled = true; }; }, [issueId]); // Handle real-time task messages useWSEvent( "task:message", useCallback((payload: unknown) => { const msg = payload as TaskMessagePayload; if (msg.issue_id !== issueId) return; const key = `${msg.task_id}:${msg.seq}`; if (seenSeqs.current.has(key)) return; seenSeqs.current.add(key); setItems((prev) => { const item: TimelineItem = { seq: msg.seq, type: msg.type, tool: msg.tool, content: msg.content, input: msg.input, output: msg.output, }; const next = [...prev, item]; next.sort((a, b) => a.seq - b.seq); return next; }); }, [issueId]), ); // Handle task completion/failure useWSEvent( "task:completed", useCallback((payload: unknown) => { const p = payload as TaskCompletedPayload; if (p.issue_id !== issueId) return; setActiveTask(null); setItems([]); seenSeqs.current.clear(); setCancelling(false); }, [issueId]), ); useWSEvent( "task:failed", useCallback((payload: unknown) => { const p = payload as TaskFailedPayload; if (p.issue_id !== issueId) return; setActiveTask(null); setItems([]); seenSeqs.current.clear(); setCancelling(false); }, [issueId]), ); useWSEvent( "task:cancelled", useCallback((payload: unknown) => { const p = payload as TaskCancelledPayload; if (p.issue_id !== issueId) return; setActiveTask(null); setItems([]); seenSeqs.current.clear(); setCancelling(false); }, [issueId]), ); // Pick up new tasks useWSEvent( "task:dispatch", useCallback(() => { api.getActiveTaskForIssue(issueId).then(({ task }) => { if (task) { setActiveTask(task); setItems([]); seenSeqs.current.clear(); } }).catch(() => {}); }, [issueId]), ); // Elapsed time useEffect(() => { if (!activeTask?.started_at && !activeTask?.dispatched_at) return; const ref = activeTask.started_at ?? activeTask.dispatched_at!; setElapsed(formatElapsed(ref)); const interval = setInterval(() => setElapsed(formatElapsed(ref)), 1000); return () => clearInterval(interval); }, [activeTask?.started_at, activeTask?.dispatched_at]); // Auto-scroll useEffect(() => { if (autoScroll && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [items, autoScroll]); const handleScroll = useCallback(() => { if (!scrollRef.current) return; const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; setAutoScroll(scrollHeight - scrollTop - clientHeight < 40); }, []); const handleCancel = useCallback(async () => { if (!activeTask || cancelling) return; setCancelling(true); try { await api.cancelTask(issueId, activeTask.id); } catch { setCancelling(false); } }, [activeTask, issueId, cancelling]); if (!activeTask) return null; const toolCount = items.filter((i) => i.type === "tool_use").length; return (
{/* Header */}
{(activeTask?.agent_id ? getActorName("agent", activeTask.agent_id) : agentName) ?? "Agent"} is working
{elapsed} {toolCount > 0 && ( {toolCount} tool {toolCount === 1 ? "call" : "calls"} )}
{/* Timeline content */} {items.length > 0 && (
{items.map((item, idx) => ( ))} {!autoScroll && ( )}
)}
); } // ─── TaskRunHistory (past execution logs) ────────────────────────────────── interface TaskRunHistoryProps { issueId: string; } export function TaskRunHistory({ issueId }: TaskRunHistoryProps) { const [tasks, setTasks] = useState([]); const [open, setOpen] = useState(false); useEffect(() => { api.listTasksByIssue(issueId).then(setTasks).catch(() => {}); }, [issueId]); // Refresh when a task completes useWSEvent( "task:completed", useCallback((payload: unknown) => { const p = payload as TaskCompletedPayload; if (p.issue_id !== issueId) return; api.listTasksByIssue(issueId).then(setTasks).catch(() => {}); }, [issueId]), ); useWSEvent( "task:failed", useCallback((payload: unknown) => { const p = payload as TaskFailedPayload; if (p.issue_id !== issueId) return; api.listTasksByIssue(issueId).then(setTasks).catch(() => {}); }, [issueId]), ); // Refresh when a task is cancelled useWSEvent( "task:cancelled", useCallback((payload: unknown) => { const p = payload as TaskCancelledPayload; if (p.issue_id !== issueId) return; api.listTasksByIssue(issueId).then(setTasks).catch(() => {}); }, [issueId]), ); const completedTasks = tasks.filter((t) => t.status === "completed" || t.status === "failed" || t.status === "cancelled"); if (completedTasks.length === 0) return null; return ( Execution history ({completedTasks.length})
{completedTasks.map((task) => ( ))}
); } function TaskRunEntry({ task }: { task: AgentTask }) { const [open, setOpen] = useState(false); const [items, setItems] = useState(null); const loadMessages = useCallback(() => { if (items !== null) return; // already loaded api.listTaskMessages(task.id).then((msgs) => { setItems(buildTimeline(msgs)); }).catch(() => setItems([])); }, [task.id, items]); useEffect(() => { if (open) loadMessages(); }, [open, loadMessages]); const duration = task.started_at && task.completed_at ? formatDuration(task.started_at, task.completed_at) : null; return ( {task.status === "completed" ? ( ) : ( )} {new Date(task.created_at).toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })} {duration && {duration}} {task.status}
{items === null ? (
Loading...
) : items.length === 0 ? (

No execution data recorded.

) : ( items.map((item, idx) => ( )) )}
); } // ─── Shared timeline row rendering ────────────────────────────────────────── function TimelineRow({ item }: { item: TimelineItem }) { switch (item.type) { case "tool_use": return ; case "tool_result": return ; case "thinking": return ; case "text": return ; case "error": return ; default: return null; } } function ToolCallRow({ item }: { item: TimelineItem }) { const [open, setOpen] = useState(false); const summary = getToolSummary(item); const hasInput = item.input && Object.keys(item.input).length > 0; return ( {item.tool} {summary && {summary}} {hasInput && (
            {redactSecrets(JSON.stringify(item.input, null, 2))}
          
)}
); } function ToolResultRow({ item }: { item: TimelineItem }) { const [open, setOpen] = useState(false); const output = item.output ?? ""; if (!output) return null; const preview = output.length > 120 ? output.slice(0, 120) + "..." : output; return ( {item.tool ? `${item.tool} result: ` : "result: "}{preview}
          {output.length > 4000 ? output.slice(0, 4000) + "\n... (truncated)" : output}
        
); } function ThinkingRow({ item }: { item: TimelineItem }) { const [open, setOpen] = useState(false); const text = item.content ?? ""; if (!text) return null; const preview = text.length > 150 ? text.slice(0, 150) + "..." : text; return ( {preview}
          {text}
        
); } function TextRow({ item }: { item: TimelineItem }) { const text = item.content ?? ""; if (!text.trim()) return null; const lines = text.trim().split("\n").filter(Boolean); const last = lines[lines.length - 1] ?? ""; if (!last) return null; return (
{last}
); } function ErrorRow({ item }: { item: TimelineItem }) { return (
{item.content}
); }