import { useState, useEffect, useMemo } from "react"; import { Link, useNavigate, useLocation } from "@/lib/router"; import { useQuery } from "@tanstack/react-query"; import { agentsApi, type OrgNode } from "../api/agents"; import { heartbeatsApi } from "../api/heartbeats"; import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useSidebar } from "../context/SidebarContext"; import { queryKeys } from "../lib/queryKeys"; import { StatusBadge } from "../components/StatusBadge"; import { agentStatusDot, agentStatusDotDefault } from "../lib/status-colors"; import { EntityRow } from "../components/EntityRow"; import { EmptyState } from "../components/EmptyState"; import { PageSkeleton } from "../components/PageSkeleton"; import { relativeTime, cn, agentRouteRef, agentUrl } from "../lib/utils"; import { PageTabBar } from "../components/PageTabBar"; import { Tabs } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Bot, Plus, List, GitBranch, SlidersHorizontal } from "lucide-react"; import { AGENT_ROLE_LABELS, type Agent } from "@paperclipai/shared"; const adapterLabels: Record = { claude_local: "Claude", codex_local: "Codex", gemini_local: "Gemini", opencode_local: "OpenCode", cursor: "Cursor", openclaw_gateway: "OpenClaw Gateway", process: "Process", http: "HTTP", }; const roleLabels = AGENT_ROLE_LABELS as Record; type FilterTab = "all" | "active" | "paused" | "error"; function matchesFilter(status: string, tab: FilterTab, showTerminated: boolean): boolean { if (status === "terminated") return showTerminated; 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"; return true; } function filterAgents(agents: Agent[], tab: FilterTab, showTerminated: boolean): Agent[] { return agents.filter((a) => matchesFilter(a.status, tab, showTerminated)); } function filterOrgTree(nodes: OrgNode[], tab: FilterTab, showTerminated: boolean): OrgNode[] { return nodes.reduce((acc, node) => { const filteredReports = filterOrgTree(node.reports, tab, showTerminated); if (matchesFilter(node.status, tab, showTerminated) || 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 location = useLocation(); const { isMobile } = useSidebar(); const pathSegment = location.pathname.split("/").pop() ?? "all"; const tab: FilterTab = (pathSegment === "all" || pathSegment === "active" || pathSegment === "paused" || pathSegment === "error") ? pathSegment : "all"; const [view, setView] = useState<"list" | "org">("org"); const forceListView = isMobile; const effectiveView: "list" | "org" = forceListView ? "list" : view; const [showTerminated, setShowTerminated] = useState(false); const [filtersOpen, setFiltersOpen] = useState(false); 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 && effectiveView === "org", }); const { data: runs } = useQuery({ queryKey: queryKeys.heartbeats(selectedCompanyId!), queryFn: () => heartbeatsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, refetchInterval: 15_000, }); // Map agentId -> first live run + live run count const liveRunByAgent = useMemo(() => { const map = new Map(); for (const r of runs ?? []) { if (r.status !== "running" && r.status !== "queued") continue; const existing = map.get(r.agentId); if (existing) { existing.liveCount += 1; continue; } map.set(r.agentId, { runId: r.id, liveCount: 1 }); } return map; }, [runs]); 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 ; } if (isLoading) { return ; } const filtered = filterAgents(agents ?? [], tab, showTerminated); const filteredOrg = filterOrgTree(orgTree ?? [], tab, showTerminated); return (
navigate(`/agents/${v}`)}> navigate(`/agents/${v}`)} />
{/* Filters */}
{filtersOpen && (
)}
{/* View toggle */} {!forceListView && (
)}
{filtered.length > 0 && (

{filtered.length} agent{filtered.length !== 1 ? "s" : ""}

)} {error &&

{error.message}

} {agents && agents.length === 0 && ( )} {/* List view */} {effectiveView === "list" && filtered.length > 0 && (
{filtered.map((agent) => { return ( } trailing={
{liveRunByAgent.has(agent.id) ? ( ) : ( )}
{liveRunByAgent.has(agent.id) && ( )} {adapterLabels[agent.adapterType] ?? agent.adapterType} {agent.lastHeartbeatAt ? relativeTime(agent.lastHeartbeatAt) : "—"}
} /> ); })}
)} {effectiveView === "list" && agents && agents.length > 0 && filtered.length === 0 && (

No agents match the selected filter.

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

No agents match the selected filter.

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

No organizational hierarchy defined.

)}
); } function OrgTreeNode({ node, depth, agentMap, liveRunByAgent, }: { node: OrgNode; depth: number; agentMap: Map; liveRunByAgent: Map; }) { const agent = agentMap.get(node.id); const statusColor = agentStatusDot[node.status] ?? agentStatusDotDefault; return (
{node.name} {roleLabels[node.role] ?? node.role} {agent?.title ? ` - ${agent.title}` : ""}
{liveRunByAgent.has(node.id) ? ( ) : ( )}
{liveRunByAgent.has(node.id) && ( )} {agent && ( <> {adapterLabels[agent.adapterType] ?? agent.adapterType} {agent.lastHeartbeatAt ? relativeTime(agent.lastHeartbeatAt) : "—"} )}
{node.reports && node.reports.length > 0 && (
{node.reports.map((child) => ( ))}
)}
); } function LiveRunIndicator({ agentRef, runId, liveCount, }: { agentRef: string; runId: string; liveCount: number; }) { return ( e.stopPropagation()} > Live{liveCount > 1 ? ` (${liveCount})` : ""} ); }