import { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { useParams, useNavigate, Link, useBeforeUnload } from "@/lib/router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { agentsApi, type AgentKey, type ClaudeLoginResult } from "../api/agents"; import { heartbeatsApi } from "../api/heartbeats"; import { ChartCard, RunActivityChart, PriorityChart, IssueStatusChart, SuccessRateChart } from "../components/ActivityCharts"; import { activityApi } from "../api/activity"; import { issuesApi } from "../api/issues"; import { usePanel } from "../context/PanelContext"; import { useSidebar } from "../context/SidebarContext"; import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { AgentConfigForm } from "../components/AgentConfigForm"; import { adapterLabels, roleLabels } from "../components/agent-config-primitives"; import { getUIAdapter, buildTranscript } from "../adapters"; import type { TranscriptEntry } from "../adapters"; import { StatusBadge } from "../components/StatusBadge"; import { agentStatusDot, agentStatusDotDefault } from "../lib/status-colors"; import { MarkdownBody } from "../components/MarkdownBody"; import { CopyText } from "../components/CopyText"; import { EntityRow } from "../components/EntityRow"; import { Identity } from "../components/Identity"; import { PageSkeleton } from "../components/PageSkeleton"; import { formatCents, formatDate, relativeTime, formatTokens } from "../lib/utils"; import { cn } from "../lib/utils"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { MoreHorizontal, Play, Pause, CheckCircle2, XCircle, Clock, Timer, Loader2, Slash, RotateCcw, Trash2, Plus, Key, Eye, EyeOff, Copy, ChevronRight, ChevronDown, ArrowLeft, Settings, } from "lucide-react"; import { Input } from "@/components/ui/input"; import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker"; import { isUuidLike, type Agent, type HeartbeatRun, type HeartbeatRunEvent, type AgentRuntimeState } from "@paperclipai/shared"; import { agentRouteRef } from "../lib/utils"; const runStatusIcons: Record = { succeeded: { icon: CheckCircle2, color: "text-green-600 dark:text-green-400" }, failed: { icon: XCircle, color: "text-red-600 dark:text-red-400" }, running: { icon: Loader2, color: "text-cyan-600 dark:text-cyan-400" }, queued: { icon: Clock, color: "text-yellow-600 dark:text-yellow-400" }, timed_out: { icon: Timer, color: "text-orange-600 dark:text-orange-400" }, cancelled: { icon: Slash, color: "text-neutral-500 dark:text-neutral-400" }, }; const REDACTED_ENV_VALUE = "***REDACTED***"; const SECRET_ENV_KEY_RE = /(api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)/i; const JWT_VALUE_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?$/; function shouldRedactSecretValue(key: string, value: unknown): boolean { if (SECRET_ENV_KEY_RE.test(key)) return true; if (typeof value !== "string") return false; return JWT_VALUE_RE.test(value); } function redactEnvValue(key: string, value: unknown): string { if ( typeof value === "object" && value !== null && !Array.isArray(value) && (value as { type?: unknown }).type === "secret_ref" ) { return "***SECRET_REF***"; } if (shouldRedactSecretValue(key, value)) return REDACTED_ENV_VALUE; if (value === null || value === undefined) return ""; if (typeof value === "string") return value; try { return JSON.stringify(value); } catch { return String(value); } } function formatEnvForDisplay(envValue: unknown): string { const env = asRecord(envValue); if (!env) return ""; const keys = Object.keys(env); if (keys.length === 0) return ""; return keys .sort() .map((key) => `${key}=${redactEnvValue(key, env[key])}`) .join("\n"); } const sourceLabels: Record = { timer: "Timer", assignment: "Assignment", on_demand: "On-demand", automation: "Automation", }; const LIVE_SCROLL_BOTTOM_TOLERANCE_PX = 32; type ScrollContainer = Window | HTMLElement; function isWindowContainer(container: ScrollContainer): container is Window { return container === window; } function isElementScrollContainer(element: HTMLElement): boolean { const overflowY = window.getComputedStyle(element).overflowY; return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay"; } function findScrollContainer(anchor: HTMLElement | null): ScrollContainer { let parent = anchor?.parentElement ?? null; while (parent) { if (isElementScrollContainer(parent)) return parent; parent = parent.parentElement; } return window; } function readScrollMetrics(container: ScrollContainer): { scrollHeight: number; distanceFromBottom: number } { if (isWindowContainer(container)) { const pageHeight = Math.max( document.documentElement.scrollHeight, document.body.scrollHeight, ); const viewportBottom = window.scrollY + window.innerHeight; return { scrollHeight: pageHeight, distanceFromBottom: Math.max(0, pageHeight - viewportBottom), }; } const viewportBottom = container.scrollTop + container.clientHeight; return { scrollHeight: container.scrollHeight, distanceFromBottom: Math.max(0, container.scrollHeight - viewportBottom), }; } function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBehavior = "auto") { if (isWindowContainer(container)) { const pageHeight = Math.max( document.documentElement.scrollHeight, document.body.scrollHeight, ); window.scrollTo({ top: pageHeight, behavior }); return; } container.scrollTo({ top: container.scrollHeight, behavior }); } type AgentDetailView = "overview" | "configure" | "runs"; function parseAgentDetailView(value: string | null): AgentDetailView { if (value === "configure" || value === "configuration") return "configure"; if (value === "runs") return value; return "overview"; } function usageNumber(usage: Record | null, ...keys: string[]) { if (!usage) return 0; for (const key of keys) { const value = usage[key]; if (typeof value === "number" && Number.isFinite(value)) return value; } return 0; } function runMetrics(run: HeartbeatRun) { const usage = (run.usageJson ?? null) as Record | null; const result = (run.resultJson ?? null) as Record | null; const input = usageNumber(usage, "inputTokens", "input_tokens"); const output = usageNumber(usage, "outputTokens", "output_tokens"); const cached = usageNumber( usage, "cachedInputTokens", "cached_input_tokens", "cache_read_input_tokens", ); const cost = usageNumber(usage, "costUsd", "cost_usd", "total_cost_usd") || usageNumber(result, "total_cost_usd", "cost_usd", "costUsd"); return { input, output, cached, cost, totalTokens: input + output, }; } type RunLogChunk = { ts: string; stream: "stdout" | "stderr" | "system"; chunk: string }; function asRecord(value: unknown): Record | null { if (typeof value !== "object" || value === null || Array.isArray(value)) return null; return value as Record; } function asNonEmptyString(value: unknown): string | null { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } export function AgentDetail() { const { companyPrefix, agentId, tab: urlTab, runId: urlRunId } = useParams<{ companyPrefix?: string; agentId: string; tab?: string; runId?: string; }>(); const { companies, selectedCompanyId, setSelectedCompanyId } = useCompany(); const { closePanel } = usePanel(); const { openNewIssue } = useDialog(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const navigate = useNavigate(); const [actionError, setActionError] = useState(null); const [moreOpen, setMoreOpen] = useState(false); const activeView = urlRunId ? "runs" as AgentDetailView : parseAgentDetailView(urlTab ?? null); const [configDirty, setConfigDirty] = useState(false); const [configSaving, setConfigSaving] = useState(false); const saveConfigActionRef = useRef<(() => void) | null>(null); const cancelConfigActionRef = useRef<(() => void) | null>(null); const { isMobile } = useSidebar(); const routeAgentRef = agentId ?? ""; const routeCompanyId = useMemo(() => { if (!companyPrefix) return null; const requestedPrefix = companyPrefix.toUpperCase(); return companies.find((company) => company.issuePrefix.toUpperCase() === requestedPrefix)?.id ?? null; }, [companies, companyPrefix]); const lookupCompanyId = routeCompanyId ?? selectedCompanyId ?? undefined; const canFetchAgent = routeAgentRef.length > 0 && (isUuidLike(routeAgentRef) || Boolean(lookupCompanyId)); const setSaveConfigAction = useCallback((fn: (() => void) | null) => { saveConfigActionRef.current = fn; }, []); const setCancelConfigAction = useCallback((fn: (() => void) | null) => { cancelConfigActionRef.current = fn; }, []); const { data: agent, isLoading, error } = useQuery({ queryKey: [...queryKeys.agents.detail(routeAgentRef), lookupCompanyId ?? null], queryFn: () => agentsApi.get(routeAgentRef, lookupCompanyId), enabled: canFetchAgent, }); const resolvedCompanyId = agent?.companyId ?? selectedCompanyId; const canonicalAgentRef = agent ? agentRouteRef(agent) : routeAgentRef; const agentLookupRef = agent?.id ?? routeAgentRef; const resolvedAgentId = agent?.id ?? null; const { data: runtimeState } = useQuery({ queryKey: queryKeys.agents.runtimeState(resolvedAgentId ?? routeAgentRef), queryFn: () => agentsApi.runtimeState(resolvedAgentId!, resolvedCompanyId ?? undefined), enabled: Boolean(resolvedAgentId), }); const { data: heartbeats } = useQuery({ queryKey: queryKeys.heartbeats(resolvedCompanyId!, agent?.id ?? undefined), queryFn: () => heartbeatsApi.list(resolvedCompanyId!, agent?.id ?? undefined), enabled: !!resolvedCompanyId && !!agent?.id, }); const { data: allIssues } = useQuery({ queryKey: queryKeys.issues.list(resolvedCompanyId!), queryFn: () => issuesApi.list(resolvedCompanyId!), enabled: !!resolvedCompanyId, }); const { data: allAgents } = useQuery({ queryKey: queryKeys.agents.list(resolvedCompanyId!), queryFn: () => agentsApi.list(resolvedCompanyId!), enabled: !!resolvedCompanyId, }); const assignedIssues = (allIssues ?? []) .filter((i) => i.assigneeAgentId === agent?.id) .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); const reportsToAgent = (allAgents ?? []).find((a) => a.id === agent?.reportsTo); const directReports = (allAgents ?? []).filter((a) => a.reportsTo === agent?.id && a.status !== "terminated"); const mobileLiveRun = useMemo( () => (heartbeats ?? []).find((r) => r.status === "running" || r.status === "queued") ?? null, [heartbeats], ); useEffect(() => { if (!agent) return; if (routeAgentRef === canonicalAgentRef) return; if (urlRunId) { navigate(`/agents/${canonicalAgentRef}/runs/${urlRunId}`, { replace: true }); return; } if (urlTab) { navigate(`/agents/${canonicalAgentRef}/${urlTab}`, { replace: true }); return; } navigate(`/agents/${canonicalAgentRef}`, { replace: true }); }, [agent, routeAgentRef, canonicalAgentRef, urlRunId, urlTab, navigate]); useEffect(() => { if (!agent?.companyId || agent.companyId === selectedCompanyId) return; setSelectedCompanyId(agent.companyId, { source: "route_sync" }); }, [agent?.companyId, selectedCompanyId, setSelectedCompanyId]); const agentAction = useMutation({ mutationFn: async (action: "invoke" | "pause" | "resume" | "terminate") => { if (!agentLookupRef) return Promise.reject(new Error("No agent reference")); switch (action) { case "invoke": return agentsApi.invoke(agentLookupRef, resolvedCompanyId ?? undefined); case "pause": return agentsApi.pause(agentLookupRef, resolvedCompanyId ?? undefined); case "resume": return agentsApi.resume(agentLookupRef, resolvedCompanyId ?? undefined); case "terminate": return agentsApi.terminate(agentLookupRef, resolvedCompanyId ?? undefined); } }, onSuccess: (data, action) => { setActionError(null); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentLookupRef) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(agentLookupRef) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(agentLookupRef) }); if (resolvedCompanyId) { queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) }); if (agent?.id) { queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(resolvedCompanyId, agent.id) }); } } if (action === "invoke" && data && typeof data === "object" && "id" in data) { navigate(`/agents/${canonicalAgentRef}/runs/${(data as HeartbeatRun).id}`); } }, onError: (err) => { setActionError(err instanceof Error ? err.message : "Action failed"); }, }); const updateIcon = useMutation({ mutationFn: (icon: string) => agentsApi.update(agentLookupRef, { icon }, resolvedCompanyId ?? undefined), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentLookupRef) }); if (resolvedCompanyId) { queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) }); } }, }); const resetTaskSession = useMutation({ mutationFn: (taskKey: string | null) => agentsApi.resetSession(agentLookupRef, taskKey, resolvedCompanyId ?? undefined), onSuccess: () => { setActionError(null); queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(agentLookupRef) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(agentLookupRef) }); }, onError: (err) => { setActionError(err instanceof Error ? err.message : "Failed to reset session"); }, }); const updatePermissions = useMutation({ mutationFn: (canCreateAgents: boolean) => agentsApi.updatePermissions(agentLookupRef, { canCreateAgents }, resolvedCompanyId ?? undefined), onSuccess: () => { setActionError(null); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentLookupRef) }); if (resolvedCompanyId) { queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) }); } }, onError: (err) => { setActionError(err instanceof Error ? err.message : "Failed to update permissions"); }, }); useEffect(() => { const crumbs: { label: string; href?: string }[] = [ { label: "Agents", href: "/agents" }, ]; const agentName = agent?.name ?? routeAgentRef ?? "Agent"; if (activeView === "overview" && !urlRunId) { crumbs.push({ label: agentName }); } else { crumbs.push({ label: agentName, href: `/agents/${canonicalAgentRef}` }); if (urlRunId) { crumbs.push({ label: "Runs", href: `/agents/${canonicalAgentRef}/runs` }); crumbs.push({ label: `Run ${urlRunId.slice(0, 8)}` }); } else if (activeView === "configure") { crumbs.push({ label: "Configure" }); } else if (activeView === "runs") { crumbs.push({ label: "Runs" }); } } setBreadcrumbs(crumbs); }, [setBreadcrumbs, agent, routeAgentRef, canonicalAgentRef, activeView, urlRunId]); useEffect(() => { closePanel(); return () => closePanel(); }, []); // eslint-disable-line react-hooks/exhaustive-deps useBeforeUnload( useCallback((event) => { if (!configDirty) return; event.preventDefault(); event.returnValue = ""; }, [configDirty]), ); if (isLoading) return ; if (error) return

{error.message}

; if (!agent) return null; const isPendingApproval = agent.status === "pending_approval"; const showConfigActionBar = activeView === "configure" && configDirty; return (
{/* Header */}
updateIcon.mutate(icon)} >

{agent.name}

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

{agent.status === "paused" ? ( ) : ( )} {mobileLiveRun && ( Live )} {/* Overflow menu */}
{actionError &&

{actionError}

} {isPendingApproval && (

This agent is pending board approval and cannot be invoked yet.

)} {/* Floating Save/Cancel (desktop) */} {!isMobile && (
)} {/* Mobile bottom Save/Cancel bar */} {isMobile && showConfigActionBar && (
)} {/* View content */} {activeView === "overview" && ( )} {activeView === "configure" && ( )} {activeView === "runs" && ( )}
); } /* ---- Helper components ---- */ function SummaryRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: string }) { if (runs.length === 0) return null; const sorted = [...runs].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); const liveRun = sorted.find((r) => r.status === "running" || r.status === "queued"); const run = liveRun ?? sorted[0]; const isLive = run.status === "running" || run.status === "queued"; const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" }; const StatusIcon = statusInfo.icon; const summary = run.resultJson ? String((run.resultJson as Record).summary ?? (run.resultJson as Record).result ?? "") : run.error ?? ""; return (

