import { useState, useEffect, useCallback, useRef } from "react"; import type { JSX } from "react"; import { X, Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, Filter } from "lucide-react"; import type { Agent, AgentCapability, AgentState } from "../api"; import { fetchAgents, createAgent, updateAgent, updateAgentState, deleteAgent } from "../api"; interface AgentListModalProps { isOpen: boolean; onClose: () => void; addToast: (message: string, type?: "success" | "error") => void; projectId?: string; } const AGENT_ROLES: { value: AgentCapability; label: string; icon: string }[] = [ { value: "triage", label: "Triage", icon: "🔍" }, { value: "executor", label: "Executor", icon: "⚡" }, { value: "reviewer", label: "Reviewer", icon: "👁" }, { value: "merger", label: "Merger", icon: "🔀" }, { value: "scheduler", label: "Scheduler", icon: "⏰" }, { value: "engineer", label: "Engineer", icon: "🛠" }, { value: "custom", label: "Custom", icon: "🔧" }, ]; const STATE_COLORS: Record = { idle: { bg: "var(--state-idle-bg)", text: "var(--state-idle-text)", border: "var(--state-idle-border)" }, active: { bg: "var(--state-active-bg)", text: "var(--state-active-text)", border: "var(--state-active-border)" }, running: { bg: "var(--state-active-bg)", text: "var(--state-active-text)", border: "var(--state-active-border)" }, paused: { bg: "var(--state-paused-bg)", text: "var(--state-paused-text)", border: "var(--state-paused-border)" }, terminated: { bg: "var(--state-error-bg)", text: "var(--state-error-text)", border: "var(--state-error-border)" }, error: { bg: "var(--state-error-bg)", text: "var(--state-error-text)", border: "var(--state-error-border)" }, }; export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentListModalProps) { const [agents, setAgents] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isCreating, setIsCreating] = useState(false); const [newAgentName, setNewAgentName] = useState(""); const [newAgentRole, setNewAgentRole] = useState("custom"); const [filterState, setFilterState] = useState("all"); const [view, setView] = useState<"board" | "list">(() => { if (typeof window === "undefined") return "list"; const saved = localStorage.getItem("kb-agent-view"); return (saved === "board" || saved === "list") ? saved : "list"; }); // Persist view preference to localStorage useEffect(() => { localStorage.setItem("kb-agent-view", view); }, [view]); const [editingRoleForAgent, setEditingRoleForAgent] = useState(null); const roleSelectRef = useRef(null); const loadAgents = useCallback(async () => { setIsLoading(true); try { const filter = filterState !== "all" ? { state: filterState } : undefined; const data = await fetchAgents(filter, projectId); setAgents(data); } catch (err: any) { addToast(`Failed to load agents: ${err.message}`, "error"); } finally { setIsLoading(false); } }, [filterState, addToast, projectId]); useEffect(() => { if (isOpen) { void loadAgents(); } }, [isOpen, loadAgents]); const handleCreate = async () => { if (!newAgentName.trim()) return; try { await createAgent({ name: newAgentName.trim(), role: newAgentRole }, projectId); addToast(`Agent "${newAgentName}" created`, "success"); setNewAgentName(""); setIsCreating(false); void loadAgents(); } catch (err: any) { addToast(`Failed to create agent: ${err.message}`, "error"); } }; const handleStateChange = async (agentId: string, newState: AgentState) => { try { await updateAgentState(agentId, newState, projectId); addToast(`Agent state updated to ${newState}`, "success"); void loadAgents(); } catch (err: any) { addToast(`Failed to update state: ${err.message}`, "error"); } }; const handleDelete = async (agentId: string, agentName: string) => { if (!confirm(`Delete agent "${agentName}"? This cannot be undone.`)) return; try { await deleteAgent(agentId, projectId); addToast(`Agent "${agentName}" deleted`, "success"); void loadAgents(); } catch (err: any) { addToast(`Failed to delete agent: ${err.message}`, "error"); } }; const handleRoleChange = async (agentId: string, newRole: AgentCapability) => { const agent = agents.find(a => a.id === agentId); if (!agent) return; // If same role, just cancel editing without API call if (agent.role === newRole) { setEditingRoleForAgent(null); return; } try { await updateAgent(agentId, { role: newRole }, projectId); addToast(`Agent role updated to ${AGENT_ROLES.find(r => r.value === newRole)?.label ?? newRole}`, "success"); setEditingRoleForAgent(null); void loadAgents(); } catch (err: any) { addToast(`Failed to update role: ${err.message}`, "error"); } }; const handleRoleKeyDown = (e: React.KeyboardEvent, agentId: string) => { if (e.key === "Escape") { setEditingRoleForAgent(null); } }; const getRoleLabel = (role: AgentCapability) => AGENT_ROLES.find(r => r.value === role)?.label ?? role; const getRoleIcon = (role: AgentCapability) => AGENT_ROLES.find(r => r.value === role)?.icon ?? "🤖"; const getHealthStatus = (agent: Agent): { label: string; icon: JSX.Element; color: string } => { if (agent.state === "terminated") { return { label: "Terminated", icon: , color: "var(--state-error-text)" }; } if (agent.state === "error") { return { label: agent.lastError ?? "Error", icon: , color: "var(--state-error-text)" }; } if (agent.state === "running") { return { label: "Running", icon: , color: "var(--state-active-text)" }; } if (agent.state === "paused") { return { label: agent.pauseReason ?? "Paused", icon: , color: "var(--state-paused-text)" }; } if (!agent.lastHeartbeatAt) { return { label: agent.state === "active" ? "Starting..." : "Idle", icon: , color: "var(--text-secondary)" }; } const lastHeartbeat = new Date(agent.lastHeartbeatAt).getTime(); const elapsed = Date.now() - lastHeartbeat; const timeoutMs = 60000; // 60 second timeout if (elapsed > timeoutMs) { return { label: "Unresponsive", icon: , color: "var(--state-error-text)" }; } return { label: "Healthy", icon: , color: "var(--state-active-text)" }; }; if (!isOpen) return null; return (
e.target === e.currentTarget && onClose()}>

