import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { agentsApi } from "../api/agents"; import { heartbeatsApi } from "../api/heartbeats"; import { issuesApi } from "../api/issues"; import { usePanel } from "../context/PanelContext"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { AgentProperties } from "../components/AgentProperties"; import { StatusBadge } from "../components/StatusBadge"; import { EntityRow } from "../components/EntityRow"; import { formatCents, formatDate } from "../lib/utils"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import type { Issue, HeartbeatRun } from "@paperclip/shared"; export function AgentDetail() { const { agentId } = useParams<{ agentId: string }>(); const { selectedCompanyId } = useCompany(); const { openPanel, closePanel } = usePanel(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const [actionError, setActionError] = useState(null); const { data: agent, isLoading, error } = useQuery({ queryKey: queryKeys.agents.detail(agentId!), queryFn: () => agentsApi.get(agentId!), enabled: !!agentId, }); const { data: heartbeats } = useQuery({ queryKey: queryKeys.heartbeats(selectedCompanyId!, agentId), queryFn: () => heartbeatsApi.list(selectedCompanyId!, agentId), enabled: !!selectedCompanyId && !!agentId, }); const { data: allIssues } = useQuery({ queryKey: queryKeys.issues.list(selectedCompanyId!), queryFn: () => issuesApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const assignedIssues = (allIssues ?? []).filter((i) => i.assigneeAgentId === agentId); const agentAction = useMutation({ mutationFn: async (action: "invoke" | "pause" | "resume") => { if (!agentId) return Promise.reject(new Error("No agent ID")); if (action === "invoke") { await agentsApi.invoke(agentId); return; } if (action === "pause") { await agentsApi.pause(agentId); return; } await agentsApi.resume(agentId); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId!) }); if (selectedCompanyId) { queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(selectedCompanyId) }); } }, onError: (err) => { setActionError(err instanceof Error ? err.message : "Action failed"); }, }); useEffect(() => { setBreadcrumbs([ { label: "Agents", href: "/agents" }, { label: agent?.name ?? agentId ?? "Agent" }, ]); }, [setBreadcrumbs, agent, agentId]); useEffect(() => { if (agent) { openPanel(); } return () => closePanel(); }, [agent]); // eslint-disable-line react-hooks/exhaustive-deps if (isLoading) return

Loading...

; if (error) return

{error.message}

; if (!agent) return null; const budgetPct = agent.budgetMonthlyCents > 0 ? Math.round((agent.spentMonthlyCents / agent.budgetMonthlyCents) * 100) : 0; return (

{agent.name}

{agent.role}{agent.title ? ` - ${agent.title}` : ""}

{agent.status === "active" ? ( ) : ( )}
{actionError &&

{actionError}

} Overview Heartbeats Issues ({assignedIssues.length}) Costs
Adapter

{agent.adapterType}

Context Mode

{agent.contextMode}

{agent.reportsTo && (
Reports To

{agent.reportsTo}

)} {agent.capabilities && (
Capabilities

{agent.capabilities}

)} {agent.lastHeartbeatAt && (
Last Heartbeat

{formatDate(agent.lastHeartbeatAt)}

)}
{(!heartbeats || heartbeats.length === 0) ? (

No heartbeat runs.

) : (
{heartbeats.map((run) => ( {formatDate(run.createdAt)}
} /> ))}
)} {assignedIssues.length === 0 ? (

No assigned issues.

) : (
{assignedIssues.map((issue) => ( } /> ))}
)}
Monthly Budget {formatCents(agent.spentMonthlyCents)} / {formatCents(agent.budgetMonthlyCents)}
90 ? "bg-red-400" : budgetPct > 70 ? "bg-yellow-400" : "bg-green-400" }`} style={{ width: `${Math.min(100, budgetPct)}%` }} />

{budgetPct}% utilized

); }