{isLive && ( )} {isLive ? "Live Run" : "Latest Run"}

View details →
{run.id.slice(0, 8)} {sourceLabels[run.invocationSource] ?? run.invocationSource} {relativeTime(run.createdAt)}
{summary && (
{summary}
)}
); } /* ---- Agent Overview (main single-page view) ---- */ function AgentOverview({ agent, runs, assignedIssues, runtimeState, reportsToAgent, directReports, agentId, agentRouteId, }: { agent: Agent; runs: HeartbeatRun[]; assignedIssues: { id: string; title: string; status: string; priority: string; identifier?: string | null; createdAt: Date }[]; runtimeState?: AgentRuntimeState; reportsToAgent: Agent | null; directReports: Agent[]; agentId: string; agentRouteId: string; }) { return (
{/* Latest Run */} {/* Charts */}
{/* Recent Issues */}

Recent Issues

See All →
{assignedIssues.length === 0 ? (

No assigned issues.

) : (
{assignedIssues.slice(0, 10).map((issue) => ( } /> ))} {assignedIssues.length > 10 && (
+{assignedIssues.length - 10} more issues
)}
)}
{/* Costs */}

Costs

{/* Configuration Summary */}
); } /* Chart components imported from ../components/ActivityCharts */ /* ---- Configuration Summary ---- */ function ConfigSummary({ agent, agentRouteId, reportsToAgent, directReports, }: { agent: Agent; agentRouteId: string; reportsToAgent: Agent | null; directReports: Agent[]; }) { const config = agent.adapterConfig as Record; const promptText = typeof config?.promptTemplate === "string" ? config.promptTemplate : ""; return (

Configuration

Manage →

Agent Details

{adapterLabels[agent.adapterType] ?? agent.adapterType} {String(config?.model ?? "") !== "" && ( ({String(config.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; const maxConcurrentRuns = Math.max(1, Math.floor(Number(hb.maxConcurrentRuns) || 1)); const intervalLabel = sec >= 60 ? `${Math.round(sec / 60)} min` : `${sec}s`; return ( Every {intervalLabel} {maxConcurrentRuns > 1 ? ` (max ${maxConcurrentRuns} concurrent)` : ""} ); })() : Not configured } {agent.lastHeartbeatAt ? {relativeTime(agent.lastHeartbeatAt)} : Never } {reportsToAgent ? ( ) : ( Nobody (top-level) )}
{directReports.length > 0 && (
Direct reports
{directReports.map((r) => ( {r.name} ({roleLabels[r.role] ?? r.role}) ))}
)} {agent.capabilities && (
Capabilities

{agent.capabilities}

)}
{promptText && (

Prompt Template

{promptText}
)}
); } /* ---- Costs Section (inline) ---- */ function CostsSection({ runtimeState, runs, }: { runtimeState?: AgentRuntimeState; runs: HeartbeatRun[]; }) { const runsWithCost = runs .filter((r) => { const u = r.usageJson as Record | null; return u && (u.cost_usd || u.total_cost_usd || u.input_tokens); }) .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); return (
{runtimeState && (
Input tokens {formatTokens(runtimeState.totalInputTokens)}
Output tokens {formatTokens(runtimeState.totalOutputTokens)}
Cached tokens {formatTokens(runtimeState.totalCachedInputTokens)}
Total cost {formatCents(runtimeState.totalCostCents)}
)} {runsWithCost.length > 0 && (
{runsWithCost.slice(0, 10).map((run) => { const u = run.usageJson as Record; return ( ); })}
Date Run Input Output Cost
{formatDate(run.createdAt)} {run.id.slice(0, 8)} {formatTokens(Number(u.input_tokens ?? 0))} {formatTokens(Number(u.output_tokens ?? 0))} {(u.cost_usd || u.total_cost_usd) ? `$${Number(u.cost_usd ?? u.total_cost_usd ?? 0).toFixed(4)}` : "-" }
)}
); } /* ---- Agent Configure Page ---- */ function AgentConfigurePage({ agent, agentId, companyId, onDirtyChange, onSaveActionChange, onCancelActionChange, onSavingChange, updatePermissions, }: { agent: Agent; agentId: string; companyId?: string; onDirtyChange: (dirty: boolean) => void; onSaveActionChange: (save: (() => void) | null) => void; onCancelActionChange: (cancel: (() => void) | null) => void; onSavingChange: (saving: boolean) => void; updatePermissions: { mutate: (canCreate: boolean) => void; isPending: boolean }; }) { const queryClient = useQueryClient(); const [revisionsOpen, setRevisionsOpen] = useState(false); const { data: configRevisions } = useQuery({ queryKey: queryKeys.agents.configRevisions(agent.id), queryFn: () => agentsApi.listConfigRevisions(agent.id, companyId), }); const rollbackConfig = useMutation({ mutationFn: (revisionId: string) => agentsApi.rollbackConfigRevision(agent.id, revisionId, companyId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.urlKey) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.configRevisions(agent.id) }); }, }); return (

API Keys

{/* Configuration Revisions — collapsible at the bottom */}
{revisionsOpen && (
{(configRevisions ?? []).length === 0 ? (

No configuration revisions yet.

) : (
{(configRevisions ?? []).slice(0, 10).map((revision) => (
{revision.id.slice(0, 8)} · {formatDate(revision.createdAt)} · {revision.source}

Changed:{" "} {revision.changedKeys.length > 0 ? revision.changedKeys.join(", ") : "no tracked changes"}

))}
)}
)}
); } /* ---- Configuration Tab ---- */ function ConfigurationTab({ agent, companyId, onDirtyChange, onSaveActionChange, onCancelActionChange, onSavingChange, updatePermissions, }: { agent: Agent; companyId?: string; onDirtyChange: (dirty: boolean) => void; onSaveActionChange: (save: (() => void) | null) => void; onCancelActionChange: (cancel: (() => void) | null) => void; onSavingChange: (saving: boolean) => void; updatePermissions: { mutate: (canCreate: boolean) => void; isPending: boolean }; }) { const queryClient = useQueryClient(); const { data: adapterModels } = useQuery({ queryKey: ["adapter-models", agent.adapterType], queryFn: () => agentsApi.adapterModels(agent.adapterType), }); const updateAgent = useMutation({ mutationFn: (data: Record) => agentsApi.update(agent.id, data, companyId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.urlKey) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.configRevisions(agent.id) }); }, }); useEffect(() => { onSavingChange(updateAgent.isPending); }, [onSavingChange, updateAgent.isPending]); return (
updateAgent.mutate(patch)} isSaving={updateAgent.isPending} adapterModels={adapterModels} onDirtyChange={onDirtyChange} onSaveActionChange={onSaveActionChange} onCancelActionChange={onCancelActionChange} hideInlineSave sectionLayout="cards" />

