import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Bot, Heart, Activity, Pause, Play, Square, Trash2, RefreshCw, Settings, FileText, ActivitySquare, X, Copy, ExternalLink, CheckCircle, XCircle, Loader2 } from "lucide-react"; import type { AgentDetail, AgentState, AgentHeartbeatRun } from "../api"; import { fetchAgent, updateAgentState, deleteAgent, fetchAgentLogs } from "../api"; import type { AgentLogEntry } from "@fusion/core"; /** * Simple className utility - joins class names conditionally */ function cn(...classes: (string | boolean | undefined | null)[]): string { return classes.filter(Boolean).join(" "); } /** * Format an ISO timestamp to a relative time string. */ function relativeTime(iso: string): string { const now = Date.now(); const then = new Date(iso).getTime(); const diffMs = now - then; // Future if (diffMs < 0) { const absDiff = Math.abs(diffMs); if (absDiff < 60_000) return "in a moment"; if (absDiff < 3_600_000) return `in ${Math.floor(absDiff / 60_000)}m`; if (absDiff < 86_400_000) return `in ${Math.floor(absDiff / 3_600_000)}h`; return `in ${Math.floor(absDiff / 86_400_000)}d`; } // Past if (diffMs < 60_000) return "just now"; if (diffMs < 3_600_000) return `${Math.floor(diffMs / 60_000)}m ago`; if (diffMs < 86_400_000) return `${Math.floor(diffMs / 3_600_000)}h ago`; return `${Math.floor(diffMs / 86_400_000)}d ago`; } interface AgentDetailViewProps { agentId: string; projectId?: string; onClose: () => void; addToast: (message: string, type?: "success" | "error") => void; } type TabId = "dashboard" | "logs" | "config" | "runs"; const TABS: { id: TabId; label: string; icon: typeof Activity }[] = [ { id: "dashboard", label: "Dashboard", icon: ActivitySquare }, { id: "logs", label: "Logs", icon: FileText }, { id: "runs", label: "Runs", icon: Activity }, { id: "config", label: "Settings", icon: Settings }, ]; const STATE_COLORS: Record = { idle: { bg: "rgba(139, 148, 158, 0.15)", text: "#8b949e", border: "#8b949e" }, active: { bg: "rgba(46, 160, 67, 0.15)", text: "#3fb950", border: "#3fb950" }, paused: { bg: "rgba(227, 179, 65, 0.15)", text: "#e3b541", border: "#e3b541" }, terminated: { bg: "rgba(248, 81, 73, 0.15)", text: "#f85149", border: "#f85149" }, }; const RUN_STATUS_ICONS: Record = { completed: { icon: CheckCircle, color: "text-green-500" }, failed: { icon: XCircle, color: "text-red-500" }, active: { icon: Loader2, color: "text-cyan-500 animate-spin" }, terminated: { icon: Square, color: "text-gray-500" }, }; export function AgentDetailView({ agentId, projectId, onClose, addToast }: AgentDetailViewProps) { const [agent, setAgent] = useState(null); const [logs, setLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); const [activeTab, setActiveTab] = useState("dashboard"); const [isStreaming, setIsStreaming] = useState(false); const logContainerRef = useRef(null); const loadAgent = useCallback(async () => { setIsLoading(true); try { const data = await fetchAgent(agentId, projectId); setAgent(data); } catch (err: any) { addToast(`Failed to load agent: ${err.message}`, "error"); onClose(); } finally { setIsLoading(false); } }, [agentId, addToast, onClose, projectId]); const loadLogs = useCallback(async () => { // Agent logs are tied to tasks, not agents directly. // If the agent has a current task, we could show those logs. // For now, we'll show heartbeat runs as the "activity" for the agent. // If the agent is working on a task, we could show task logs. if (agent?.taskId) { try { const data = await fetchAgentLogs(agent.taskId, projectId); setLogs(data); } catch (err: any) { console.error("Failed to load task logs:", err); } } }, [agent?.taskId, projectId]); useEffect(() => { void loadAgent(); }, [loadAgent]); useEffect(() => { if (agent?.taskId) { void loadLogs(); } }, [agent?.taskId, loadLogs]); // Set up SSE for live log streaming when viewing logs tab with a task useEffect(() => { if (activeTab !== "logs" || !agent?.taskId) { setIsStreaming(false); return; } const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : ""; const es = new EventSource(`/api/tasks/${encodeURIComponent(agent.taskId)}/logs/stream${query}`); const handleAgentLog = (e: MessageEvent) => { try { const entry: AgentLogEntry = JSON.parse(e.data); setLogs(prev => [entry, ...prev]); // Auto-scroll to top for new entries const container = logContainerRef.current; if (container && container.scrollTop < 50) { container.scrollTop = 0; } } catch { // Ignore parse errors } }; es.addEventListener("agent:log", handleAgentLog as EventListener); es.onerror = () => { setIsStreaming(false); }; es.onopen = () => { setIsStreaming(true); }; return () => { es.removeEventListener("agent:log", handleAgentLog as EventListener); es.close(); setIsStreaming(false); }; }, [agent?.taskId, activeTab, projectId]); const handleStateChange = async (newState: AgentState) => { try { await updateAgentState(agentId, newState, projectId); addToast(`Agent state updated to ${newState}`, "success"); void loadAgent(); } catch (err: any) { addToast(`Failed to update state: ${err.message}`, "error"); } }; const handleDelete = async () => { if (!agent || !confirm(`Delete agent "${agent.name}"? This cannot be undone.`)) return; try { await deleteAgent(agentId, projectId); addToast(`Agent "${agent.name}" deleted`, "success"); onClose(); } catch (err: any) { addToast(`Failed to delete agent: ${err.message}`, "error"); } }; const getHealthStatus = () => { if (!agent) return { label: "Unknown", color: "#888" }; if (agent.state === "terminated") { return { label: "Terminated", color: "#f85149" }; } if (agent.state === "paused") { return { label: "Paused", color: "#e3b541" }; } if (!agent.lastHeartbeatAt) { return { label: agent.state === "active" ? "Starting..." : "Idle", color: "#8b949e" }; } const lastHeartbeat = new Date(agent.lastHeartbeatAt).getTime(); const elapsed = Date.now() - lastHeartbeat; const timeoutMs = 60000; if (elapsed > timeoutMs) { return { label: "Unresponsive", color: "#f85149" }; } return { label: "Healthy", color: "#3fb950" }; }; const copyAgentId = () => { if (agent) { navigator.clipboard.writeText(agent.id); addToast("Agent ID copied to clipboard", "success"); } }; if (isLoading) { return (
e.target === e.currentTarget && onClose()}>
Loading agent...
); } if (!agent) { return null; } const stateStyle = STATE_COLORS[agent.state]; const health = getHealthStatus(); const runs = (agent as any).completedRuns || []; const activeRun = (agent as any).activeRun; return (
e.target === e.currentTarget && onClose()}>
{/* Header */}

