import { useState, useEffect, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { agentsApi, type OrgNode } from "../api/agents"; import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { StatusBadge } from "../components/StatusBadge"; import { EntityRow } from "../components/EntityRow"; import { EmptyState } from "../components/EmptyState"; import { formatCents, relativeTime, cn } from "../lib/utils"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Bot, Plus, List, GitBranch } from "lucide-react"; import type { Agent } from "@paperclip/shared"; const adapterLabels: Record = { claude_local: "Claude", codex_local: "Codex", process: "Process", http: "HTTP", }; const roleLabels: Record = { ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO", engineer: "Engineer", designer: "Designer", pm: "PM", qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General", }; type FilterTab = "all" | "active" | "paused" | "error"; function matchesFilter(status: string, tab: FilterTab): boolean { if (tab === "all") return true; if (tab === "active") return status === "active" || status === "running" || status === "idle"; if (tab === "paused") return status === "paused"; if (tab === "error") return status === "error" || status === "terminated"; return true; } function filterAgents(agents: Agent[], tab: FilterTab): Agent[] { return agents.filter((a) => matchesFilter(a.status, tab)); } function filterOrgTree(nodes: OrgNode[], tab: FilterTab): OrgNode[] { if (tab === "all") return nodes; return nodes.reduce((acc, node) => { const filteredReports = filterOrgTree(node.reports, tab); if (matchesFilter(node.status, tab) || filteredReports.length > 0) { acc.push({ ...node, reports: filteredReports }); } return acc; }, []); } export function Agents() { const { selectedCompanyId } = useCompany(); const { openNewAgent } = useDialog(); const { setBreadcrumbs } = useBreadcrumbs(); const navigate = useNavigate(); const [tab, setTab] = useState("all"); const [view, setView] = useState<"list" | "org">("org"); const { data: agents, isLoading, error } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: orgTree } = useQuery({ queryKey: queryKeys.org(selectedCompanyId!), queryFn: () => agentsApi.org(selectedCompanyId!), enabled: !!selectedCompanyId && view === "org", }); const agentMap = useMemo(() => { const map = new Map(); for (const a of agents ?? []) map.set(a.id, a); return map; }, [agents]); useEffect(() => { setBreadcrumbs([{ label: "Agents" }]); }, [setBreadcrumbs]); if (!selectedCompanyId) { return ; } const filtered = filterAgents(agents ?? [], tab); const filteredOrg = filterOrgTree(orgTree ?? [], tab); return (
setTab(v as FilterTab)}> All{agents ? ` (${agents.length})` : ""} Active Paused Error
{/* View toggle */}
{isLoading &&

Loading...

} {error &&

{error.message}

} {agents && agents.length === 0 && ( )} {/* List view */} {view === "list" && filtered.length > 0 && (
{filtered.map((agent) => { const budgetPct = agent.budgetMonthlyCents > 0 ? Math.round((agent.spentMonthlyCents / agent.budgetMonthlyCents) * 100) : 0; return ( navigate(`/agents/${agent.id}`)} leading={ } trailing={
{adapterLabels[agent.adapterType] ?? agent.adapterType} {agent.lastHeartbeatAt && ( {relativeTime(agent.lastHeartbeatAt)} )}
90 ? "bg-red-400" : budgetPct > 70 ? "bg-yellow-400" : "bg-green-400" }`} style={{ width: `${Math.min(100, budgetPct)}%` }} />
{formatCents(agent.spentMonthlyCents)} / {formatCents(agent.budgetMonthlyCents)}
} /> ); })}
)} {view === "list" && agents && agents.length > 0 && filtered.length === 0 && (

No agents match the selected filter.

)} {/* Org chart view */} {view === "org" && filteredOrg.length > 0 && (
{filteredOrg.map((node) => ( ))}
)} {view === "org" && orgTree && orgTree.length > 0 && filteredOrg.length === 0 && (

No agents match the selected filter.

)} {view === "org" && orgTree && orgTree.length === 0 && (

No organizational hierarchy defined.

)}
); } function OrgTreeNode({ node, depth, navigate, agentMap, }: { node: OrgNode; depth: number; navigate: (path: string) => void; agentMap: Map; }) { const agent = agentMap.get(node.id); const statusColor = node.status === "running" ? "bg-cyan-400 animate-pulse" : node.status === "active" ? "bg-green-400" : node.status === "paused" ? "bg-yellow-400" : node.status === "error" ? "bg-red-400" : "bg-neutral-400"; const budgetPct = agent && agent.budgetMonthlyCents > 0 ? Math.round((agent.spentMonthlyCents / agent.budgetMonthlyCents) * 100) : 0; return (
{node.reports && node.reports.length > 0 && (
{node.reports.map((child) => ( ))}
)}
); }