Permissions

Can create new agents
); } /* ---- Runs Tab ---- */ function RunListItem({ run, isSelected, agentId }: { run: HeartbeatRun; isSelected: boolean; agentId: string }) { const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" }; const StatusIcon = statusInfo.icon; const metrics = runMetrics(run); const summary = run.resultJson ? String((run.resultJson as Record).summary ?? (run.resultJson as Record).result ?? "") : run.error ?? ""; return (
{run.id.slice(0, 8)} {sourceLabels[run.invocationSource] ?? run.invocationSource} {relativeTime(run.createdAt)}
{summary && ( {summary.slice(0, 60)} )} {(metrics.totalTokens > 0 || metrics.cost > 0) && (
{metrics.totalTokens > 0 && {formatTokens(metrics.totalTokens)} tok} {metrics.cost > 0 && ${metrics.cost.toFixed(3)}}
)} ); } function RunsTab({ runs, companyId, agentId, agentRouteId, selectedRunId, adapterType, }: { runs: HeartbeatRun[]; companyId: string; agentId: string; agentRouteId: string; selectedRunId: string | null; adapterType: string; }) { const { isMobile } = useSidebar(); if (runs.length === 0) { return

No runs yet.

; } // Sort by created descending const sorted = [...runs].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); // On mobile, don't auto-select so the list shows first; on desktop, auto-select latest const effectiveRunId = isMobile ? selectedRunId : (selectedRunId ?? sorted[0]?.id ?? null); const selectedRun = sorted.find((r) => r.id === effectiveRunId) ?? null; // Mobile: show either run list OR run detail with back button if (isMobile) { if (selectedRun) { return (
Back to runs
); } return (
{sorted.map((run) => ( ))}
); } // Desktop: side-by-side layout return (
{/* Left: run list — border stretches full height, content sticks */}
{sorted.map((run) => ( ))}
{/* Right: run detail — natural height, page scrolls */} {selectedRun && (
)}
); } /* ---- Run Detail (expanded) ---- */ function RunDetail({ run, agentRouteId, adapterType }: { run: HeartbeatRun; agentRouteId: string; adapterType: string }) { const queryClient = useQueryClient(); const navigate = useNavigate(); const metrics = runMetrics(run); const [sessionOpen, setSessionOpen] = useState(false); const [claudeLoginResult, setClaudeLoginResult] = useState(null); useEffect(() => { setClaudeLoginResult(null); }, [run.id]); const cancelRun = useMutation({ mutationFn: () => heartbeatsApi.cancel(run.id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(run.companyId, run.agentId) }); }, }); const canResumeLostRun = run.errorCode === "process_lost" && run.status === "failed"; const resumePayload = useMemo(() => { const payload: Record = { resumeFromRunId: run.id, }; const context = asRecord(run.contextSnapshot); if (!context) return payload; const issueId = asNonEmptyString(context.issueId); const taskId = asNonEmptyString(context.taskId); const taskKey = asNonEmptyString(context.taskKey); const commentId = asNonEmptyString(context.wakeCommentId) ?? asNonEmptyString(context.commentId); if (issueId) payload.issueId = issueId; if (taskId) payload.taskId = taskId; if (taskKey) payload.taskKey = taskKey; if (commentId) payload.commentId = commentId; return payload; }, [run.contextSnapshot, run.id]); const resumeRun = useMutation({ mutationFn: async () => { const result = await agentsApi.wakeup(run.agentId, { source: "on_demand", triggerDetail: "manual", reason: "resume_process_lost_run", payload: resumePayload, }, run.companyId); if (!("id" in result)) { throw new Error("Resume request was skipped because the agent is not currently invokable."); } return result; }, onSuccess: (resumedRun) => { queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(run.companyId, run.agentId) }); navigate(`/agents/${agentRouteId}/runs/${resumedRun.id}`); }, }); const canRetryRun = run.status === "failed" || run.status === "timed_out"; const retryPayload = useMemo(() => { const payload: Record = {}; const context = asRecord(run.contextSnapshot); if (!context) return payload; const issueId = asNonEmptyString(context.issueId); const taskId = asNonEmptyString(context.taskId); const taskKey = asNonEmptyString(context.taskKey); if (issueId) payload.issueId = issueId; if (taskId) payload.taskId = taskId; if (taskKey) payload.taskKey = taskKey; return payload; }, [run.contextSnapshot]); const retryRun = useMutation({ mutationFn: async () => { const result = await agentsApi.wakeup(run.agentId, { source: "on_demand", triggerDetail: "manual", reason: "retry_failed_run", payload: retryPayload, }, run.companyId); if (!("id" in result)) { throw new Error("Retry was skipped because the agent is not currently invokable."); } return result; }, onSuccess: (newRun) => { queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(run.companyId, run.agentId) }); navigate(`/agents/${agentRouteId}/runs/${newRun.id}`); }, }); const { data: touchedIssues } = useQuery({ queryKey: queryKeys.runIssues(run.id), queryFn: () => activityApi.issuesForRun(run.id), }); const touchedIssueIds = useMemo( () => Array.from(new Set((touchedIssues ?? []).map((issue) => issue.issueId))), [touchedIssues], ); const clearSessionsForTouchedIssues = useMutation({ mutationFn: async () => { if (touchedIssueIds.length === 0) return 0; await Promise.all(touchedIssueIds.map((issueId) => agentsApi.resetSession(run.agentId, issueId, run.companyId))); return touchedIssueIds.length; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(run.agentId) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(run.agentId) }); queryClient.invalidateQueries({ queryKey: queryKeys.runIssues(run.id) }); }, }); const runClaudeLogin = useMutation({ mutationFn: () => agentsApi.loginWithClaude(run.agentId, run.companyId), onSuccess: (data) => { setClaudeLoginResult(data); }, }); const isRunning = run.status === "running" && !!run.startedAt && !run.finishedAt; const [elapsedSec, setElapsedSec] = useState(() => { if (!run.startedAt) return 0; return Math.max(0, Math.round((Date.now() - new Date(run.startedAt).getTime()) / 1000)); }); useEffect(() => { if (!isRunning || !run.startedAt) return; const startMs = new Date(run.startedAt).getTime(); setElapsedSec(Math.max(0, Math.round((Date.now() - startMs) / 1000))); const id = setInterval(() => { setElapsedSec(Math.max(0, Math.round((Date.now() - startMs) / 1000))); }, 1000); return () => clearInterval(id); }, [isRunning, run.startedAt]); const timeFormat: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }; const startTime = run.startedAt ? new Date(run.startedAt).toLocaleTimeString("en-US", timeFormat) : null; const endTime = run.finishedAt ? new Date(run.finishedAt).toLocaleTimeString("en-US", timeFormat) : null; const durationSec = run.startedAt && run.finishedAt ? Math.round((new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime()) / 1000) : null; const displayDurationSec = durationSec ?? (isRunning ? elapsedSec : null); const hasMetrics = metrics.input > 0 || metrics.output > 0 || metrics.cached > 0 || metrics.cost > 0; const hasSession = !!(run.sessionIdBefore || run.sessionIdAfter); const sessionChanged = run.sessionIdBefore && run.sessionIdAfter && run.sessionIdBefore !== run.sessionIdAfter; const sessionId = run.sessionIdAfter || run.sessionIdBefore; const hasNonZeroExit = run.exitCode !== null && run.exitCode !== 0; return (
{/* Run summary card */}
{/* Left column: status + timing */}
{(run.status === "running" || run.status === "queued") && ( )} {canResumeLostRun && ( )} {canRetryRun && !canResumeLostRun && ( )}
{resumeRun.isError && (
{resumeRun.error instanceof Error ? resumeRun.error.message : "Failed to resume run"}
)} {retryRun.isError && (
{retryRun.error instanceof Error ? retryRun.error.message : "Failed to retry run"}
)} {startTime && (
{startTime} {endTime && } {endTime}
{relativeTime(run.startedAt!)} {run.finishedAt && <> → {relativeTime(run.finishedAt)}}
{displayDurationSec !== null && (
Duration: {displayDurationSec >= 60 ? `${Math.floor(displayDurationSec / 60)}m ${displayDurationSec % 60}s` : `${displayDurationSec}s`}
)}
)} {run.error && (
{run.error} {run.errorCode && ({run.errorCode})}
)} {run.errorCode === "claude_auth_required" && adapterType === "claude_local" && (
{runClaudeLogin.isError && (

{runClaudeLogin.error instanceof Error ? runClaudeLogin.error.message : "Failed to run Claude login"}

)} {claudeLoginResult?.loginUrl && (

Login URL: {claudeLoginResult.loginUrl}

)} {claudeLoginResult && ( <> {!!claudeLoginResult.stdout && (
                        {claudeLoginResult.stdout}
                      
)} {!!claudeLoginResult.stderr && (
                        {claudeLoginResult.stderr}
                      
)} )}
)} {hasNonZeroExit && (
Exit code {run.exitCode} {run.signal && (signal: {run.signal})}
)}
{/* Right column: metrics */} {hasMetrics && (
Input
{formatTokens(metrics.input)}
Output
{formatTokens(metrics.output)}
Cached
{formatTokens(metrics.cached)}
Cost
{metrics.cost > 0 ? `$${metrics.cost.toFixed(4)}` : "-"}
)}
{/* Collapsible session row */} {hasSession && (
{sessionOpen && (
{run.sessionIdBefore && (
{sessionChanged ? "Before" : "ID"}
)} {sessionChanged && run.sessionIdAfter && (
After
)} {touchedIssueIds.length > 0 && (
{clearSessionsForTouchedIssues.isError && (

{clearSessionsForTouchedIssues.error instanceof Error ? clearSessionsForTouchedIssues.error.message : "Failed to clear sessions"}

)}
)}
)}
)}
{/* Issues touched by this run */} {touchedIssues && touchedIssues.length > 0 && (
Issues Touched ({touchedIssues.length})
{touchedIssues.map((issue) => (
{issue.title}
{issue.identifier ?? issue.issueId.slice(0, 8)} ))}
)} {/* stderr excerpt for failed runs */} {run.stderrExcerpt && (
stderr
{run.stderrExcerpt}
)} {/* stdout excerpt when no log is available */} {run.stdoutExcerpt && !run.logRef && (
stdout
{run.stdoutExcerpt}
)} {/* Log viewer */}
); } /* ---- Log Viewer ---- */ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: string }) { const [events, setEvents] = useState([]); const [logLines, setLogLines] = useState>([]); const [loading, setLoading] = useState(true); const [logLoading, setLogLoading] = useState(!!run.logRef); const [logError, setLogError] = useState(null); const [logOffset, setLogOffset] = useState(0); const [isFollowing, setIsFollowing] = useState(false); const logEndRef = useRef(null); const pendingLogLineRef = useRef(""); const scrollContainerRef = useRef(null); const isFollowingRef = useRef(false); const lastMetricsRef = useRef<{ scrollHeight: number; distanceFromBottom: number }>({ scrollHeight: 0, distanceFromBottom: Number.POSITIVE_INFINITY, }); const isLive = run.status === "running" || run.status === "queued"; function appendLogContent(content: string, finalize = false) { if (!content && !finalize) return; const combined = `${pendingLogLineRef.current}${content}`; const split = combined.split("\n"); pendingLogLineRef.current = split.pop() ?? ""; if (finalize && pendingLogLineRef.current) { split.push(pendingLogLineRef.current); pendingLogLineRef.current = ""; } const parsed: Array<{ ts: string; stream: "stdout" | "stderr" | "system"; chunk: string }> = []; for (const line of split) { const trimmed = line.trim(); if (!trimmed) continue; try { const raw = JSON.parse(trimmed) as { ts?: unknown; stream?: unknown; chunk?: unknown }; const stream = raw.stream === "stderr" || raw.stream === "system" ? raw.stream : "stdout"; const chunk = typeof raw.chunk === "string" ? raw.chunk : ""; const ts = typeof raw.ts === "string" ? raw.ts : new Date().toISOString(); if (!chunk) continue; parsed.push({ ts, stream, chunk }); } catch { // ignore malformed lines } } if (parsed.length > 0) { setLogLines((prev) => [...prev, ...parsed]); } } // Fetch events const { data: initialEvents } = useQuery({ queryKey: ["run-events", run.id], queryFn: () => heartbeatsApi.events(run.id, 0, 200), }); useEffect(() => { if (initialEvents) { setEvents(initialEvents); setLoading(false); } }, [initialEvents]); const getScrollContainer = useCallback((): ScrollContainer => { if (scrollContainerRef.current) return scrollContainerRef.current; const container = findScrollContainer(logEndRef.current); scrollContainerRef.current = container; return container; }, []); const updateFollowingState = useCallback(() => { const container = getScrollContainer(); const metrics = readScrollMetrics(container); lastMetricsRef.current = metrics; const nearBottom = metrics.distanceFromBottom <= LIVE_SCROLL_BOTTOM_TOLERANCE_PX; isFollowingRef.current = nearBottom; setIsFollowing((prev) => (prev === nearBottom ? prev : nearBottom)); }, [getScrollContainer]); useEffect(() => { scrollContainerRef.current = null; lastMetricsRef.current = { scrollHeight: 0, distanceFromBottom: Number.POSITIVE_INFINITY, }; if (!isLive) { isFollowingRef.current = false; setIsFollowing(false); return; } updateFollowingState(); }, [isLive, run.id, updateFollowingState]); useEffect(() => { if (!isLive) return; const container = getScrollContainer(); updateFollowingState(); if (container === window) { window.addEventListener("scroll", updateFollowingState, { passive: true }); } else { container.addEventListener("scroll", updateFollowingState, { passive: true }); } window.addEventListener("resize", updateFollowingState); return () => { if (container === window) { window.removeEventListener("scroll", updateFollowingState); } else { container.removeEventListener("scroll", updateFollowingState); } window.removeEventListener("resize", updateFollowingState); }; }, [isLive, run.id, getScrollContainer, updateFollowingState]); // Auto-scroll only for live runs when following useEffect(() => { if (!isLive || !isFollowingRef.current) return; const container = getScrollContainer(); const previous = lastMetricsRef.current; const current = readScrollMetrics(container); const growth = Math.max(0, current.scrollHeight - previous.scrollHeight); const expectedDistance = previous.distanceFromBottom + growth; const movedAwayBy = current.distanceFromBottom - expectedDistance; // If user moved away from bottom between updates, release auto-follow immediately. if (movedAwayBy > LIVE_SCROLL_BOTTOM_TOLERANCE_PX) { isFollowingRef.current = false; setIsFollowing(false); lastMetricsRef.current = current; return; } scrollToContainerBottom(container, "auto"); const after = readScrollMetrics(container); lastMetricsRef.current = after; if (!isFollowingRef.current) { isFollowingRef.current = true; } setIsFollowing((prev) => (prev ? prev : true)); }, [events.length, logLines.length, isLive, getScrollContainer]); // Fetch persisted shell log useEffect(() => { let cancelled = false; pendingLogLineRef.current = ""; setLogLines([]); setLogOffset(0); setLogError(null); if (!run.logRef) { setLogLoading(false); return () => { cancelled = true; }; } setLogLoading(true); const firstLimit = typeof run.logBytes === "number" && run.logBytes > 0 ? Math.min(Math.max(run.logBytes + 1024, 256_000), 2_000_000) : 256_000; const load = async () => { try { let offset = 0; let first = true; while (!cancelled) { const result = await heartbeatsApi.log(run.id, offset, first ? firstLimit : 256_000); if (cancelled) break; appendLogContent(result.content, result.nextOffset === undefined); const next = result.nextOffset ?? offset + result.content.length; setLogOffset(next); offset = next; first = false; if (result.nextOffset === undefined || isLive) break; } } catch (err) { if (!cancelled) { setLogError(err instanceof Error ? err.message : "Failed to load run log"); } } finally { if (!cancelled) setLogLoading(false); } }; void load(); return () => { cancelled = true; }; }, [run.id, run.logRef, run.logBytes, isLive]); // Poll for live updates useEffect(() => { if (!isLive) return; const interval = setInterval(async () => { const maxSeq = events.length > 0 ? Math.max(...events.map((e) => e.seq)) : 0; try { const newEvents = await heartbeatsApi.events(run.id, maxSeq, 100); if (newEvents.length > 0) { setEvents((prev) => [...prev, ...newEvents]); } } catch { // ignore polling errors } }, 2000); return () => clearInterval(interval); }, [run.id, isLive, events]); // Poll shell log for running runs useEffect(() => { if (!isLive || !run.logRef) return; const interval = setInterval(async () => { try { const result = await heartbeatsApi.log(run.id, logOffset, 256_000); if (result.content) { appendLogContent(result.content, result.nextOffset === undefined); } if (result.nextOffset !== undefined) { setLogOffset(result.nextOffset); } else if (result.content.length > 0) { setLogOffset((prev) => prev + result.content.length); } } catch { // ignore polling errors } }, 2000); return () => clearInterval(interval); }, [run.id, run.logRef, isLive, logOffset]); const adapterInvokePayload = useMemo(() => { const evt = events.find((e) => e.eventType === "adapter.invoke"); return asRecord(evt?.payload ?? null); }, [events]); const adapter = useMemo(() => getUIAdapter(adapterType), [adapterType]); const transcript = useMemo(() => buildTranscript(logLines, adapter.parseStdoutLine), [logLines, adapter]); if (loading && logLoading) { return

Loading run logs...

; } if (events.length === 0 && logLines.length === 0 && !logError) { return

No log events.

; } const levelColors: Record = { info: "text-foreground", warn: "text-yellow-600 dark:text-yellow-400", error: "text-red-600 dark:text-red-400", }; const streamColors: Record = { stdout: "text-foreground", stderr: "text-red-600 dark:text-red-300", system: "text-blue-600 dark:text-blue-300", }; return (
{adapterInvokePayload && (
Invocation
{typeof adapterInvokePayload.adapterType === "string" && (
Adapter: {adapterInvokePayload.adapterType}
)} {typeof adapterInvokePayload.cwd === "string" && (
Working dir: {adapterInvokePayload.cwd}
)} {typeof adapterInvokePayload.command === "string" && (
Command: {[ adapterInvokePayload.command, ...(Array.isArray(adapterInvokePayload.commandArgs) ? adapterInvokePayload.commandArgs.filter((v): v is string => typeof v === "string") : []), ].join(" ")}
)} {Array.isArray(adapterInvokePayload.commandNotes) && adapterInvokePayload.commandNotes.length > 0 && (
Command notes
    {adapterInvokePayload.commandNotes .filter((value): value is string => typeof value === "string" && value.trim().length > 0) .map((note, idx) => (
  • {note}
  • ))}
)} {adapterInvokePayload.prompt !== undefined && (
Prompt
                {typeof adapterInvokePayload.prompt === "string"
                  ? adapterInvokePayload.prompt
                  : JSON.stringify(adapterInvokePayload.prompt, null, 2)}
              
)} {adapterInvokePayload.context !== undefined && (
Context
                {JSON.stringify(adapterInvokePayload.context, null, 2)}
              
)} {adapterInvokePayload.env !== undefined && (
Environment
                {formatEnvForDisplay(adapterInvokePayload.env)}
              
)}
)}
Transcript ({transcript.length})
{isLive && !isFollowing && ( )} {isLive && ( Live )}
{transcript.length === 0 && !run.logRef && (
No persisted transcript for this run.
)} {transcript.map((entry, idx) => { const time = new Date(entry.ts).toLocaleTimeString("en-US", { hour12: false }); const grid = "grid grid-cols-[auto_auto_1fr] gap-x-2 sm:gap-x-3 items-baseline"; const tsCell = "text-neutral-400 dark:text-neutral-600 select-none w-12 sm:w-16 text-[10px] sm:text-xs"; const lblCell = "w-14 sm:w-20 text-[10px] sm:text-xs"; const contentCell = "min-w-0 whitespace-pre-wrap break-words overflow-hidden"; const expandCell = "col-span-full md:col-start-3 md:col-span-1"; if (entry.kind === "assistant") { return (
{time} assistant {entry.text}
); } if (entry.kind === "thinking") { return (
{time} thinking {entry.text}
); } if (entry.kind === "user") { return (
{time} user {entry.text}
); } if (entry.kind === "tool_call") { return (
{time} tool_call {entry.name}
                  {JSON.stringify(entry.input, null, 2)}
                
); } if (entry.kind === "tool_result") { return (
{time} tool_result {entry.isError ? error : }
                  {(() => { try { return JSON.stringify(JSON.parse(entry.content), null, 2); } catch { return entry.content; } })()}
                
); } if (entry.kind === "init") { return (
{time} init model: {entry.model}{entry.sessionId ? `, session: ${entry.sessionId}` : ""}
); } if (entry.kind === "result") { return (
{time} result tokens in={formatTokens(entry.inputTokens)} out={formatTokens(entry.outputTokens)} cached={formatTokens(entry.cachedTokens)} cost=${entry.costUsd.toFixed(6)} {(entry.subtype || entry.isError || entry.errors.length > 0) && (
subtype={entry.subtype || "unknown"} is_error={entry.isError ? "true" : "false"} {entry.errors.length > 0 ? ` errors=${entry.errors.join(" | ")}` : ""}
)} {entry.text && (
{entry.text}
)}
); } const rawText = entry.text; const label = entry.kind === "stderr" ? "stderr" : entry.kind === "system" ? "system" : "stdout"; const color = entry.kind === "stderr" ? "text-red-600 dark:text-red-300" : entry.kind === "system" ? "text-blue-600 dark:text-blue-300" : "text-neutral-500"; return (
{time} {label} {rawText}
) })} {logError &&
{logError}
}
{(run.status === "failed" || run.status === "timed_out") && (
Failure details
{run.error && (
Error: {run.error}
)} {run.stderrExcerpt && run.stderrExcerpt.trim() && (
stderr excerpt
                {run.stderrExcerpt}
              
)} {run.resultJson && (
adapter result JSON
                {JSON.stringify(run.resultJson, null, 2)}
              
)} {run.stdoutExcerpt && run.stdoutExcerpt.trim() && !run.resultJson && (
stdout excerpt
                {run.stdoutExcerpt}
              
)}
)} {events.length > 0 && (
Events ({events.length})
{events.map((evt) => { const color = evt.color ?? (evt.level ? levelColors[evt.level] : null) ?? (evt.stream ? streamColors[evt.stream] : null) ?? "text-foreground"; return (
{new Date(evt.createdAt).toLocaleTimeString("en-US", { hour12: false })} {evt.stream ? `[${evt.stream}]` : ""} {evt.message ?? (evt.payload ? JSON.stringify(evt.payload) : "")}
); })}
)}
); } /* ---- Keys Tab ---- */ function KeysTab({ agentId, companyId }: { agentId: string; companyId?: string }) { const queryClient = useQueryClient(); const [newKeyName, setNewKeyName] = useState(""); const [newToken, setNewToken] = useState(null); const [tokenVisible, setTokenVisible] = useState(false); const [copied, setCopied] = useState(false); const { data: keys, isLoading } = useQuery({ queryKey: queryKeys.agents.keys(agentId), queryFn: () => agentsApi.listKeys(agentId, companyId), }); const createKey = useMutation({ mutationFn: () => agentsApi.createKey(agentId, newKeyName.trim() || "Default", companyId), onSuccess: (data) => { setNewToken(data.token); setTokenVisible(true); setNewKeyName(""); queryClient.invalidateQueries({ queryKey: queryKeys.agents.keys(agentId) }); }, }); const revokeKey = useMutation({ mutationFn: (keyId: string) => agentsApi.revokeKey(agentId, keyId, companyId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.agents.keys(agentId) }); }, }); function copyToken() { if (!newToken) return; navigator.clipboard.writeText(newToken); setCopied(true); setTimeout(() => setCopied(false), 2000); } const activeKeys = (keys ?? []).filter((k: AgentKey) => !k.revokedAt); const revokedKeys = (keys ?? []).filter((k: AgentKey) => k.revokedAt); return (
{/* New token banner */} {newToken && (

API key created — copy it now, it will not be shown again.

{tokenVisible ? newToken : newToken.replace(/./g, "•")} {copied && Copied!}
)} {/* Create new key */}

Create API Key

API keys allow this agent to authenticate calls to the Paperclip server.

setNewKeyName(e.target.value)} className="h-8 text-sm" onKeyDown={(e) => { if (e.key === "Enter") createKey.mutate(); }} />
{/* Active keys */} {isLoading &&

Loading keys...

} {!isLoading && activeKeys.length === 0 && !newToken && (

No active API keys.

)} {activeKeys.length > 0 && (

Active Keys

{activeKeys.map((key: AgentKey) => (
{key.name} Created {formatDate(key.createdAt)}
))}
)} {/* Revoked keys */} {revokedKeys.length > 0 && (

Revoked Keys

{revokedKeys.map((key: AgentKey) => (
{key.name} Revoked {key.revokedAt ? formatDate(key.revokedAt) : ""}
))}
)}
); }