{agent.name}

{agent.state} {health.label === "Healthy" && } {health.label === "Unresponsive" && } {health.label}
{/* State-dependent action buttons */} {agent.state === "idle" && ( )} {agent.state === "active" && ( <> )} {agent.state === "paused" && ( <> )} {agent.state === "terminated" && ( )}
{/* Tabs */}
{TABS.map(tab => ( ))}
{/* Tab Content */}
{activeTab === "dashboard" && ( )} {activeTab === "logs" && ( )} {activeTab === "runs" && ( )} {activeTab === "config" && ( )}
{/* Footer with agent ID */}
{agent.id} {agent.taskId && ( <> | Working on: {agent.taskId} )}
); } // ── Dashboard Tab ─────────────────────────────────────────────────────────── function DashboardTab({ agent, health }: { agent: AgentDetail; health: { label: string; color: string }; }) { const stateStyle = STATE_COLORS[agent.state]; const stats = useMemo(() => { const runs = (agent as any).completedRuns || []; const today = new Date(); today.setHours(0, 0, 0, 0); const todayRuns = runs.filter((r: AgentHeartbeatRun) => new Date(r.startedAt) >= today ); const successfulRuns = runs.filter((r: AgentHeartbeatRun) => r.status === "completed" ); return { totalRuns: runs.length, todayRuns: todayRuns.length, successfulRuns: successfulRuns.length, successRate: runs.length > 0 ? Math.round((successfulRuns.length / runs.length) * 100) : 0, }; }, [agent]); return (
{/* Agent Info Card */}

