import { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { useParams, useNavigate, Link, Navigate, useBeforeUnload } from "@/lib/router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { agentsApi, type AgentKey, type ClaudeLoginResult } from "../api/agents"; import { budgetsApi } from "../api/budgets"; import { heartbeatsApi } from "../api/heartbeats"; import { ApiError } from "../api/client"; 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 { PageTabBar } from "../components/PageTabBar"; import { adapterLabels, roleLabels } from "../components/agent-config-primitives"; import { getUIAdapter, buildTranscript } 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 { BudgetPolicyCard } from "../components/BudgetPolicyCard"; import { ScrollToBottom } from "../components/ScrollToBottom"; import { formatCents, formatDate, relativeTime, formatTokens, visibleRunCostUsd } from "../lib/utils"; import { cn } from "../lib/utils"; import { Button } from "@/components/ui/button"; import { Tabs } from "@/components/ui/tabs"; 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, } from "lucide-react"; import { Input } from "@/components/ui/input"; import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker"; import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView"; import { isUuidLike, type Agent, type BudgetPolicySummary, type HeartbeatRun, type HeartbeatRunEvent, type AgentRuntimeState, type LiveEvent, } from "@paperclipai/shared"; import { redactHomePathUserSegments, redactHomePathUserSegmentsInValue } from "@paperclipai/adapter-utils"; 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 redactHomePathUserSegments(value); try { return JSON.stringify(redactHomePathUserSegmentsInValue(value)); } catch { return redactHomePathUserSegments(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 = "dashboard" | "configuration" | "runs" | "budget"; function parseAgentDetailView(value: string | null): AgentDetailView { if (value === "configure" || value === "configuration") return "configuration"; if (value === "budget") return "budget"; if (value === "runs") return value; return "dashboard"; } 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 = visibleRunCostUsd(usage, result); 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 { data: budgetOverview } = useQuery({ queryKey: queryKeys.budgets.overview(resolvedCompanyId ?? "__none__"), queryFn: () => budgetsApi.overview(resolvedCompanyId!), enabled: !!resolvedCompanyId, refetchInterval: 30_000, staleTime: 5_000, }); 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 agentBudgetSummary = useMemo(() => { const matched = budgetOverview?.policies.find( (policy) => policy.scopeType === "agent" && policy.scopeId === (agent?.id ?? routeAgentRef), ); if (matched) return matched; const budgetMonthlyCents = agent?.budgetMonthlyCents ?? 0; const spentMonthlyCents = agent?.spentMonthlyCents ?? 0; return { policyId: "", companyId: resolvedCompanyId ?? "", scopeType: "agent", scopeId: agent?.id ?? routeAgentRef, scopeName: agent?.name ?? "Agent", metric: "billed_cents", windowKind: "calendar_month_utc", amount: budgetMonthlyCents, observedAmount: spentMonthlyCents, remainingAmount: Math.max(0, budgetMonthlyCents - spentMonthlyCents), utilizationPercent: budgetMonthlyCents > 0 ? Number(((spentMonthlyCents / budgetMonthlyCents) * 100).toFixed(2)) : 0, warnPercent: 80, hardStopEnabled: true, notifyEnabled: true, isActive: budgetMonthlyCents > 0, status: budgetMonthlyCents > 0 && spentMonthlyCents >= budgetMonthlyCents ? "hard_stop" : "ok", paused: agent?.status === "paused", pauseReason: agent?.pauseReason ?? null, windowStart: new Date(), windowEnd: new Date(), } satisfies BudgetPolicySummary; }, [agent, budgetOverview?.policies, resolvedCompanyId, routeAgentRef]); const mobileLiveRun = useMemo( () => (heartbeats ?? []).find((r) => r.status === "running" || r.status === "queued") ?? null, [heartbeats], ); useEffect(() => { if (!agent) return; if (urlRunId) { if (routeAgentRef !== canonicalAgentRef) { navigate(`/agents/${canonicalAgentRef}/runs/${urlRunId}`, { replace: true }); } return; } const canonicalTab = activeView === "configuration" ? "configuration" : activeView === "runs" ? "runs" : activeView === "budget" ? "budget" : "dashboard"; if (routeAgentRef !== canonicalAgentRef || urlTab !== canonicalTab) { navigate(`/agents/${canonicalAgentRef}/${canonicalTab}`, { replace: true }); return; } }, [agent, routeAgentRef, canonicalAgentRef, urlRunId, urlTab, activeView, 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 budgetMutation = useMutation({ mutationFn: (amount: number) => budgetsApi.upsertPolicy(resolvedCompanyId!, { scopeType: "agent", scopeId: agent?.id ?? routeAgentRef, amount, windowKind: "calendar_month_utc", }), onSuccess: () => { if (!resolvedCompanyId) return; queryClient.invalidateQueries({ queryKey: queryKeys.budgets.overview(resolvedCompanyId) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentLookupRef) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) }); queryClient.invalidateQueries({ queryKey: queryKeys.dashboard(resolvedCompanyId) }); }, }); 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 === "dashboard" && !urlRunId) { crumbs.push({ label: agentName }); } else { crumbs.push({ label: agentName, href: `/agents/${canonicalAgentRef}/dashboard` }); if (urlRunId) { crumbs.push({ label: "Runs", href: `/agents/${canonicalAgentRef}/runs` }); crumbs.push({ label: `Run ${urlRunId.slice(0, 8)}` }); } else if (activeView === "configuration") { crumbs.push({ label: "Configuration" }); } else if (activeView === "runs") { crumbs.push({ label: "Runs" }); } else if (activeView === "budget") { crumbs.push({ label: "Budget" }); } else { crumbs.push({ label: "Dashboard" }); } } setBreadcrumbs(crumbs); }, [setBreadcrumbs, agent, routeAgentRef, canonicalAgentRef, activeView, urlRunId]); useEffect(() => { closePanel(); return () => closePanel(); }, [closePanel]); useBeforeUnload( useCallback((event) => { if (!configDirty) return; event.preventDefault(); event.returnValue = ""; }, [configDirty]), ); if (isLoading) return ; if (error) return

{error.message}

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

{agent.name}

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

{agent.status === "paused" ? ( ) : ( )} {mobileLiveRun && ( Live )} {/* Overflow menu */}
{!urlRunId && ( navigate(`/agents/${canonicalAgentRef}/${value}`)} > navigate(`/agents/${canonicalAgentRef}/${value}`)} /> )} {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 === "dashboard" && ( )} {activeView === "configuration" && ( )} {activeView === "runs" && ( )} {activeView === "budget" && resolvedCompanyId ? (
budgetMutation.mutate(amount)} variant="plain" />
) : null}
); } /* ---- 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, agentId, agentRouteId, }: { agent: Agent; runs: HeartbeatRun[]; assignedIssues: { id: string; title: string; status: string; priority: string; identifier?: string | null; createdAt: Date }[]; runtimeState?: AgentRuntimeState; 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

); } /* ---- Costs Section (inline) ---- */ function CostsSection({ runtimeState, runs, }: { runtimeState?: AgentRuntimeState; runs: HeartbeatRun[]; }) { const runsWithCost = runs .filter((r) => { const metrics = runMetrics(r); return metrics.cost > 0 || metrics.input > 0 || metrics.output > 0 || metrics.cached > 0; }) .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 metrics = runMetrics(run); return ( ); })}
Date Run Input Output Cost
{formatDate(run.createdAt)} {run.id.slice(0, 8)} {formatTokens(metrics.input)} {formatTokens(metrics.output)} {metrics.cost > 0 ? `$${metrics.cost.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 [awaitingRefreshAfterSave, setAwaitingRefreshAfterSave] = useState(false); const lastAgentRef = useRef(agent); const { data: adapterModels } = useQuery({ queryKey: companyId ? queryKeys.agents.adapterModels(companyId, agent.adapterType) : ["agents", "none", "adapter-models", agent.adapterType], queryFn: () => agentsApi.adapterModels(companyId!, agent.adapterType), enabled: Boolean(companyId), }); const updateAgent = useMutation({ mutationFn: (data: Record) => agentsApi.update(agent.id, data, companyId), onMutate: () => { setAwaitingRefreshAfterSave(true); }, 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) }); }, onError: () => { setAwaitingRefreshAfterSave(false); }, }); useEffect(() => { if (awaitingRefreshAfterSave && agent !== lastAgentRef.current) { setAwaitingRefreshAfterSave(false); } lastAgentRef.current = agent; }, [agent, awaitingRefreshAfterSave]); const isConfigSaving = updateAgent.isPending || awaitingRefreshAfterSave; useEffect(() => { onSavingChange(isConfigSaving); }, [onSavingChange, isConfigSaving]); return (
updateAgent.mutate(patch)} isSaving={isConfigSaving} 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: initialRun, agentRouteId, adapterType }: { run: HeartbeatRun; agentRouteId: string; adapterType: string }) { const queryClient = useQueryClient(); const navigate = useNavigate(); const { data: hydratedRun } = useQuery({ queryKey: queryKeys.runDetail(initialRun.id), queryFn: () => heartbeatsApi.get(initialRun.id), enabled: Boolean(initialRun.id), }); const run = hydratedRun ?? initialRun; 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 [isStreamingConnected, setIsStreamingConnected] = useState(false); const [transcriptMode, setTranscriptMode] = useState("nice"); 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 isRunLogUnavailable(err: unknown): boolean { return err instanceof ApiError && err.status === 404; } 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 && !isLive) { 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) { if (isLive && isRunLogUnavailable(err)) { setLogLoading(false); return; } 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 || isStreamingConnected) 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, isStreamingConnected, events]); // Poll shell log for running runs useEffect(() => { if (!isLive || isStreamingConnected) 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 (err) { if (isRunLogUnavailable(err)) return; // ignore polling errors } }, 2000); return () => clearInterval(interval); }, [run.id, isLive, isStreamingConnected, logOffset]); // Stream live updates from websocket (primary path for running runs). useEffect(() => { if (!isLive) return; let closed = false; let reconnectTimer: number | null = null; let socket: WebSocket | null = null; const scheduleReconnect = () => { if (closed) return; reconnectTimer = window.setTimeout(connect, 1500); }; const connect = () => { if (closed) return; const protocol = window.location.protocol === "https:" ? "wss" : "ws"; const url = `${protocol}://${window.location.host}/api/companies/${encodeURIComponent(run.companyId)}/events/ws`; socket = new WebSocket(url); socket.onopen = () => { setIsStreamingConnected(true); }; socket.onmessage = (message) => { const rawMessage = typeof message.data === "string" ? message.data : ""; if (!rawMessage) return; let event: LiveEvent; try { event = JSON.parse(rawMessage) as LiveEvent; } catch { return; } if (event.companyId !== run.companyId) return; const payload = asRecord(event.payload); const eventRunId = asNonEmptyString(payload?.runId); if (!payload || eventRunId !== run.id) return; if (event.type === "heartbeat.run.log") { const chunk = typeof payload.chunk === "string" ? payload.chunk : ""; if (!chunk) return; const streamRaw = asNonEmptyString(payload.stream); const stream = streamRaw === "stderr" || streamRaw === "system" ? streamRaw : "stdout"; const ts = asNonEmptyString((payload as Record).ts) ?? event.createdAt; setLogLines((prev) => [...prev, { ts, stream, chunk }]); return; } if (event.type !== "heartbeat.run.event") return; const seq = typeof payload.seq === "number" ? payload.seq : null; if (seq === null || !Number.isFinite(seq)) return; const streamRaw = asNonEmptyString(payload.stream); const stream = streamRaw === "stdout" || streamRaw === "stderr" || streamRaw === "system" ? streamRaw : null; const levelRaw = asNonEmptyString(payload.level); const level = levelRaw === "info" || levelRaw === "warn" || levelRaw === "error" ? levelRaw : null; const liveEvent: HeartbeatRunEvent = { id: seq, companyId: run.companyId, runId: run.id, agentId: run.agentId, seq, eventType: asNonEmptyString(payload.eventType) ?? "event", stream, level, color: asNonEmptyString(payload.color), message: asNonEmptyString(payload.message), payload: asRecord(payload.payload), createdAt: new Date(event.createdAt), }; setEvents((prev) => { if (prev.some((existing) => existing.seq === seq)) return prev; return [...prev, liveEvent]; }); }; socket.onerror = () => { socket?.close(); }; socket.onclose = () => { setIsStreamingConnected(false); scheduleReconnect(); }; }; connect(); return () => { closed = true; setIsStreamingConnected(false); if (reconnectTimer !== null) window.clearTimeout(reconnectTimer); if (socket) { socket.onopen = null; socket.onmessage = null; socket.onerror = null; socket.onclose = null; socket.close(1000, "run_detail_unmount"); } }; }, [isLive, run.companyId, run.id, run.agentId]); const adapterInvokePayload = useMemo(() => { const evt = events.find((e) => e.eventType === "adapter.invoke"); return redactHomePathUserSegmentsInValue(asRecord(evt?.payload ?? null)); }, [events]); const adapter = useMemo(() => getUIAdapter(adapterType), [adapterType]); const transcript = useMemo(() => buildTranscript(logLines, adapter.parseStdoutLine), [logLines, adapter]); useEffect(() => { setTranscriptMode("nice"); }, [run.id]); 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"
                  ? redactHomePathUserSegments(adapterInvokePayload.prompt)
                  : JSON.stringify(redactHomePathUserSegmentsInValue(adapterInvokePayload.prompt), null, 2)}
              
)} {adapterInvokePayload.context !== undefined && (
Context
                {JSON.stringify(redactHomePathUserSegmentsInValue(adapterInvokePayload.context), null, 2)}
              
)} {adapterInvokePayload.env !== undefined && (
Environment
                {formatEnvForDisplay(adapterInvokePayload.env)}
              
)}
)}
Transcript ({transcript.length})
{(["nice", "raw"] as const).map((mode) => ( ))}
{isLive && !isFollowing && ( )} {isLive && ( Live )}
{logError && (
{logError}
)}
{(run.status === "failed" || run.status === "timed_out") && (
Failure details
{run.error && (
Error: {redactHomePathUserSegments(run.error)}
)} {run.stderrExcerpt && run.stderrExcerpt.trim() && (
stderr excerpt
                {redactHomePathUserSegments(run.stderrExcerpt)}
              
)} {run.resultJson && (
adapter result JSON
                {JSON.stringify(redactHomePathUserSegmentsInValue(run.resultJson), null, 2)}
              
)} {run.stdoutExcerpt && run.stdoutExcerpt.trim() && !run.resultJson && (
stdout excerpt
                {redactHomePathUserSegments(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 ? redactHomePathUserSegments(evt.message) : evt.payload ? JSON.stringify(redactHomePathUserSegmentsInValue(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) : ""}
))}
)}
); }