import { useCallback, useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import { agentsApi } from "../api/agents"; import { heartbeatsApi } from "../api/heartbeats"; import { issuesApi } from "../api/issues"; import { useApi } from "../hooks/useApi"; import { usePanel } from "../context/PanelContext"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; 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 [actionError, setActionError] = useState(null); const agentFetcher = useCallback(() => { if (!agentId) return Promise.reject(new Error("No agent ID")); return agentsApi.get(agentId); }, [agentId]); const heartbeatsFetcher = useCallback(() => { if (!selectedCompanyId || !agentId) return Promise.resolve([] as HeartbeatRun[]); return heartbeatsApi.list(selectedCompanyId, agentId); }, [selectedCompanyId, agentId]); const issuesFetcher = useCallback(() => { if (!selectedCompanyId) return Promise.resolve([] as Issue[]); return issuesApi.list(selectedCompanyId); }, [selectedCompanyId]); const { data: agent, loading, error, reload: reloadAgent } = useApi(agentFetcher); const { data: heartbeats } = useApi(heartbeatsFetcher); const { data: allIssues } = useApi(issuesFetcher); const assignedIssues = (allIssues ?? []).filter((i) => i.assigneeAgentId === agentId); 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 async function handleAction(action: "invoke" | "pause" | "resume") { if (!agentId) return; setActionError(null); try { if (action === "invoke") await agentsApi.invoke(agentId); else if (action === "pause") await agentsApi.pause(agentId); else await agentsApi.resume(agentId); reloadAgent(); } catch (err) { setActionError(err instanceof Error ? err.message : `Failed to ${action} agent`); } } if (loading) 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

); }