import { useMemo, useState } from "react"; import { NavLink, useLocation } from "@/lib/router"; import { useQuery } from "@tanstack/react-query"; import { ChevronRight, Plus } from "lucide-react"; import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { useSidebar } from "../context/SidebarContext"; import { agentsApi } from "../api/agents"; import { heartbeatsApi } from "../api/heartbeats"; import { queryKeys } from "../lib/queryKeys"; import { cn, agentRouteRef, agentUrl } from "../lib/utils"; import { AgentIcon } from "./AgentIconPicker"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import type { Agent } from "@paperclipai/shared"; /** BFS sort: roots first (no reportsTo), then their direct reports, etc. */ function sortByHierarchy(agents: Agent[]): Agent[] { const byId = new Map(agents.map((a) => [a.id, a])); const childrenOf = new Map(); for (const a of agents) { const parent = a.reportsTo && byId.has(a.reportsTo) ? a.reportsTo : null; const list = childrenOf.get(parent) ?? []; list.push(a); childrenOf.set(parent, list); } const sorted: Agent[] = []; const queue = childrenOf.get(null) ?? []; while (queue.length > 0) { const agent = queue.shift()!; sorted.push(agent); const children = childrenOf.get(agent.id); if (children) queue.push(...children); } return sorted; } export function SidebarAgents() { const [open, setOpen] = useState(true); const { selectedCompanyId } = useCompany(); const { openNewAgent } = useDialog(); const { isMobile, setSidebarOpen } = useSidebar(); const location = useLocation(); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: liveRuns } = useQuery({ queryKey: queryKeys.liveRuns(selectedCompanyId!), queryFn: () => heartbeatsApi.liveRunsForCompany(selectedCompanyId!), enabled: !!selectedCompanyId, refetchInterval: 10_000, }); const liveCountByAgent = useMemo(() => { const counts = new Map(); for (const run of liveRuns ?? []) { counts.set(run.agentId, (counts.get(run.agentId) ?? 0) + 1); } return counts; }, [liveRuns]); const visibleAgents = useMemo(() => { const filtered = (agents ?? []).filter( (a: Agent) => a.status !== "terminated" ); return sortByHierarchy(filtered); }, [agents]); const agentMatch = location.pathname.match(/^\/(?:[^/]+\/)?agents\/([^/]+)/); const activeAgentId = agentMatch?.[1] ?? null; return (
Agents
{visibleAgents.map((agent: Agent) => { const runCount = liveCountByAgent.get(agent.id) ?? 0; return ( { if (isMobile) setSidebarOpen(false); }} className={cn( "flex items-center gap-2.5 px-3 py-1.5 text-[13px] font-medium transition-colors", activeAgentId === agentRouteRef(agent) ? "bg-accent text-foreground" : "text-foreground/80 hover:bg-accent/50 hover:text-foreground" )} > {agent.name} {runCount > 0 && ( {runCount} live )} ); })}
); }