Agent Information

Name {agent.name}
Role {agent.role}
State {agent.state}
Health {health.label}
Created {new Date(agent.createdAt).toLocaleDateString()}
Last Heartbeat {agent.lastHeartbeatAt ? relativeTime(agent.lastHeartbeatAt) : "Never" }
{/* Stats Cards */}

Statistics

{stats.totalRuns}
Total Runs
{stats.todayRuns}
Runs Today
{stats.successRate}%
Success Rate
{/* Current Task */} {agent.taskId && (

Current Task

{agent.taskId} View Task
)} {/* Metadata */} {agent.metadata && Object.keys(agent.metadata).length > 0 && (

Metadata

            {JSON.stringify(agent.metadata, null, 2)}
          
)}
); } // ── Logs Tab ────────────────────────────────────────────────────────────── function LogsTab({ logs, isStreaming, containerRef, hasTask }: { logs: AgentLogEntry[]; isStreaming: boolean; containerRef: React.RefObject; hasTask: boolean; }) { if (!hasTask) { return (

No task assigned

Agent logs are available when the agent is assigned to a task

); } return (
{logs.length} entries {isStreaming && ( Live )}
{logs.length === 0 ? (

No log entries yet

{isStreaming ? "Waiting for activity..." : "Logs will appear here when the agent is active"}

) : ( logs.map((entry, i) => ( )) )}
); } function LogEntry({ entry }: { entry: AgentLogEntry }) { const getEntryStyles = () => { switch (entry.type) { case "tool": return { color: "var(--accent)", borderLeft: "3px solid var(--accent)", background: "rgba(124, 92, 191, 0.08)", }; case "tool_result": return { color: "var(--success)", borderLeft: "3px solid var(--success)", background: "rgba(76, 175, 80, 0.06)", }; case "tool_error": return { color: "var(--error)", borderLeft: "3px solid var(--error)", background: "rgba(229, 57, 53, 0.06)", }; case "thinking": return { color: "var(--text-muted)", fontStyle: "italic" as const, opacity: 0.7, }; default: return { color: "var(--text-primary)", }; } }; const styles = getEntryStyles(); const timestamp = new Date(entry.timestamp).toLocaleTimeString(); return (
[{timestamp}] {entry.agent && ( [{entry.agent}] )} {entry.type === "tool" && } {entry.type === "tool_result" && } {entry.type === "tool_error" && } {entry.text} {entry.detail && ( — {entry.detail} )}
); } // ── Runs Tab ─────────────────────────────────────────────────────────────── function RunsTab({ runs, activeRun, addToast }: { runs: AgentHeartbeatRun[]; activeRun?: AgentHeartbeatRun; addToast: (msg: string, type?: "success" | "error") => void; }) { if (runs.length === 0 && !activeRun) { return (

No runs yet

Heartbeat runs will appear here

); } const sortedRuns = [...runs].sort( (a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime() ); return (
{activeRun && (
Live Run Active
Started {relativeTime(activeRun.startedAt)}
)} {sortedRuns.map((run, i) => { const statusInfo = RUN_STATUS_ICONS[run.status] || RUN_STATUS_ICONS.completed; const StatusIcon = statusInfo.icon; const duration = run.endedAt ? formatDuration(new Date(run.startedAt), new Date(run.endedAt)) : "In progress"; return (
#{i + 1} {run.id.slice(0, 8)} {run.status}
Started {relativeTime(run.startedAt)} {duration}
); })}
); } function formatDuration(start: Date, end: Date): string { const diff = Math.floor((end.getTime() - start.getTime()) / 1000); if (diff < 60) return `${diff}s`; if (diff < 3600) return `${Math.floor(diff / 60)}m ${diff % 60}s`; return `${Math.floor(diff / 3600)}h ${Math.floor((diff % 3600) / 60)}m`; } // ── Config Tab ───────────────────────────────────────────────────────────── function ConfigTab({ agent }: { agent: AgentDetail; }) { return (

Agent Configuration

Configure agent settings and behavior.

Name changes coming soon
Role changes coming soon

Advanced Settings

Advanced configuration options for power users.

Advanced configuration options will be available in a future update.

This will include model selection, heartbeat intervals, and environment variables.

); }