Agents

{/* Filter and Create Bar */}
{/* Create Form */} {isCreating && (
setNewAgentName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleCreate()} className="input" autoFocus />
)} {/* Agent List */}
{agents.length === 0 ? (

No agents found

Create an agent to get started

) : view === "board" ? ( // Board view: compact grid layout agents.map(agent => { const health = getHealthStatus(agent); const stateStyle = STATE_COLORS[agent.state]; return (
{getRoleIcon(agent.role)} {agent.state} {health.icon}
{agent.name}
{agent.id}
{agent.state === "idle" && ( <> )} {agent.state === "active" && ( <> )} {agent.state === "paused" && ( <> )} {agent.state === "running" && ( <> )} {agent.state === "error" && ( <> )} {agent.state === "terminated" && ( )}
); }) ) : ( // List view: detailed card layout agents.map(agent => { const health = getHealthStatus(agent); const stateStyle = STATE_COLORS[agent.state]; return (
{editingRoleForAgent === agent.id ? ( ) : ( setEditingRoleForAgent(agent.id)} title="Click to change role" role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { setEditingRoleForAgent(agent.id); } }} > {getRoleIcon(agent.role)} )}
{agent.name} {agent.id}
{agent.state} {health.icon} {health.label} {getRoleLabel(agent.role)}
{agent.taskId && (
Working on: {agent.taskId}
)} {agent.lastHeartbeatAt && (
Last heartbeat: {new Date(agent.lastHeartbeatAt).toLocaleString()}
)}
{agent.state === "idle" && ( <> )} {agent.state === "active" && ( <> )} {agent.state === "paused" && ( <> )} {agent.state === "running" && ( <> )} {agent.state === "error" && ( <> )} {agent.state === "terminated" && ( )}
); }) )}
); }