import { useMemo, useState } from "react"; import { Link, NavLink, useLocation } from "@/lib/router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ChevronRight, MoreHorizontal, PauseCircle, Pencil, PlayCircle, Plus, } from "lucide-react"; import { useCompany } from "../context/CompanyContext"; import { useDialogActions } from "../context/DialogContext"; import { useSidebar } from "../context/SidebarContext"; import { useToastActions } from "../context/ToastContext"; import { agentsApi } from "../api/agents"; import { authApi } from "../api/auth"; import { heartbeatsApi } from "../api/heartbeats"; import { SIDEBAR_SCROLL_RESET_STATE } from "../lib/navigation-scroll"; import { queryKeys } from "../lib/queryKeys"; import { cn, agentRouteRef, agentUrl } from "../lib/utils"; import { useAgentOrder } from "../hooks/useAgentOrder"; import { AgentIcon } from "./AgentIconPicker"; import { BudgetSidebarMarker } from "./BudgetSidebarMarker"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import type { Agent } from "@paperclipai/shared"; function SidebarAgentItem({ activeAgentId, activeTab, agent, disabled, isMobile, onPauseResume, runCount, setSidebarOpen, }: { activeAgentId: string | null; activeTab: string | null; agent: Agent; disabled: boolean; isMobile: boolean; onPauseResume: (agent: Agent, action: "pause" | "resume") => void; runCount: number; setSidebarOpen: (open: boolean) => void; }) { const routeRef = agentRouteRef(agent); const href = activeTab ? `${agentUrl(agent)}/${activeTab}` : agentUrl(agent); const editHref = `${agentUrl(agent)}/configuration`; const isActive = activeAgentId === routeRef; const isPaused = agent.status === "paused"; const isBudgetPaused = isPaused && agent.pauseReason === "budget"; const pauseResumeLabel = isPaused ? "Resume agent" : "Pause agent"; const pauseResumeDisabled = disabled || agent.status === "pending_approval" || isBudgetPaused; const pauseResumeDisabledLabel = disabled ? "Updating..." : isBudgetPaused ? "Budget paused" : pauseResumeLabel; return (
{ if (isMobile) setSidebarOpen(false); }} className={cn( "flex min-w-0 flex-1 items-center gap-2.5 px-3 py-1.5 pr-8 text-[13px] font-medium transition-colors", isActive ? "bg-accent text-foreground" : "text-foreground/80 hover:bg-accent/50 hover:text-foreground" )} > {agent.name} {(agent.pauseReason === "budget" || runCount > 0) && ( {agent.pauseReason === "budget" ? ( ) : null} {runCount > 0 ? ( ) : null} {runCount > 0 ? ( {runCount} live ) : null} )} { if (isMobile) setSidebarOpen(false); }} > Edit agent { if (pauseResumeDisabled) return; onPauseResume(agent, isPaused ? "resume" : "pause"); }} disabled={pauseResumeDisabled} title={isBudgetPaused ? "Agent was paused by budget limits" : undefined} > {isPaused ? : } {pauseResumeDisabledLabel}
); } export function SidebarAgents() { const [open, setOpen] = useState(true); const [pendingAgentIds, setPendingAgentIds] = useState>(() => new Set()); const queryClient = useQueryClient(); const { selectedCompanyId } = useCompany(); const { openNewAgent } = useDialogActions(); const { isMobile, setSidebarOpen } = useSidebar(); const { pushToast } = useToastActions(); const location = useLocation(); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: session } = useQuery({ queryKey: queryKeys.auth.session, queryFn: () => authApi.getSession(), }); 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 filtered; }, [agents]); const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; const { orderedAgents } = useAgentOrder({ agents: visibleAgents, companyId: selectedCompanyId, userId: currentUserId, }); const agentMatch = location.pathname.match(/^\/(?:[^/]+\/)?agents\/([^/]+)(?:\/([^/]+))?/); const activeAgentId = agentMatch?.[1] ?? null; const activeTab = agentMatch?.[2] ?? null; const pauseResumeAgent = useMutation({ mutationFn: ({ agent, action }: { agent: Agent; action: "pause" | "resume" }) => action === "pause" ? agentsApi.pause(agent.id, selectedCompanyId ?? undefined) : agentsApi.resume(agent.id, selectedCompanyId ?? undefined), onMutate: ({ agent }) => { setPendingAgentIds((current) => { const next = new Set(current); next.add(agent.id); return next; }); }, onSuccess: async (_agent, { agent, action }) => { if (selectedCompanyId) { await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(selectedCompanyId) }), queryClient.invalidateQueries({ queryKey: queryKeys.liveRuns(selectedCompanyId) }), queryClient.invalidateQueries({ queryKey: queryKeys.dashboard(selectedCompanyId) }), ]); } await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }), queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentRouteRef(agent)) }), ]); pushToast({ title: action === "pause" ? "Agent paused" : "Agent resumed", body: agent.name, tone: "success", }); }, onError: (error, { agent, action }) => { pushToast({ title: action === "pause" ? "Could not pause agent" : "Could not resume agent", body: error instanceof Error ? error.message : agent.name, tone: "error", }); }, onSettled: (_data, _error, { agent }) => { setPendingAgentIds((current) => { const next = new Set(current); next.delete(agent.id); return next; }); }, }); return (
Agents
{orderedAgents.map((agent: Agent) => { const runCount = liveCountByAgent.get(agent.id) ?? 0; return ( pauseResumeAgent.mutate({ agent: targetAgent, action })} runCount={runCount} setSidebarOpen={setSidebarOpen} /> ); })}
); }