import { useEffect, useState, useRef } from "react"; import { useParams, useNavigate, Link } 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, relativeTime, formatTokens } from "../lib/utils"; import { cn } from "../lib/utils"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { MoreHorizontal, Play, Pause, ChevronDown, ChevronRight, CheckCircle2, XCircle, Clock, Timer, Loader2, Slash, RotateCcw, Trash2, } from "lucide-react"; import type { Agent, HeartbeatRun, HeartbeatRunEvent, AgentRuntimeState } from "@paperclip/shared"; const adapterLabels: Record = { claude_local: "Claude (local)", codex_local: "Codex (local)", process: "Process", http: "HTTP", }; const roleLabels: Record = { ceo: "CEO", cto: "CTO", cmo: "CMO", cfo: "CFO", engineer: "Engineer", designer: "Designer", pm: "PM", qa: "QA", devops: "DevOps", researcher: "Researcher", general: "General", }; const runStatusIcons: Record = { succeeded: { icon: CheckCircle2, color: "text-green-400" }, failed: { icon: XCircle, color: "text-red-400" }, running: { icon: Loader2, color: "text-cyan-400" }, queued: { icon: Clock, color: "text-yellow-400" }, timed_out: { icon: Timer, color: "text-orange-400" }, cancelled: { icon: Slash, color: "text-neutral-400" }, }; const sourceLabels: Record = { timer: "Timer", assignment: "Assignment", on_demand: "On-demand", automation: "Automation", }; export function AgentDetail() { const { agentId } = useParams<{ agentId: string }>(); const { selectedCompanyId } = useCompany(); const { openPanel, closePanel } = usePanel(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const navigate = useNavigate(); const [actionError, setActionError] = useState(null); const [moreOpen, setMoreOpen] = useState(false); const { data: agent, isLoading, error } = useQuery({ queryKey: queryKeys.agents.detail(agentId!), queryFn: () => agentsApi.get(agentId!), enabled: !!agentId, }); const { data: runtimeState } = useQuery({ queryKey: queryKeys.agents.runtimeState(agentId!), queryFn: () => agentsApi.runtimeState(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 { data: allAgents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const assignedIssues = (allIssues ?? []).filter((i) => i.assigneeAgentId === agentId); const reportsToAgent = (allAgents ?? []).find((a) => a.id === agent?.reportsTo); const directReports = (allAgents ?? []).filter((a) => a.reportsTo === agentId); const agentAction = useMutation({ mutationFn: async (action: "invoke" | "pause" | "resume" | "terminate" | "resetSession") => { if (!agentId) return Promise.reject(new Error("No agent ID")); switch (action) { case "invoke": return agentsApi.invoke(agentId); case "pause": return agentsApi.pause(agentId); case "resume": return agentsApi.resume(agentId); case "terminate": return agentsApi.terminate(agentId); case "resetSession": return agentsApi.resetSession(agentId); } }, onSuccess: () => { setActionError(null); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId!) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(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, runtimeState]); // eslint-disable-line react-hooks/exhaustive-deps if (isLoading) return

Loading...

; if (error) return

{error.message}

; if (!agent) return null; return (
{/* Header */}

{agent.name}

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

{agent.status === "active" || agent.status === "running" ? ( ) : ( )} {/* Overflow menu */}
{actionError &&

{actionError}

} Overview Configuration Runs{heartbeats ? ` (${heartbeats.length})` : ""} Issues ({assignedIssues.length}) Costs {/* OVERVIEW TAB */}
{/* Summary card */}

Summary

{adapterLabels[agent.adapterType] ?? agent.adapterType} {String((agent.adapterConfig as Record)?.model ?? "") !== "" && ( ({String((agent.adapterConfig as Record).model)}) )} {(agent.runtimeConfig as Record)?.heartbeat ? (() => { const hb = (agent.runtimeConfig as Record).heartbeat as Record; if (!hb.enabled) return Disabled; const sec = Number(hb.intervalSec) || 300; return Every {sec >= 60 ? `${Math.round(sec / 60)} min` : `${sec}s`}; })() : Not configured } {agent.lastHeartbeatAt ? {relativeTime(agent.lastHeartbeatAt)} : Never } {runtimeState?.sessionId ? {runtimeState.sessionId.slice(0, 16)}... : No session } {runtimeState && ( {formatCents(runtimeState.totalCostCents)} )}
{/* Org card */}

Organization

{reportsToAgent ? ( {reportsToAgent.name} ) : ( Nobody (top-level) )} {directReports.length > 0 && (
Direct reports
{directReports.map((r) => ( {r.name} ({roleLabels[r.role] ?? r.role}) ))}
)} {agent.capabilities && (
Capabilities

{agent.capabilities}

)}
{/* CONFIGURATION TAB */} {/* RUNS TAB */} {/* ISSUES TAB */} {assignedIssues.length === 0 ? (

No assigned issues.

) : (
{assignedIssues.map((issue) => ( navigate(`/issues/${issue.id}`)} trailing={} /> ))}
)}
{/* COSTS TAB */}
); } /* ---- Helper components ---- */ function SummaryRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } /* ---- Configuration Tab ---- */ function ConfigurationTab({ agent }: { agent: Agent }) { const queryClient = useQueryClient(); const updateAgent = useMutation({ mutationFn: (data: Record) => agentsApi.update(agent.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }); }, }); const config = (agent.adapterConfig ?? {}) as Record; const heartbeat = ((agent.runtimeConfig ?? {}) as Record).heartbeat as Record | undefined; return (
{/* Identity */} updateAgent.mutate({ name: v })} /> updateAgent.mutate({ title: v || null })} /> updateAgent.mutate({ capabilities: v || null })} /> {/* Adapter Config */} updateAgent.mutate({ adapterConfig: { ...config, cwd: v || undefined } })} /> updateAgent.mutate({ adapterConfig: { ...config, promptTemplate: v || undefined } })} /> updateAgent.mutate({ adapterConfig: { ...config, bootstrapPromptTemplate: v || undefined } })} /> updateAgent.mutate({ adapterConfig: { ...config, model: v || undefined } })} /> updateAgent.mutate({ adapterConfig: { ...config, timeoutSec: Number(v) || 900 } })} /> {agent.adapterType === "claude_local" && ( <> updateAgent.mutate({ adapterConfig: { ...config, maxTurnsPerRun: Number(v) || 80 } })} /> updateAgent.mutate({ adapterConfig: { ...config, dangerouslySkipPermissions: v } })} /> )} {agent.adapterType === "codex_local" && ( <> updateAgent.mutate({ adapterConfig: { ...config, search: v } })} /> updateAgent.mutate({ adapterConfig: { ...config, dangerouslyBypassApprovalsAndSandbox: v } })} /> )} {/* Heartbeat Policy */} updateAgent.mutate({ runtimeConfig: { ...agent.runtimeConfig as object, heartbeat: { ...heartbeat, enabled: v } } })} /> updateAgent.mutate({ runtimeConfig: { ...agent.runtimeConfig as object, heartbeat: { ...heartbeat, intervalSec: Number(v) || 300 } } })} /> updateAgent.mutate({ runtimeConfig: { ...agent.runtimeConfig as object, heartbeat: { ...heartbeat, wakeOnAssignment: v } } })} /> updateAgent.mutate({ runtimeConfig: { ...agent.runtimeConfig as object, heartbeat: { ...heartbeat, wakeOnOnDemand: v } } })} /> updateAgent.mutate({ runtimeConfig: { ...agent.runtimeConfig as object, heartbeat: { ...heartbeat, wakeOnAutomation: v } } })} /> updateAgent.mutate({ runtimeConfig: { ...agent.runtimeConfig as object, heartbeat: { ...heartbeat, cooldownSec: Number(v) || 10 } } })} /> {/* Runtime */} updateAgent.mutate({ budgetMonthlyCents: Number(v) || 0 })} />
); } function ConfigSection({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } function ConfigField({ label, value, mono, multiline, readOnly, onSave, }: { label: string; value: string; mono?: boolean; multiline?: boolean; readOnly?: boolean; onSave?: (value: string) => void; }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(value); const inputRef = useRef(null); useEffect(() => { setDraft(value); }, [value]); function handleSave() { if (draft !== value && onSave) { onSave(draft); } setEditing(false); } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === "Enter" && !multiline) { e.preventDefault(); handleSave(); } if (e.key === "Escape") { setDraft(value); setEditing(false); } } return (
{label}
{editing && !readOnly ? ( multiline ? (