import { IconLoader2, IconCheck, IconChevronRight, IconExternalLink, IconAlertCircle, IconPlayerStop, IconSubtask, } from "@tabler/icons-react"; import React, { useState, useEffect, useRef, useCallback } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { agentNativePath } from "./api-path.js"; import { cn } from "./utils.js"; export interface AgentTaskCardProps { taskId: string; threadId: string; description: string; onOpen?: (threadId: string) => void; } /** * Rich preview card for a sub-agent task. Listens for agent-task-event * CustomEvents to update its state in real-time. */ export function AgentTaskCard({ taskId, threadId, description, onOpen, }: AgentTaskCardProps) { const [expanded, setExpanded] = useState(true); const [status, setStatus] = useState<"running" | "completed" | "errored">( "running", ); const [preview, setPreview] = useState(""); const [currentStep, setCurrentStep] = useState(""); const [summary, setSummary] = useState(""); const previewRef = useRef(null); useEffect(() => { function handleEvent(e: Event) { const detail = (e as CustomEvent).detail; if (!detail?.taskId || detail.taskId !== taskId) return; if (detail.type === "agent_task_update") { if (detail.preview != null) setPreview(detail.preview); if (detail.currentStep != null) setCurrentStep(detail.currentStep); } else if (detail.type === "agent_task_complete") { setStatus("completed"); if (detail.summary) setSummary(detail.summary); setCurrentStep(""); } else if (detail.type === "agent_task" && detail.status === "errored") { setStatus("errored"); setCurrentStep(""); } } window.addEventListener("agent-task-event", handleEvent); return () => window.removeEventListener("agent-task-event", handleEvent); }, [taskId]); // Poll for task status when running — the main chat's SSE stream may close // before the sub-agent completes, so SSE events alone aren't reliable. useEffect(() => { if (status !== "running") return; let stopped = false; const poll = async () => { while (!stopped) { await new Promise((r) => setTimeout(r, 3000)); if (stopped) break; try { const res = await fetch( agentNativePath( `/_agent-native/application-state/agent-task:${taskId}`, ), ); if (!res.ok) continue; const data = await res.json(); // The HTTP handler returns the value directly (not wrapped) const task = data?.value ?? data; if (!task || !task.status) continue; if (task.status === "completed") { setStatus("completed"); if (task.summary) setSummary(task.summary); if (task.preview) setPreview(task.preview); setCurrentStep(""); break; } else if (task.status === "errored") { setStatus("errored"); if (task.summary) setSummary(task.summary); setCurrentStep(""); break; } else { // Still running — update preview from persisted state if (task.preview) setPreview(task.preview); if (task.currentStep) setCurrentStep(task.currentStep); } } catch { // Polling error — continue } } }; poll(); return () => { stopped = true; }; }, [status, taskId, threadId]); // Auto-scroll preview to bottom useEffect(() => { if (previewRef.current && status === "running") { previewRef.current.scrollTop = previewRef.current.scrollHeight; } }, [preview, status]); const handleOpen = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); onOpen?.(threadId); }, [onOpen, threadId], ); const handleStop = useCallback( async (e: React.MouseEvent) => { e.stopPropagation(); // Optimistic UI: mark stopped immediately setStatus("errored"); setCurrentStep(""); try { await fetch( agentNativePath( `/_agent-native/agent-chat/runs/run-task-${taskId}/stop`, ), { method: "POST", headers: { "X-Agent-Native-CSRF": "1" }, }, ); } catch { // best-effort } }, [taskId], ); const isRunning = status === "running"; const isComplete = status === "completed"; const isError = status === "errored"; const taskTitle = description.trim() || "Task"; const currentStepText = currentStep.trim(); const statusLabel = isRunning ? "Running" : isError ? "Error" : "Done"; const statusClassName = cn( "inline-flex h-5 shrink-0 items-center gap-1 rounded-md px-1.5 text-[10px] font-medium", isError ? "bg-destructive/10 text-destructive" : isComplete ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" : "bg-muted text-muted-foreground", ); const displayText = isComplete && summary ? summary : preview; const hasContent = displayText.length > 0; const emptyMessage = isRunning ? "Waiting for updates" : isError ? "No error details yet" : "No summary available"; return (
{/* Header */} {expanded && isRunning && currentStepText && (
Now: {currentStepText}
)} {/* Preview content */} {expanded && hasContent && (
{displayText.length > 800 ? "..." + displayText.slice(-800) : displayText}
)} {/* Footer with Open / Stop buttons */} {expanded && (
{!hasContent ? emptyMessage : ""}
{isRunning && ( )}
)}
); }