import { PrimaryAgentIndicator, SetPrimaryAgentButton } from "@/components/primary-agent/PrimaryAgentPresentation"; import { AiConnectionPoolRunDetails } from "@/components/ai-connections/AiConnectionPoolRunDetails"; import { AgentConnectionInstructions } from "@/features/connections/ConnectionInstructions"; import type { AgentInstructionCandidate, AgentInstructionsBundle } from "@paperclipai/shared"; import { InstructionHistory } from "../components/InstructionHistory"; import { AgentCharacter } from "../components/AgentCharacter"; import { characterStateForAgent } from "@paperclipai/shared"; import { mergeRunLogChunks, readChunkSeq } from "../lib/run-log-chunks"; import { getPageVisibility, usePageVisibility } from "../lib/page-visibility"; import { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { useParams, useNavigate, Link, Navigate, useBeforeUnload, type NavigateFunction } from "@/lib/router"; import { useQuery, useMutation, useQueryClient, type QueryClient } from "@tanstack/react-query"; import { agentsApi, type AgentKey, type ClaudeLoginResult, type AgentPermissionUpdate, } from "../api/agents"; import { builtInAgentsApi, type BuiltInManagedResourceKind } from "../api/builtInAgents"; import { companySkillsApi } from "../api/companySkills"; import { heartbeatsApi } from "../api/heartbeats"; import { instanceSettingsApi } from "../api/instanceSettings"; import { ApiError } from "../api/client"; import { activityApi } from "../api/activity"; import { accessApi } from "../api/access"; import { issuesApi } from "../api/issues"; import { projectsApi } from "../api/projects"; import { usePanel } from "../context/PanelContext"; import { useSidebar } from "../context/SidebarContext"; import { useCompany } from "../context/CompanyContext"; import { useToastActions } from "../context/ToastContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { applyCompanyPrefix } from "../lib/company-routes"; import { copyTextToClipboard } from "../lib/clipboard"; import { AgentSkillsTab } from "./agent-skills/AgentSkillsTab"; import { AgentConfigForm } from "../components/AgentConfigForm"; import { getAdapterDisplay } from "../adapters/adapter-display-registry"; import { adapterLabels, roleLabels, help } from "../components/agent-config-primitives"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; import { useAdapterCapabilities } from "@/adapters/use-adapter-capabilities"; import { redactCommandText as redactCommandSecretText } from "@paperclipai/adapter-utils"; import { MarkdownEditor } from "../components/MarkdownEditor"; import { assetsApi } from "../api/assets"; import { toolsApi } from "../api/tools"; import { getUIAdapter, buildTranscript, onAdapterChange } from "../adapters"; import { StatusBadge } from "../components/StatusBadge"; import { MarkdownBody } from "../components/MarkdownBody"; import { CopyText } from "../components/CopyText"; import { IssueRow } from "../components/IssueRow"; import { StatusGlyph } from "../components/StatusGlyph"; import { MembershipAction } from "../components/MembershipAction"; import { StarToggle } from "../components/StarToggle"; import { Identity } from "../components/Identity"; import { PageSkeleton } from "../components/PageSkeleton"; import { AgentActionButtons } from "../components/AgentActionButtons"; import { InlineBanner } from "../components/InlineBanner"; import { BuiltInBundlePanel } from "../components/BuiltInBundlePanel"; import { ConfigureBuiltInAgentModal } from "../components/ConfigureBuiltInAgentModal"; import { TrustPresetSection } from "../components/TrustPresetSection"; import { FileTree, buildFileTree } from "../components/FileTree"; import { ScrollToBottom } from "../components/ScrollToBottom"; import { SourceResolvedFoldCallout } from "../components/SourceResolvedFoldCallout"; import { SourceResolvedFoldBadge } from "../components/SourceResolvedFoldBadge"; import { readSourceResolvedWatchdogFold } from "../lib/source-resolved-watchdog-fold"; import { buildSameOriginWebSocketUrl } from "../lib/websocket-url"; import { tryCreateWebSocket } from "../lib/websocket"; import { formatDate, relativeTime, formatTokens, visibleRunCostUsd, visibleRunTokenTotal } from "../lib/utils"; import { cn } from "../lib/utils"; import { RunRetryDetails } from "../components/RunRetryDetails"; import { Button } from "@/components/ui/button"; import { Tabs } from "@/components/ui/tabs"; import { PageTabBar } from "../components/PageTabBar"; import { AuditFeed } from "./audit/AuditFeed"; import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { CheckCircle2, XCircle, Clock, Timer, Loader2, Slash, RotateCcw, Plus, Key, Eye, EyeOff, Copy, ChevronRight, ChevronDown, ArrowLeft, HelpCircle, FolderOpen, AlertTriangle, } from "lucide-react"; import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Input } from "@/components/ui/input"; import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView"; import { AgentToolsTab } from "./AgentToolsTab"; import { AgentChannelsPanel } from "../components/chat/AgentChannelsPanel"; import { appendCapped, LIVE_TRANSCRIPT_RENDER_LIMIT, MAX_LIVE_EVENTS, MAX_LIVE_LOG_LINES, } from "../lib/live-log-buffer"; import { isUuidLike, type Agent, type AgentDetail as AgentDetailRecord, type HeartbeatRun, type HeartbeatRunEvent, type AgentRuntimeState, type Issue, type LiveEvent, type WorkspaceOperation, isResponsibleUserDenialCode, responsibleUserLabel, } from "@paperclipai/shared"; import { ResponsibleUserDenialNotice } from "../components/ResponsibleUserDenialNotice"; import { RunWorkspaceRecoverySurface } from "../components/RunWorkspaceRecoverySurface"; import { RunnerInspector } from "../components/RunnerInspector"; import { HoneycombRunLink } from "../components/HoneycombRunLink"; import { ProviderTraceStatusBadge, runRequestedProviderTrace, } from "../components/ProviderTraceStatusBadge"; import { buildPermissionsForTrustPreset, getTrustPreset } from "../lib/trust-policy-ui"; import { redactHomePathUserSegments, redactHomePathUserSegmentsInValue } from "@paperclipai/adapter-utils"; import { agentRouteRef } from "../lib/utils"; import { isStarred, resourceMembershipState, useResourceMembershipMutation, useResourceMemberships, } from "../hooks/useResourceMemberships"; import { Badge } from "@/components/ui/badge"; import { AGENT_DETAIL_NAVIGATION, agentDetailHref, agentLegacyAuditSection, agentScopedAuditHref, parseAgentDetailView, type AgentDetailView, } from "./agent-detail-navigation"; 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-blue-600 dark:text-blue-400" }, // Gallery feedback r1: running = status blue, not cyan. queued: { icon: Clock, color: "text-yellow-600 dark:text-yellow-400" }, scheduled_retry: { icon: Clock, color: "text-sky-600 dark:text-sky-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 RUN_LOG_PAGE_BYTES = 256_000; 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 COMMAND_ENV_KEY_RE = /(^command$|^cmd$|command[-_]?line|resolved[-_]?command|PAPERCLIP_RESOLVED_COMMAND)/i; const JWT_VALUE_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?$/; function formatOrgChainHealthPath(agent: AgentDetailRecord) { return agent.orgChainHealth?.fullChain .map((entry) => `${entry.name}${entry.status !== "active" && entry.status !== "idle" ? ` (${entry.status})` : ""}`) .join(" -> ") ?? agent.name; } function redactPathText(value: string, censorUsernameInLogs: boolean) { return redactHomePathUserSegments(value, { enabled: censorUsernameInLogs }); } function redactPathValue(value: T, censorUsernameInLogs: boolean): T { return redactHomePathUserSegmentsInValue(value, { enabled: censorUsernameInLogs }); } function redactCommandText(value: string, censorUsernameInLogs: boolean): string { return redactPathText(redactCommandSecretText(value, REDACTED_ENV_VALUE), censorUsernameInLogs); } 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, censorUsernameInLogs: boolean): 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" && COMMAND_ENV_KEY_RE.test(key)) return redactCommandText(value, censorUsernameInLogs); if (typeof value === "string") return redactPathText(value, censorUsernameInLogs); try { return JSON.stringify(redactPathValue(value, censorUsernameInLogs)); } catch { return redactPathText(String(value), censorUsernameInLogs); } } function isMarkdown(pathValue: string) { return pathValue.toLowerCase().endsWith(".md"); } function shouldUseMarkdownInstructionsEditor(input: { selectedFileExists: boolean; selectedPath: string; detail?: { markdown?: boolean } | null; summary?: { markdown?: boolean } | null; }) { const metadataMarkdown = input.detail?.markdown ?? input.summary?.markdown; if (typeof metadataMarkdown === "boolean") return metadataMarkdown; return isMarkdown(input.selectedPath); } function formatEnvForDisplay(envValue: unknown, censorUsernameInLogs: boolean): 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], censorUsernameInLogs)}`) .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 }); } /** @deprecated Use AGENT_DETAIL_NAVIGATION for contextual navigation. */ export const AGENT_DETAIL_TABS = AGENT_DETAIL_NAVIGATION.flatMap((section) => section.items); const LEGACY_AGENT_DETAIL_TABS = [ { value: "dashboard", label: "Dashboard" }, { value: "instructions", label: "Instructions" }, { value: "skills", label: "Skills" }, { value: "configuration", label: "Configuration" }, { value: "secrets", label: "Secrets" }, { value: "tools", label: "Tools" }, { value: "runs", label: "Runs" }, { value: "audit", label: "Audit" }, { value: "budget", label: "Budget" }, ] as const; export const DISCARD_AGENT_CONFIG_CHANGES_MESSAGE = "Discard unsaved agent configuration changes?"; export function confirmAgentConfigNavigation( dirty: boolean, confirm: (message: string) => boolean = (message) => typeof window === "undefined" || window.confirm(message), ): boolean { return !dirty || confirm(DISCARD_AGENT_CONFIG_CHANGES_MESSAGE); } export function agentConfigHistoryRestoreDelta(currentIndex: unknown, nextIndex: unknown): number | null { if (typeof currentIndex !== "number" || typeof nextIndex !== "number") return null; const delta = currentIndex - nextIndex; return delta === 0 ? null : delta; } export function restoreAgentConfigHistoryEntry( history: Pick, currentEntry: { index: unknown; state: unknown; url: string }, nextIndex: unknown, ): boolean { const restoreDelta = agentConfigHistoryRestoreDelta(currentEntry.index, nextIndex); if (restoreDelta === null) { // Some legacy URL-cleanup paths erased React Router's history index. A // fresh copy of the guarded entry is the only safe way to return without // letting Router consume the unindexed destination and discard the form. history.pushState(currentEntry.state, "", currentEntry.url); return false; } history.go(restoreDelta); return true; } export { agentDetailHref, agentScopedAuditHref, parseAgentDetailView }; 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 setsEqual(left: Set, right: Set) { if (left.size !== right.size) return false; for (const value of left) { if (!right.has(value)) return false; } return true; } 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); const provider = asNonEmptyString(usage?.provider) ?? null; const model = asNonEmptyString(usage?.model) ?? null; return { input, output, cached, cost, totalTokens: visibleRunTokenTotal(usage), provider, model, }; } export type RunLogChunk = { seq?: number; 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 buildHeartbeatProgressLogLine( payload: Record, fallbackTimestamp: string, ): RunLogChunk | null { const message = asNonEmptyString(payload.message); if (!message) return null; const phase = asNonEmptyString(payload.phase); const ts = asNonEmptyString(payload.updatedAt) ?? fallbackTimestamp; const chunk = phase ? `[${phase}] ${message}` : message; return { ts, stream: "system", chunk }; } export function heartbeatProgressLogLineKey(line: RunLogChunk): string { return `${line.ts}\u0000${line.stream}\u0000${line.chunk}`; } export function shouldPollRunShellLog(status: HeartbeatRun["status"]): boolean { return status === "running"; } export function runDetailRefetchIntervalMs(status: HeartbeatRun["status"]): 5000 | 15000 | false { if (status === "queued") return 5000; if (status === "running") return 15000; return false; } export function RunInvocationCard({ payload, censorUsernameInLogs, }: { payload: Record; censorUsernameInLogs: boolean; }) { const rawCommandLine = [ typeof payload.command === "string" ? payload.command : null, ...(Array.isArray(payload.commandArgs) ? payload.commandArgs.filter((value): value is string => typeof value === "string") : []), ] .filter((value): value is string => Boolean(value)) .join(" "); const commandLine = rawCommandLine ? redactCommandText(rawCommandLine, censorUsernameInLogs) : ""; const hasAdvancedDetails = commandLine.length > 0 || (Array.isArray(payload.commandNotes) && payload.commandNotes.length > 0) || payload.prompt !== undefined || payload.context !== undefined || payload.env !== undefined; return (
Invocation
{typeof payload.adapterType === "string" && (
Adapter: {payload.adapterType}
)} {typeof payload.cwd === "string" && (
Working dir: {payload.cwd}
)} {hasAdvancedDetails && ( Details {commandLine && (
Command: {commandLine}
)} {Array.isArray(payload.commandNotes) && payload.commandNotes.length > 0 && (
Command notes
    {payload.commandNotes .filter((value): value is string => typeof value === "string" && value.trim().length > 0) .map((note, idx) => (
  • {note}
  • ))}
)} {payload.prompt !== undefined && (
Prompt
                  {typeof payload.prompt === "string"
                    ? redactPathText(payload.prompt, censorUsernameInLogs)
                    : JSON.stringify(redactPathValue(payload.prompt, censorUsernameInLogs), null, 2)}
                
)} {payload.context !== undefined && (
Context
                  {JSON.stringify(redactPathValue(payload.context, censorUsernameInLogs), null, 2)}
                
)} {payload.env !== undefined && (
Environment
                  {formatEnvForDisplay(payload.env, censorUsernameInLogs)}
                
)}
)}
); } function parseStoredLogContent(content: string): RunLogChunk[] { const parsed: RunLogChunk[] = []; for (const line of content.split("\n")) { 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 log lines. } } return parsed; } function workspaceOperationPhaseLabel(phase: WorkspaceOperation["phase"]) { switch (phase) { case "worktree_prepare": return "Worktree setup"; case "workspace_config_freshness": return "Config freshness"; case "workspace_provision": return "Provision"; case "workspace_teardown": return "Teardown"; case "worktree_cleanup": return "Worktree cleanup"; default: return phase; } } function workspaceOperationStatusTone(status: WorkspaceOperation["status"]) { switch (status) { case "succeeded": return "border-green-500/20 bg-green-500/10 text-green-700 dark:text-green-300"; case "failed": return "border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300"; case "running": return "border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-300"; case "skipped": return "border-yellow-500/20 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300"; default: return "border-border bg-muted/40 text-muted-foreground"; } } function WorkspaceOperationStatusBadge({ status }: { status: WorkspaceOperation["status"] }) { return ( {status.replace("_", " ")} ); } function WorkspaceOperationLogViewer({ operation, censorUsernameInLogs, }: { operation: WorkspaceOperation; censorUsernameInLogs: boolean; }) { const [open, setOpen] = useState(false); const { data: logData, isLoading, error } = useQuery({ queryKey: ["workspace-operation-log", operation.id], queryFn: () => heartbeatsApi.workspaceOperationLog(operation.id), enabled: open && Boolean(operation.logRef), refetchInterval: open && operation.status === "running" ? 2000 : false, }); const chunks = useMemo( () => (logData?.content ? parseStoredLogContent(logData.content) : []), [logData?.content], ); return (
{open && (
{isLoading &&
Loading log...
} {error && (
{error instanceof Error ? error.message : "Failed to load workspace operation log"}
)} {!isLoading && !error && chunks.length === 0 && (
No persisted log lines.
)} {chunks.length > 0 && (
{chunks.map((chunk, index) => (
{new Date(chunk.ts).toLocaleTimeString("en-US", { hour12: false })} [{chunk.stream}] {redactPathText(chunk.chunk, censorUsernameInLogs)}
))}
)}
)}
); } function WorkspaceOperationsSection({ operations, censorUsernameInLogs, }: { operations: WorkspaceOperation[]; censorUsernameInLogs: boolean; }) { if (operations.length === 0) return null; return (
Workspace ({operations.length})
{operations.map((operation) => { const metadata = asRecord(operation.metadata); return (
{workspaceOperationPhaseLabel(operation.phase)}
{relativeTime(operation.startedAt)} {operation.finishedAt && ` to ${relativeTime(operation.finishedAt)}`}
{operation.command && (
Command: {operation.command}
)} {operation.cwd && (
Working dir: {operation.cwd}
)} {(asNonEmptyString(metadata?.branchName) || asNonEmptyString(metadata?.baseRef) || asNonEmptyString(metadata?.worktreePath) || asNonEmptyString(metadata?.repoRoot) || asNonEmptyString(metadata?.cleanupAction)) && (
{asNonEmptyString(metadata?.branchName) && (
Branch: {metadata?.branchName as string}
)} {asNonEmptyString(metadata?.baseRef) && (
Base ref: {metadata?.baseRef as string}
)} {asNonEmptyString(metadata?.worktreePath) && (
Worktree: {metadata?.worktreePath as string}
)} {asNonEmptyString(metadata?.repoRoot) && (
Repo root: {metadata?.repoRoot as string}
)} {asNonEmptyString(metadata?.cleanupAction) && (
Cleanup: {metadata?.cleanupAction as string}
)}
)} {typeof metadata?.created === "boolean" && (
{metadata.created ? "Created by this run" : "Reused existing workspace"}
)} {operation.stderrExcerpt && operation.stderrExcerpt.trim() && (
stderr excerpt
                    {redactPathText(operation.stderrExcerpt, censorUsernameInLogs)}
                  
)} {operation.stdoutExcerpt && operation.stdoutExcerpt.trim() && (
stdout excerpt
                    {redactPathText(operation.stdoutExcerpt, censorUsernameInLogs)}
                  
)} {operation.logRef && ( )}
); })}
); } export function AgentDetail() { const { companyPrefix, agentId, tab: urlTab, runId: urlRunId } = useParams<{ companyPrefix?: string; agentId: string; tab?: string; runId?: string; }>(); const { companies, selectedCompanyId, selectionSource, setSelectedCompanyId } = useCompany(); const { closePanel } = usePanel(); const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const navigate = useNavigate(); const [actionError, setActionError] = useState(null); const [dismissedLeftAgentIds, setDismissedLeftAgentIds] = useState>(() => new Set()); const activeView: AgentDetailView = urlRunId ? "run-detail" : parseAgentDetailView(urlTab ?? null); const legacyAuditSection = !urlRunId ? agentLegacyAuditSection(urlTab ?? null) : null; const legacyView = urlRunId ? "runs" : legacyAuditSection === "runs" ? "runs" : legacyAuditSection === "activity" ? "audit" : legacyAuditSection === "costs" || legacyAuditSection === "budgets" ? "budget" : activeView === "overview" ? "dashboard" : activeView === "runtime" ? "configuration" : activeView; const needsOverviewData = activeView === "overview"; const needsRunData = activeView === "run-detail"; const shouldLoadHeartbeats = needsOverviewData || needsRunData; 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 prepareAgentNavigation = useCallback(() => { return confirmAgentConfigNavigation(configDirty); }, [configDirty]); 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 agentCompany = companies.find((company) => company.id === agent?.companyId); const handleLegacyTabChange = useCallback((next: string) => { if (!prepareAgentNavigation()) return; navigate(`/agents/${canonicalAgentRef || routeAgentRef}/${next}`); }, [canonicalAgentRef, navigate, prepareAgentNavigation, routeAgentRef]); const agentLookupRef = agent?.id ?? routeAgentRef; const resolvedAgentId = agent?.id ?? null; const { data: boardAccess } = useQuery({ queryKey: queryKeys.access.currentBoardAccess, queryFn: () => accessApi.getCurrentBoardAccess(), retry: false, }); const canUseProviderTrace = boardAccess?.source === "local_implicit" || boardAccess?.isInstanceAdmin === true; const membershipsQuery = useResourceMemberships(resolvedCompanyId); const membershipMutation = useResourceMembershipMutation(resolvedCompanyId); const agentMembershipState = resolvedAgentId ? resourceMembershipState(membershipsQuery.data, "agent", resolvedAgentId) : "joined"; const { data: experimentalSettings } = useQuery({ queryKey: queryKeys.instance.experimentalSettings, queryFn: () => instanceSettingsApi.getExperimental(), enabled: !!resolvedCompanyId, }); const builtInAgentsEnabled = experimentalSettings?.enableBuiltInAgents === true; const { data: builtInStates } = useQuery({ queryKey: queryKeys.builtInAgents.list(resolvedCompanyId!), queryFn: () => builtInAgentsApi.list(resolvedCompanyId!), enabled: !!resolvedCompanyId && builtInAgentsEnabled, }); const builtInState = builtInAgentsEnabled ? builtInStates?.find((entry) => entry.agentId === resolvedAgentId) ?? null : null; const builtInFeatureLabel = builtInState ? builtInState.definition.featureKeys .map((key) => key.charAt(0).toUpperCase() + key.slice(1)) .join(", ") : ""; const invalidateBuiltIn = useCallback(() => { queryClient.invalidateQueries({ queryKey: queryKeys.builtInAgents.list(resolvedCompanyId!) }); if (resolvedAgentId) { queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(resolvedAgentId) }); } queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) }); }, [queryClient, resolvedCompanyId, resolvedAgentId, routeAgentRef]); const resetBuiltIn = useMutation({ mutationFn: () => builtInAgentsApi.reset(resolvedCompanyId!, builtInState!.definition.key), onSuccess: invalidateBuiltIn, }); const [showBuiltInConfigure, setShowBuiltInConfigure] = useState(false); const resetBuiltInResource = useMutation({ mutationFn: (kind: BuiltInManagedResourceKind) => builtInAgentsApi.reset(resolvedCompanyId!, builtInState!.definition.key, [kind]), onSuccess: invalidateBuiltIn, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to update bundle resource"); }, }); const runBuiltInRoutine = useMutation({ mutationFn: (routineKey: string) => builtInAgentsApi.runRoutine(resolvedCompanyId!, builtInState!.definition.key, routineKey), onSuccess: invalidateBuiltIn, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to run built-in routine"); }, }); const enableBuiltInSchedule = useMutation({ mutationFn: (routineKey: string) => builtInAgentsApi.enableRoutineSchedule(resolvedCompanyId!, builtInState!.definition.key, routineKey), onSuccess: invalidateBuiltIn, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to enable routine schedule"); }, }); const disableBuiltInSchedule = useMutation({ mutationFn: (routineKey: string) => builtInAgentsApi.disableRoutineSchedule(resolvedCompanyId!, builtInState!.definition.key, routineKey), onSuccess: invalidateBuiltIn, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to disable routine schedule"); }, }); const builtInRoutineActionPending = runBuiltInRoutine.isPending ? "run" : enableBuiltInSchedule.isPending ? "enable" : disableBuiltInSchedule.isPending ? "disable" : null; const { data: runtimeState } = useQuery({ queryKey: queryKeys.agents.runtimeState(resolvedAgentId ?? routeAgentRef), queryFn: () => agentsApi.runtimeState(resolvedAgentId!, resolvedCompanyId ?? undefined), enabled: Boolean(resolvedAgentId) && needsOverviewData, }); const { data: heartbeats } = useQuery({ queryKey: queryKeys.heartbeats(resolvedCompanyId!, agent?.id ?? undefined), queryFn: () => heartbeatsApi.list(resolvedCompanyId!, agent?.id ?? undefined), enabled: !!resolvedCompanyId && !!agent?.id && shouldLoadHeartbeats, }); const { data: allIssues } = useQuery({ queryKey: [...queryKeys.issues.list(resolvedCompanyId!), "participant-agent", resolvedAgentId ?? "__none__"], queryFn: () => issuesApi.list(resolvedCompanyId!, { participantAgentId: resolvedAgentId! }), enabled: !!resolvedCompanyId && !!resolvedAgentId && needsOverviewData, }); const { data: allAgents } = useQuery({ queryKey: queryKeys.agents.list(resolvedCompanyId!), queryFn: () => agentsApi.list(resolvedCompanyId!), enabled: !!resolvedCompanyId && needsOverviewData, }); const { data: skillSnapshot } = useQuery({ queryKey: queryKeys.agents.skills(resolvedAgentId ?? "__none__"), queryFn: () => agentsApi.skills(resolvedAgentId!, resolvedCompanyId ?? undefined), enabled: Boolean(resolvedCompanyId && resolvedAgentId && needsOverviewData), }); const { data: overviewCompanySkills } = useQuery({ queryKey: queryKeys.companySkills.list(resolvedCompanyId ?? "__none__"), queryFn: () => companySkillsApi.list(resolvedCompanyId!), enabled: Boolean(resolvedCompanyId && needsOverviewData), }); const assignedIssues = useMemo( () => [...(allIssues ?? [])].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()), [allIssues], ); const reportsToAgent = (allAgents ?? []).find((a) => a.id === agent?.reportsTo); const directReports = (allAgents ?? []).filter((a) => a.reportsTo === agent?.id && a.status !== "terminated"); const overviewSkillNames = useMemo(() => { const namesByKey = new Map((overviewCompanySkills ?? []).map((skill) => [skill.key, skill.name])); return (skillSnapshot?.desiredSkills ?? []).map((key) => namesByKey.get(key) ?? key); }, [overviewCompanySkills, skillSnapshot?.desiredSkills]); const mobileLiveRun = useMemo( () => (heartbeats ?? []).find((r) => r.status === "running" || r.status === "queued") ?? null, [heartbeats], ); useEffect(() => { if (!agent) return; if (!agentCompany) return; const companyMismatch = companyPrefix?.toUpperCase() !== agentCompany.issuePrefix.toUpperCase(); if (routeAgentRef !== canonicalAgentRef) { // Reuse the authorized response across the alias redirect. An empty // alias query would unmount the selected run and discard its mutations. // UUID lookups can return another accessible company's agent. Its alias // belongs only to that company's cache, even if the URL used another scope. queryClient.setQueryData( [...queryKeys.agents.detail(canonicalAgentRef), agent.companyId], agent, ); } if (urlRunId) { if (routeAgentRef !== canonicalAgentRef || companyMismatch) { navigate(`/${agentCompany.issuePrefix}/agents/${canonicalAgentRef}/runs/${urlRunId}`, { replace: true }); } return; } if (legacyAuditSection) return; const canonicalTab = activeView === "run-detail" ? "overview" : activeView; if (routeAgentRef !== canonicalAgentRef || urlTab !== canonicalTab || companyMismatch) { navigate(`/${agentCompany.issuePrefix}${agentDetailHref(canonicalAgentRef, canonicalTab)}`, { replace: true }); return; } }, [agent, agentCompany, companyPrefix, routeAgentRef, canonicalAgentRef, urlRunId, urlTab, activeView, legacyAuditSection, navigate, queryClient]); useEffect(() => { if (!agent?.companyId || agent.companyId === selectedCompanyId) return; // Let an explicit organization switch finish its navigation first. if (selectionSource === "manual") return; // The route owns selection until canonical navigation reaches this company. // Competing with Layout's old-prefix selection can cause an update loop. if (routeCompanyId && routeCompanyId !== agent.companyId) return; setSelectedCompanyId(agent.companyId, { source: "route_sync" }); }, [agent?.companyId, routeCompanyId, selectedCompanyId, selectionSource, setSelectedCompanyId]); // Invoke / pause / resume / terminate / duplicate / reset live in the shared // AgentActionButtons component. The detail header keeps only "approve" here, // which is surfaced via the pending-approval banner below. const agentAction = useMutation({ mutationFn: async (action: "approve") => { if (!agentLookupRef) return Promise.reject(new Error("No agent reference")); if (action === "approve") { return agentsApi.approve(agentLookupRef, resolvedCompanyId ?? undefined); } }, onSuccess: () => { 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) }); } } }, 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 updatePermissions = useMutation({ mutationFn: (permissions: AgentPermissionUpdate) => agentsApi.updatePermissions(agentLookupRef, permissions, 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: agentDetailHref(canonicalAgentRef) }); if (urlRunId) { crumbs.push({ label: "Runs", href: agent?.id ? agentScopedAuditHref(agent.id, "runs") : undefined }); crumbs.push({ label: `Run ${urlRunId.slice(0, 8)}` }); } else { const item = AGENT_DETAIL_NAVIGATION .flatMap((section) => section.items) .find((candidate) => candidate.value === activeView); crumbs.push({ label: item?.label ?? "Overview" }); } } setBreadcrumbs(crumbs); }, [setBreadcrumbs, agent, routeAgentRef, canonicalAgentRef, activeView, urlRunId]); useEffect(() => { closePanel(); return () => closePanel(); }, [closePanel]); useEffect(() => { if (!resolvedAgentId || agentMembershipState !== "joined") return; setDismissedLeftAgentIds((current) => { if (!current.has(resolvedAgentId)) return current; const next = new Set(current); next.delete(resolvedAgentId); return next; }); }, [resolvedAgentId, agentMembershipState]); useBeforeUnload( useCallback((event) => { if (!configDirty) return; event.preventDefault(); event.returnValue = ""; }, [configDirty]), ); useEffect(() => { if (!configDirty) return; function handleDocumentClick(event: MouseEvent) { if ( event.defaultPrevented || event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey ) { return; } const target = event.target; if (!(target instanceof Element)) return; const anchor = target.closest("a[href]"); if (!(anchor instanceof HTMLAnchorElement)) return; if (anchor.target && anchor.target !== "_self") return; const nextUrl = new URL(anchor.href, window.location.href); const currentUrl = new URL(window.location.href); if (nextUrl.origin !== currentUrl.origin) return; if ( nextUrl.pathname === currentUrl.pathname && nextUrl.search === currentUrl.search && nextUrl.hash === currentUrl.hash ) { return; } if (prepareAgentNavigation()) return; event.preventDefault(); event.stopPropagation(); } document.addEventListener("click", handleDocumentClick, true); return () => document.removeEventListener("click", handleDocumentClick, true); }, [configDirty, prepareAgentNavigation]); useEffect(() => { if (!configDirty) return; // BrowserRouter updates after popstate. Run first in the capture phase so a // rejected Back/Forward navigation can be restored before React Router // consumes it and unmounts the route-backed form. const currentEntry = { index: window.history.state?.idx, state: window.history.state, url: window.location.href, }; let restoring = false; function handlePopState(event: PopStateEvent) { if (restoring) { restoring = false; return; } if (prepareAgentNavigation()) return; event.stopImmediatePropagation(); restoring = restoreAgentConfigHistoryEntry(window.history, currentEntry, event.state?.idx); } window.addEventListener("popstate", handlePopState, true); return () => window.removeEventListener("popstate", handlePopState, true); }, [configDirty, prepareAgentNavigation]); if (isLoading) return ; if (error) return

{error.message}

; if (!agent) return null; if (!urlRunId && legacyAuditSection) { return ; } if (!urlRunId && !urlTab) { return ; } const isPendingApproval = agent.status === "pending_approval"; const hasInvalidOrgChain = agent.orgChainHealth?.status === "invalid_org_chain"; const pausedEscalationWarning = !hasInvalidOrgChain ? agent.orgChainHealth?.escalationWarning ?? null : null; const showConfigActionBar = ( activeView === "runtime" || activeView === "instructions" || activeView === "secrets" ); const showLeftAgentNotice = agentMembershipState === "left" && !dismissedLeftAgentIds.has(agent.id); const agentMembershipPending = membershipMutation.isPending && membershipMutation.variables?.resourceType === "agent" && membershipMutation.variables.resourceId === agent.id; const agentStarred = isStarred(membershipsQuery.data, "agent", agent.id); const agentStarPending = agentMembershipPending && membershipMutation.variables?.starred !== undefined; const agentJoinLeavePending = agentMembershipPending && membershipMutation.variables?.starred === undefined; return (
{showLeftAgentNotice ? (

You left this agent. It no longer appears in your sidebar.

membershipMutation.mutate({ resourceType: "agent", resourceId: agent.id, resourceName: agent.name, state: "joined", })} onLeave={() => membershipMutation.mutate({ resourceType: "agent", resourceId: agent.id, resourceName: agent.name, state: "left", })} />
) : null} {pausedEscalationWarning ? (

Escalation path is paused

{pausedEscalationWarning}

) : null} {hasInvalidOrgChain ? (

Invalid reporting chain

{agent.name} cannot accept tasks or start runs until its reporting chain is repaired.

{formatOrgChainHealthPath(agent)}

{agent.orgChainHealth?.repairGuidance ? (

{agent.orgChainHealth.repairGuidance}

) : (

Assign this agent to an active manager/root, or explicitly pause or terminate the affected agent/subtree.

)}
) : null} {/* Header */}

{agent.name}

{agent.adapterType === "claude_local" || agent.adapterType === "codex_local" ? : null} {getAdapterDisplay(agent.adapterType === "paperclip_runner" && agent.adapterConfig.provider === "openai_dot" ? "openai_dot" : agent.adapterType).label}· {agent.title || roleLabels[agent.role] || agent.role}
membershipMutation.mutate({ resourceType: "agent", resourceId: agent.id, resourceName: agent.name, starred: next, })} /> navigate("/agents/all", { replace: true })} hideTerminate={Boolean(builtInState)} pauseConfirm={ builtInState ? { title: `Pause the ${builtInState.definition.displayName}?`, description: ( <> {builtInFeatureLabel} depends on this agent. While paused,{" "} {builtInFeatureLabel.toLowerCase()} generation is skipped and the{" "} {builtInFeatureLabel} page shows a warning. ), } : undefined } > {mobileLiveRun && ( Live )}
{builtInState && ( resetBuiltIn.mutate()} disabled={resetBuiltIn.isPending} > {resetBuiltIn.isPending ? "Resetting…" : "Reset to defaults"} } > Ships with Paperclip and powers {builtInFeatureLabel}. Configure it like any agent — model, instructions, budget. It can be paused but not deleted; pausing it pauses {builtInFeatureLabel}. )} {builtInState?.definition.bundle && ( setShowBuiltInConfigure(true)} onResetResource={(kind) => resetBuiltInResource.mutate(kind)} onRunRoutine={(routineKey) => runBuiltInRoutine.mutate(routineKey)} onEnableSchedule={(routineKey) => enableBuiltInSchedule.mutate(routineKey)} onDisableSchedule={(routineKey) => disableBuiltInSchedule.mutate(routineKey)} resettingResource={resetBuiltInResource.isPending ? resetBuiltInResource.variables ?? null : null} routineActionPending={builtInRoutineActionPending} /> )} {builtInState && resolvedCompanyId && ( { setShowBuiltInConfigure(false); invalidateBuiltIn(); }} /> )} {actionError &&

{actionError}

} {isPendingApproval && (
This agent is pending board approval and cannot be invoked yet.
)} {activeView !== "run-detail" && activeView !== "channels" &&

{activeView === "secrets" ? "Secrets & variables" : AGENT_DETAIL_NAVIGATION.flatMap(section => section.items).find(item => item.value === activeView)?.label}

} {/* View content */} {activeView === "overview" && ( )} {activeView === "instructions" && (
{resolvedCompanyId && }
)} {activeView === "runtime" && (
)} {activeView === "secrets" && (
)} {activeView === "skills" && ( )} {activeView === "tools" && resolvedCompanyId && ( )} {activeView === "channels" && resolvedCompanyId && ( )} {activeView === "permissions" && (
)} {activeView === "api-keys" && (
)} {activeView === "revisions" && ( )} {activeView === "run-detail" && ( )} {showConfigActionBar &&

{configSaving ? "Saving changes…" : configDirty ? "You have unsaved changes." : ""}

}
); } /* ---- Helper components ---- */ function SummaryRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } export type LatestRunIssue = { id: string; title: string; status: string; identifier?: string | null }; /** * The id of the issue a run works on, read from its context snapshot. Newer * snapshots use `issueId`; older ones use `taskId`. Returns undefined for pure * timer heartbeats that carry no task reference. */ export function getRunSnapshotIssueId( run: Pick, ): string | undefined { const ctx = run.contextSnapshot as Record | null; const issueId = ctx?.issueId ?? ctx?.taskId; return issueId ? String(issueId) : undefined; } /** * Resolve the Live Run section's two navigation destinations and the task (if * any) the run works on. The run→task link lives in the run's context snapshot * (`issueId`, falling back to `taskId` for older snapshots); the `HeartbeatRun` * itself doesn't carry the issue id. The heading always links to the run detail * page; the running row links to the task detail page when the snapshot resolves * to a known issue, otherwise falls back to the run detail page (pure timer * heartbeats or an issue that can't be resolved). */ export function resolveLatestRunNavigation( run: Pick, agentId: string, issuesById: Map, ): { task: LatestRunIssue | undefined; runHref: string; rowHref: string } { const issueId = getRunSnapshotIssueId(run); const task = issueId ? issuesById.get(issueId) : undefined; const runHref = `/agents/${agentId}/runs/${run.id}`; const rowHref = task ? `/issues/${task.identifier ?? task.id}` : runHref; return { task, runHref, rowHref }; } function LatestRunCard({ runs, agentId, issuesById, }: { runs: HeartbeatRun[]; agentId: string; issuesById: Map; }) { const sorted = useMemo( () => [...runs].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ), [runs] ); const liveRun = sorted.find((r) => r.status === "running" || r.status === "queued"); const run = liveRun ?? sorted[0]; // The assigned-issues list this card resolves against is bounded (server page // limit), so a live run can reference a valid issue that isn't on the loaded // page. When the snapshot points at an issue we don't already have, fetch it // directly so the running row always links to the task rather than falling // back to run metadata. `enabled` keeps this a no-op for the common case. const snapshotIssueId = run ? getRunSnapshotIssueId(run) : undefined; const needsFallbackFetch = !!snapshotIssueId && !issuesById.has(snapshotIssueId); const { data: fallbackIssue } = useQuery({ queryKey: queryKeys.issues.detail(snapshotIssueId ?? "__none__"), queryFn: () => issuesApi.get(snapshotIssueId as string), enabled: needsFallbackFetch, staleTime: 30_000, }); const summaryRaw = run ? run.resultJson ? String((run.resultJson as Record).summary ?? (run.resultJson as Record).result ?? "") : run.error ?? "" : ""; // Extract a clean 2-3 line excerpt: first non-empty, non-header, non-list-mark lines const summary = useMemo(() => { if (!summaryRaw) return ""; const lines = summaryRaw .replace(/^#{1,6}\s+/gm, "") .split("\n") .map((l) => l.trim()) .filter((l) => l.length > 0 && !l.startsWith("---") && !l.startsWith("|") && !l.startsWith("```") && !/^[-*>]/.test(l) && !/^\d+\./.test(l)); const excerpt: string[] = []; let chars = 0; for (const line of lines) { if (excerpt.length >= 3 || chars + line.length > 280) break; excerpt.push(line); chars += line.length; } return excerpt.join(" "); }, [summaryRaw]); if (!run) return null; const isLive = run.status === "running" || run.status === "queued"; // Fold any directly-fetched fallback issue into the lookup, keyed by the same // snapshot id used to resolve the row so it hits regardless of id-vs-slug. const effectiveIssuesById = fallbackIssue && snapshotIssueId ? new Map(issuesById).set(snapshotIssueId, { id: fallbackIssue.id, title: fallbackIssue.title, status: fallbackIssue.status, identifier: fallbackIssue.identifier, }) : issuesById; const { task, runHref, rowHref } = resolveLatestRunNavigation(run, agentId, effectiveIssuesById); const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" }; const StatusIcon = statusInfo.icon; return (

{isLive && ( )} {isLive ? "Live Run" : "Latest Run"} · {run.id.slice(0, 8)}

{task ? ( <> {task.identifier ?? task.id.slice(0, 8)} {task.title} ) : ( <> {run.id.slice(0, 8)} {sourceLabels[run.invocationSource] ?? run.invocationSource} )} {relativeTime(run.createdAt)}
{summary && (
{summary}
)}
); } /* ---- Agent Overview ---- */ export function AgentOverview({ agent, runs, assignedIssues, runtimeState, reportsToAgent, directReportCount, skillNames, agentRouteId, }: { agent: AgentDetailRecord; runs: HeartbeatRun[]; assignedIssues: Issue[]; runtimeState?: AgentRuntimeState; reportsToAgent?: Agent; directReportCount: number; skillNames: string[]; agentRouteId: string; }) { const issuesById = useMemo(() => { const map = new Map(); for (const issue of assignedIssues) map.set(issue.id, issue); return map; }, [assignedIssues]); const configuredModel = asNonEmptyString(agent.adapterConfig?.model) ?? asNonEmptyString(agent.adapterConfig?.modelName) ?? asNonEmptyString(agent.runtimeConfig?.model) ?? "Adapter default"; const lastRun = runs[0] ?? null; const identity = useQuery({ queryKey: [...queryKeys.agents.identity(agent.id), lastRun?.id, lastRun?.status], queryFn: () => agentsApi.getIdentity(agent.id, agent.companyId), }); return (

Identity

{roleLabels[agent.role] ?? agent.role} {agent.title ?? "Not set"} {reportsToAgent ? ( {reportsToAgent.name} ) : Board} {directReportCount} {identity.isPending ? Loading… : identity.isError ? Could not load public key : identity.data ? ( {identity.data.keyId.slice(0, 19)}… ) : (
Not created yet
Created automatically on the next managed run
)}

Harness / Runtime

Configure
{getAdapterDisplay(agent.adapterType === "paperclip_runner" && agent.adapterConfig.provider === "openai_dot" ? "openai_dot" : agent.adapterType).label} {configuredModel} {runtimeState?.sessionDisplayId ?? runtimeState?.sessionId ?? "No session"} {lastRun ? `${lastRun.status} · ${relativeTime(lastRun.createdAt)}` : "No runs"}

Capabilities

{agent.capabilities?.trim() ? ( {agent.capabilities} ) : (

No capability summary has been added.

)}

Skills

Manage
{skillNames.length > 0 ? (
{skillNames.slice(0, 8).map((skill) => {skill})} {skillNames.length > 8 ? +{skillNames.length - 8} more : null}
) : (

No skills enabled.

)}

Recent Tasks

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

No recent tasks.

) : (
{assignedIssues.slice(0, 6).map((issue) => ( {relativeTime(issue.updatedAt)}} showDivider /> ))} {assignedIssues.length > 6 && (
+{assignedIssues.length - 6} more tasks
)}
)}
{(["activity", "runs", "costs", "budgets"] as const).map((section) => ( {section} ))}
); } /* ---- Agent Configure Page ---- */ /** * Agent detail URLs use a name-derived key, so updates that change the agent's * name (a rename or a config-revision rollback) can invalidate the reference * currently in the URL. When that happens, refetching the old reference would * 404 with "Agent not found". Instead, drop the stale cached queries and * replace the URL with the new canonical reference. Returns true when a * redirect happened. */ export function syncAgentRouteAfterRename( queryClient: QueryClient, navigate: NavigateFunction, previous: { id: string; urlKey?: string | null; name?: string | null }, updated: { id: string; urlKey?: string | null; name?: string | null }, tab: string, ): boolean { const previousRef = agentRouteRef(previous); const nextRef = agentRouteRef(updated); if (nextRef === previousRef) return false; queryClient.removeQueries({ queryKey: queryKeys.agents.detail(previousRef) }); navigate(`/agents/${nextRef}/${tab}`, { replace: true }); return true; } export function AgentRevisionsTab({ agent, companyId, }: { agent: AgentDetailRecord; companyId?: string; }) { const queryClient = useQueryClient(); const navigate = useNavigate(); 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: (updated) => { queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.configRevisions(agent.id) }); if (!syncAgentRouteAfterRename(queryClient, navigate, agent, updated, "revisions")) { queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.urlKey) }); } }, }); return (

Configuration Revisions

{configRevisions?.length ?? 0} total
{(configRevisions ?? []).length === 0 ? (

No configuration revisions yet.

) : (
{(configRevisions ?? []).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 ---- */ export function ConfigurationTab({ agent, companyId, onDirtyChange, onSaveActionChange, onCancelActionChange, onSavingChange, updatePermissions, hidePromptTemplate, hideInstructionsFile, content = "runtime", canConfigureProviderTrace = false, }: { agent: AgentDetailRecord; companyId?: string; onDirtyChange: (dirty: boolean) => void; onSaveActionChange: (save: (() => void) | null) => void; onCancelActionChange: (cancel: (() => void) | null) => void; onSavingChange: (saving: boolean) => void; updatePermissions: { mutate: (permissions: AgentPermissionUpdate) => void; isPending: boolean }; hidePromptTemplate?: boolean; hideInstructionsFile?: boolean; content?: "runtime" | "permissions" | "secrets"; canConfigureProviderTrace?: boolean; }) { const queryClient = useQueryClient(); const navigate = useNavigate(); const { tab: urlTab } = useParams<{ tab?: string }>(); const { pushToast } = useToastActions(); const [awaitingRefreshAfterSave, setAwaitingRefreshAfterSave] = useState(false); const lastAgentRef = useRef(agent); const catalogProvider = agent.adapterType === "paperclip_runner" ? String(agent.adapterConfig.provider ?? "codex") : undefined; const { data: adapterModels } = useQuery({ queryKey: companyId ? queryKeys.agents.adapterModels(companyId, agent.adapterType, null, catalogProvider) : ["agents", "none", "adapter-models", agent.adapterType], queryFn: () => agentsApi.adapterModels(companyId!, agent.adapterType, { provider: catalogProvider }), enabled: Boolean(companyId) && content === "runtime", }); const lowTrustSelected = getTrustPreset(agent.permissions) === "low_trust_review"; const { data: boundaryProjects, isLoading: boundaryProjectsLoading } = useQuery({ queryKey: companyId ? queryKeys.projects.list(companyId) : ["projects", "__low-trust-disabled"], queryFn: () => projectsApi.list(companyId!), enabled: Boolean(companyId && lowTrustSelected) && content === "permissions", }); const { data: boundaryIssues, isLoading: boundaryIssuesLoading } = useQuery({ queryKey: companyId ? [...queryKeys.issues.list(companyId), "low-trust-boundary-candidates"] : ["issues", "__low-trust-disabled"], queryFn: () => issuesApi.list(companyId!, { limit: 100, sortField: "updated", sortDir: "desc" }), enabled: Boolean(companyId && lowTrustSelected) && content === "permissions", }); const updateAgent = useMutation({ mutationFn: (data: Record) => agentsApi.update(agent.id, data, companyId), onMutate: () => { setAwaitingRefreshAfterSave(true); }, onSuccess: (updated) => { queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.configRevisions(agent.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(agent.companyId) }); if (!syncAgentRouteAfterRename(queryClient, navigate, agent, updated, urlTab ?? content)) { queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.urlKey) }); } pushToast({ title: "Agent saved", tone: "success" }); }, onError: (err) => { setAwaitingRefreshAfterSave(false); const message = err instanceof ApiError ? err.message : err instanceof Error ? err.message : "Could not save agent"; pushToast({ title: "Save failed", body: message, tone: "error" }); }, }); useEffect(() => { if (awaitingRefreshAfterSave && agent !== lastAgentRef.current) { setAwaitingRefreshAfterSave(false); } lastAgentRef.current = agent; }, [agent, awaitingRefreshAfterSave]); const isConfigSaving = content !== "permissions" && (updateAgent.isPending || awaitingRefreshAfterSave); useEffect(() => { onSavingChange(isConfigSaving); }, [onSavingChange, isConfigSaving]); useEffect(() => { if (content !== "permissions") return; onDirtyChange(false); onSaveActionChange(null); onCancelActionChange(null); }, [content, onCancelActionChange, onDirtyChange, onSaveActionChange]); const canCreateAgents = Boolean(agent.permissions?.canCreateAgents); const canCreateSkills = agent.permissions?.canCreateSkills !== false; const canAssignTasks = Boolean(agent.access?.canAssignTasks); const taskAssignSource = agent.access?.taskAssignSource ?? "none"; const taskAssignLocked = agent.role === "ceo" || canCreateAgents; const taskAssignHint = taskAssignSource === "ceo_role" ? "Enabled automatically for CEO agents." : taskAssignSource === "agent_creator" ? "Enabled automatically while this agent can create new agents." : taskAssignSource === "explicit_grant" ? "Enabled via explicit organization permission grant." : taskAssignSource === "simple_default" ? "Enabled by simple organization-wide task assignment defaults." : "Disabled unless explicitly granted."; return (
{content !== "permissions" ? updateAgent.mutateAsync(patch)} isSaving={isConfigSaving} adapterModels={adapterModels} onDirtyChange={onDirtyChange} onSaveActionChange={onSaveActionChange} onCancelActionChange={onCancelActionChange} hideInlineSave hidePromptTemplate={hidePromptTemplate} hideInstructionsFile={hideInstructionsFile} content={content === "runtime" ? "configuration" : "secrets"} sectionLayout="cards" environmentVariablesPlacement="configuration" compactTestFeedback sectionOrder={["identity", "adapter", "configuration", "environment", "environment-variables", "run-policy"]} sectionTitles={{ adapter: "Adapter", configuration: "Configuration", identity: "Agent identity" }} canConfigureProviderTrace={canConfigureProviderTrace} /> : null} {content === "permissions" ? ({ id: project.id, label: project.name, }))} issueCandidates={(boundaryIssues ?? []).map((issue) => ({ id: issue.id, label: `${issue.identifier ?? issue.id.slice(0, 8)} · ${issue.title}`, }))} candidatesLoading={boundaryProjectsLoading || boundaryIssuesLoading} onChange={(nextPermissions) => updatePermissions.mutate({ canCreateAgents, canCreateSkills, canAssignTasks, ...buildPermissionsForTrustPreset(nextPermissions, nextPermissions.trustPreset === "low_trust_review" ? "low_trust_review" : "standard"), }) } /> : null} {content === "permissions" ?

Permissions

Can create new agents

Lets this agent create or hire agents. This also grants task assignment authority.

updatePermissions.mutate({ canCreateAgents: !canCreateAgents, canCreateSkills, canAssignTasks: !canCreateAgents ? true : canAssignTasks, }) } disabled={updatePermissions.isPending} />
Can create/import skills

Lets this agent install, import, create, and scan organization skills without creating agents.

updatePermissions.mutate({ canCreateAgents, canCreateSkills: !canCreateSkills, canAssignTasks, }) } disabled={updatePermissions.isPending} />
Can assign tasks

{taskAssignHint}

updatePermissions.mutate({ canCreateAgents, canCreateSkills, canAssignTasks: !canAssignTasks, }) } disabled={updatePermissions.isPending || taskAssignLocked} />
: null}
); } /* ---- Prompts Tab ---- */ export function PromptsTab({ agent, companyId, onDirtyChange, onSaveActionChange, onCancelActionChange, onSavingChange, showSaveNotice = true, }: { showSaveNotice?: boolean; agent: Agent; companyId?: string; onDirtyChange: (dirty: boolean) => void; onSaveActionChange: (save: (() => void) | null) => void; onCancelActionChange: (cancel: (() => void) | null) => void; onSavingChange: (saving: boolean) => void; }) { const queryClient = useQueryClient(); const { selectedCompanyId } = useCompany(); const { isMobile } = useSidebar(); const [selectedFile, setSelectedFileState] = useState("AGENTS.md"); const [instructionMode, setInstructionMode] = useState<"read" | "edit" | "raw">("read"); const [showFilePanel, setShowFilePanel] = useState(false); const [draft, setDraft] = useState(null); const draftBaseRevisionRef = useRef(undefined); const draftBaseHashRef = useRef(undefined); const [candidateRunId, setCandidateRunId] = useState(null); const [readOnlyCandidateRunId, setReadOnlyCandidateRunId] = useState(null); const candidateAgentRef = useRef(agent.id); candidateAgentRef.current = agent.id; const [bundleDraft, setBundleDraft] = useState<{ mode: "managed" | "external"; rootPath: string; entryFile: string; } | null>(null); const [newFilePath, setNewFilePath] = useState(""); const [showNewFileInput, setShowNewFileInput] = useState(false); const [pendingFiles, setPendingFiles] = useState([]); const [expandedDirs, setExpandedDirs] = useState>(new Set()); const [filePanelWidth, setFilePanelWidth] = useState(260); const [instructionPaneWidth, setInstructionPaneWidth] = useState(null); const containerRef = useRef(null); const [awaitingRefresh, setAwaitingRefresh] = useState(false); const lastFileVersionRef = useRef(null); const externalBundleRef = useRef<{ rootPath: string; entryFile: string; selectedFile: string; } | null>(null); // MDXEditor can normalize markdown and emit onChange while it mounts. Only // treat editor output as a draft after a real interaction so merely opening // an instructions file cannot mark the agent dirty. const editorInteractedRef = useRef(false); const markEditorInteracted = useCallback(() => { editorInteractedRef.current = true; }, []); const setSelectedFile = useCallback((filePath: string) => { editorInteractedRef.current = false; draftBaseRevisionRef.current = undefined; draftBaseHashRef.current = undefined; setCandidateRunId(null); setSelectedFileState(filePath); }, []); useEffect(() => { editorInteractedRef.current = false; setSelectedFile("AGENTS.md"); setInstructionMode("read"); setShowFilePanel(false); setDraft(null); setReadOnlyCandidateRunId(null); setBundleDraft(null); setNewFilePath(""); setShowNewFileInput(false); setPendingFiles([]); setExpandedDirs(new Set()); setAwaitingRefresh(false); lastFileVersionRef.current = null; externalBundleRef.current = null; }, [agent.id]); const getCapabilities = useAdapterCapabilities(); const isLocal = getCapabilities(agent.adapterType).supportsInstructionsBundle; const { data: bundle, isLoading: bundleLoading } = useQuery({ queryKey: queryKeys.agents.instructionsBundle(agent.id), queryFn: () => agentsApi.instructionsBundle(agent.id, companyId), enabled: Boolean(companyId && isLocal), refetchInterval: draft === null ? 5000 : false, }); const persistedMode = bundle?.mode ?? "managed"; const persistedRootPath = persistedMode === "managed" ? (bundle?.managedRootPath ?? bundle?.rootPath ?? "") : (bundle?.rootPath ?? ""); const currentMode = bundleDraft?.mode ?? persistedMode; const currentEntryFile = bundleDraft?.entryFile ?? bundle?.entryFile ?? "AGENTS.md"; const currentRootPath = bundleDraft?.rootPath ?? persistedRootPath; const fileOptions = useMemo( () => bundle?.files.map((file) => file.path) ?? [], [bundle], ); const bundleMatchesDraft = Boolean( bundle && currentMode === persistedMode && currentEntryFile === bundle.entryFile && currentRootPath === persistedRootPath, ); const visibleFilePaths = useMemo( () => bundleMatchesDraft ? [...new Set([currentEntryFile, ...fileOptions, ...pendingFiles])] : [currentEntryFile, ...pendingFiles], [bundleMatchesDraft, currentEntryFile, fileOptions, pendingFiles], ); const fileTree = useMemo( () => buildFileTree(Object.fromEntries(visibleFilePaths.map((filePath) => [filePath, ""]))), [visibleFilePaths], ); const selectedOrEntryFile = selectedFile || currentEntryFile; const selectedFileExists = bundleMatchesDraft && fileOptions.includes(selectedOrEntryFile); const selectedFileSummary = bundle?.files.find((file) => file.path === selectedOrEntryFile) ?? null; const { data: selectedFileDetail, isLoading: fileLoading, error: fileError } = useQuery({ queryKey: queryKeys.agents.instructionsFile(agent.id, selectedOrEntryFile), queryFn: () => agentsApi.instructionsFile(agent.id, selectedOrEntryFile, companyId), enabled: Boolean(companyId && isLocal && selectedFileExists), refetchInterval: draft === null ? 5000 : false, }); const candidates = useQuery({ queryKey: queryKeys.agents.instructionCandidates(agent.id), queryFn: () => agentsApi.instructionCandidates(agent.id, companyId), enabled: Boolean(companyId && isLocal && currentMode === "managed"), }); // Whole-folder failures belong to their run's diagnostics. They have no // preserved edits to resolve and do not describe the current saved files. const preservedCandidates = candidates.data?.filter((candidate) => candidate.contract !== "agent_files") ?? []; const loadCandidate = useMutation({ mutationFn: async (candidate: AgentInstructionCandidate) => { if (candidate.content === null) throw new Error("These instruction edits have not been retrieved yet."); const file = await agentsApi.instructionsFile(agent.id, candidate.entryFile, companyId).catch((error) => { if (error instanceof ApiError && error.status === 404 && candidate.baseRevisionId === null) return null; throw error; }); return { candidate, file, agentId: agent.id }; }, onSuccess: ({ candidate, file, agentId: requestedAgentId }) => { if (candidateAgentRef.current !== requestedAgentId) return; setSelectedFile(candidate.entryFile); if (file) queryClient.setQueryData(queryKeys.agents.instructionsFile(agent.id, candidate.entryFile), file); draftBaseRevisionRef.current = file?.revision?.id ?? null; setCandidateRunId(candidate.runId); setDraft(candidate.content); setInstructionMode("edit"); }, }); const resolveCandidate = useMutation({ mutationFn: async (data: { runId: string; content: string; baseRevisionId: string | null }) => ({ file: await agentsApi.resolveInstructionCandidate(agent.id, data.runId, { content: data.content, baseRevisionId: data.baseRevisionId }, companyId), agentId: agent.id, }), onSuccess: ({ file, agentId: requestedAgentId }) => { if (candidateAgentRef.current !== requestedAgentId) return; setDraft(null); setCandidateRunId(null); draftBaseRevisionRef.current = undefined; draftBaseHashRef.current = undefined; queryClient.setQueryData(queryKeys.agents.instructionsFile(agent.id, file.path), file); queryClient.invalidateQueries({ queryKey: queryKeys.agents.instructionsBundle(agent.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.instructionCandidates(agent.id) }); }, }); const refreshCandidateBase = useMutation({ mutationFn: async () => ({ file: await agentsApi.instructionsFile(agent.id, selectedOrEntryFile, companyId), agentId: agent.id, runId: candidateRunId, }), onSuccess: ({ file, agentId: requestedAgentId, runId }) => { if (candidateAgentRef.current !== requestedAgentId || candidateRunId !== runId) return; draftBaseRevisionRef.current = file.revision?.id ?? null; queryClient.setQueryData(queryKeys.agents.instructionsFile(agent.id, file.path), file); resolveCandidate.reset(); }, }); const updateBundle = useMutation({ mutationFn: (data: { mode?: "managed" | "external"; rootPath?: string | null; entryFile?: string; clearLegacyPromptTemplate?: boolean; }) => agentsApi.updateInstructionsBundle(agent.id, data, companyId), onMutate: () => { editorInteractedRef.current = false; }, onSuccess: () => { setAwaitingRefresh(true); queryClient.invalidateQueries({ queryKey: queryKeys.agents.instructionsBundle(agent.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.urlKey) }); }, onError: () => setAwaitingRefresh(false), }); const saveFile = useMutation({ mutationFn: (data: { path: string; content: string; baseRevisionId?: string | null; baseHash?: string | null; clearLegacyPromptTemplate?: boolean }) => agentsApi.saveInstructionsFile(agent.id, data, companyId), onMutate: () => { editorInteractedRef.current = false; }, onSuccess: (file, variables) => { setDraft(null); draftBaseRevisionRef.current = undefined; draftBaseHashRef.current = undefined; queryClient.setQueryData(queryKeys.agents.instructionsFile(agent.id, variables.path), file); // Keep the selected file present while the refreshed bundle is in flight. // Otherwise removing its pending placeholder briefly selects AGENTS.md. queryClient.setQueryData(queryKeys.agents.instructionsBundle(agent.id), previous => previous ? { ...previous, files: [...previous.files.filter(item => item.path !== file.path), { path: file.path, size: file.size, language: file.language, markdown: file.markdown, isEntryFile: file.isEntryFile, editable: file.editable, deprecated: file.deprecated, virtual: file.virtual, binary: file.binary, contentHash: file.contentHash, }], } : previous); setPendingFiles((prev) => prev.filter((f) => f !== variables.path)); queryClient.invalidateQueries({ queryKey: queryKeys.agents.instructionsBundle(agent.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.instructionsFile(agent.id, variables.path) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.urlKey) }); }, onError: () => setAwaitingRefresh(false), }); const deleteFile = useMutation({ mutationFn: (relativePath: string) => agentsApi.deleteInstructionsFile(agent.id, relativePath, companyId, bundle?.files.find(file => file.path === relativePath)?.contentHash), onMutate: () => { editorInteractedRef.current = false; setAwaitingRefresh(true); }, onSuccess: (_, relativePath) => { queryClient.invalidateQueries({ queryKey: queryKeys.agents.instructionsBundle(agent.id) }); queryClient.removeQueries({ queryKey: queryKeys.agents.instructionsFile(agent.id, relativePath) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }); queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.urlKey) }); }, onError: () => setAwaitingRefresh(false), }); const uploadMarkdownImage = useMutation({ mutationFn: async ({ file, namespace }: { file: File; namespace: string }) => { if (!selectedCompanyId) throw new Error("Select an organization to upload images"); return assetsApi.uploadImage(selectedCompanyId, file, namespace); }, }); useEffect(() => { if (!bundle) return; if (!bundleMatchesDraft) { if (selectedFile !== currentEntryFile) setSelectedFile(currentEntryFile); return; } const availablePaths = bundle.files.map((file) => file.path); if (availablePaths.length === 0) { if (selectedFile !== bundle.entryFile) setSelectedFile(bundle.entryFile); return; } if (!availablePaths.includes(selectedFile) && selectedFile !== currentEntryFile && !pendingFiles.includes(selectedFile)) { setSelectedFile(availablePaths.includes(bundle.entryFile) ? bundle.entryFile : availablePaths[0]!); } }, [bundle, bundleMatchesDraft, currentEntryFile, pendingFiles, selectedFile]); useEffect(() => { const nextExpanded = new Set(); for (const filePath of visibleFilePaths) { const parts = filePath.split("/"); let currentPath = ""; for (let i = 0; i < parts.length - 1; i++) { currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i]!; nextExpanded.add(currentPath); } } setExpandedDirs((current) => (setsEqual(current, nextExpanded) ? current : nextExpanded)); }, [visibleFilePaths]); useEffect(() => { if (isMobile) { setInstructionPaneWidth(null); return; } const element = containerRef.current; if (!element) return; const updateWidth = () => setInstructionPaneWidth(element.getBoundingClientRect().width); updateWidth(); if (typeof ResizeObserver === "undefined") return; const observer = new ResizeObserver((entries) => { const entry = entries[0]; if (entry) setInstructionPaneWidth(entry.contentRect.width); }); observer.observe(element); return () => observer.disconnect(); }, [bundleLoading, isMobile, visibleFilePaths.length]); useEffect(() => { const versionKey = selectedFileExists && selectedFileDetail ? `${selectedFileDetail.path}:${selectedFileDetail.content}` : `draft:${currentMode}:${currentRootPath}:${selectedOrEntryFile}`; if (awaitingRefresh) { setAwaitingRefresh(false); setBundleDraft(null); setDraft(null); lastFileVersionRef.current = versionKey; return; } if (lastFileVersionRef.current !== versionKey) { if (draftBaseRevisionRef.current === undefined) setDraft(null); lastFileVersionRef.current = versionKey; } }, [awaitingRefresh, currentMode, currentRootPath, selectedFileDetail, selectedFileExists, selectedOrEntryFile]); useEffect(() => { if (!bundle) return; setBundleDraft((current) => { if (current) return current; return { mode: persistedMode, rootPath: persistedRootPath, entryFile: bundle.entryFile, }; }); }, [bundle, persistedMode, persistedRootPath]); useEffect(() => { if (!bundle || currentMode !== "external") return; externalBundleRef.current = { rootPath: currentRootPath, entryFile: currentEntryFile, selectedFile: selectedOrEntryFile, }; }, [bundle, currentEntryFile, currentMode, currentRootPath, selectedOrEntryFile]); const currentContent = selectedFileExists ? (selectedFileDetail?.content ?? "") : ""; const displayValue = draft ?? currentContent; const useMarkdownEditor = shouldUseMarkdownInstructionsEditor({ selectedFileExists, selectedPath: selectedOrEntryFile, detail: selectedFileDetail, summary: selectedFileSummary, }); const bundleDirty = Boolean( bundleDraft && ( bundleDraft.mode !== persistedMode || bundleDraft.rootPath !== persistedRootPath || bundleDraft.entryFile !== (bundle?.entryFile ?? "AGENTS.md") ), ); const fileDirty = draft !== null && draft !== currentContent; const isDirty = bundleDirty || fileDirty || candidateRunId !== null; const isSaving = updateBundle.isPending || saveFile.isPending || resolveCandidate.isPending || loadCandidate.isPending || refreshCandidateBase.isPending || deleteFile.isPending || awaitingRefresh; useEffect(() => { onSavingChange(isSaving); }, [onSavingChange, isSaving]); useEffect(() => { onDirtyChange(isDirty); }, [onDirtyChange, isDirty]); useEffect(() => () => { onSaveActionChange(null); onCancelActionChange(null); onDirtyChange(false); onSavingChange(false); }, [onCancelActionChange, onDirtyChange, onSaveActionChange, onSavingChange]); useEffect(() => { onSaveActionChange(isDirty ? () => { const save = async () => { const shouldClearLegacy = Boolean(bundle?.legacyPromptTemplateActive) || Boolean(bundle?.legacyBootstrapPromptTemplateActive); if (bundleDirty && bundleDraft) { await updateBundle.mutateAsync({ mode: bundleDraft.mode, rootPath: bundleDraft.mode === "external" ? bundleDraft.rootPath : null, entryFile: bundleDraft.entryFile, }); } if (candidateRunId) { await resolveCandidate.mutateAsync({ runId: candidateRunId, content: displayValue, baseRevisionId: draftBaseRevisionRef.current ?? null }); } else if (fileDirty) { await saveFile.mutateAsync({ path: selectedOrEntryFile, content: displayValue, ...(bundle?.persistence === "agent_files" ? { baseHash: draftBaseHashRef.current !== undefined ? draftBaseHashRef.current : selectedFileDetail?.contentHash ?? null } : {}), ...(selectedOrEntryFile === currentEntryFile && currentMode === "managed" ? { baseRevisionId: draftBaseRevisionRef.current !== undefined ? draftBaseRevisionRef.current : selectedFileDetail?.revision?.id ?? null } : {}), clearLegacyPromptTemplate: shouldClearLegacy, }); } }; void save().catch(() => undefined); } : null); }, [ bundle, bundleDirty, bundleDraft, candidateRunId, resolveCandidate, displayValue, fileDirty, isDirty, onSaveActionChange, selectedFileDetail?.revision?.id, currentEntryFile, currentMode, saveFile, selectedOrEntryFile, updateBundle, ]); useEffect(() => { onCancelActionChange(isDirty ? () => { draftBaseRevisionRef.current = undefined; draftBaseHashRef.current = undefined; setCandidateRunId(null); resolveCandidate.reset(); setDraft(null); if (bundle) { setBundleDraft({ mode: persistedMode, rootPath: persistedRootPath, entryFile: bundle.entryFile, }); } } : null); }, [bundle, isDirty, onCancelActionChange, persistedMode, persistedRootPath, resolveCandidate]); const handleSeparatorDrag = useCallback((event: React.MouseEvent) => { event.preventDefault(); const startX = event.clientX; const startWidth = filePanelWidth; const onMouseMove = (moveEvent: MouseEvent) => { const delta = moveEvent.clientX - startX; const next = Math.max(180, Math.min(500, startWidth + delta)); setFilePanelWidth(next); }; const onMouseUp = () => { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }, [filePanelWidth]); const instructionsSideBySide = !isMobile && instructionPaneWidth !== null && instructionPaneWidth >= filePanelWidth + 520; if (!isLocal) { return (

Instructions bundles are only available for local adapters.

); } if (bundleLoading && !bundle) { return ; } return (
{(bundle?.warnings ?? []).length > 0 && (
{(bundle?.warnings ?? []).map((warning) => (
{warning}
))}
)} {showSaveNotice &&

Saved instructions affect the next run. Active runs keep the instructions they started with, and instruction changes may start a fresh adapter session.

} Advanced

Files

{!showNewFileInput && ( )} {isMobile && ( )}
{showNewFileInput && (
setNewFilePath(event.target.value)} placeholder="TOOLS.md" className="font-mono text-sm" autoFocus onKeyDown={(event) => { if (event.key === "Escape") { setShowNewFileInput(false); setNewFilePath(""); } }} />
)} setExpandedDirs((current) => { const next = new Set(current); if (next.has(dirPath)) next.delete(dirPath); else next.add(dirPath); return next; })} onSelectFile={(filePath) => { setSelectedFile(filePath); setInstructionMode("read"); if (!fileOptions.includes(filePath)) setDraft(""); if (isMobile) setShowFilePanel(false); }} onToggleCheck={() => {}} showCheckboxes={false} wrapLabels renderFileExtra={(node) => { const file = bundle?.files.find((entry) => entry.path === node.path); if (!file) return null; if (file.deprecated) { return ( virtual file Legacy inline prompt — this deprecated virtual file preserves the old promptTemplate content ); } return ( {file.isEntryFile ? "entry" : `${file.size}b`} ); }} />
{/* Draggable separator */} {instructionsSideBySide && (
)}
{isMobile && ( )}

{selectedOrEntryFile}

{selectedFileExists ? selectedFileSummary?.deprecated ? "Deprecated virtual file" : `${selectedFileDetail?.language ?? "text"} file` : "New file in this bundle"}

{(["read", "edit", "raw"] as const).map((mode) => ( ))}
{!fileLoading && ( )} {selectedFileExists && !selectedFileSummary?.deprecated && selectedOrEntryFile !== currentEntryFile && ( )}
{currentMode === "managed" && preservedCandidates.length > 0 && (

Preserved instruction edits

Older instruction-only sessions have edits to review.

{preservedCandidates.map((candidate) => (
{candidate.runId.slice(0, 8)} {candidate.entryFile} · {formatDate(candidate.createdAt)} {candidate.errorMessage &&

{candidate.errorMessage}

} {candidate.entryFile !== currentEntryFile &&

The instruction entry changed. These edits remain preserved for the original file.

} {candidate.entryFile !== currentEntryFile && candidate.content !== null && readOnlyCandidateRunId === candidate.runId && (

Read only: {candidate.entryFile}. To keep any of these edits in {currentEntryFile}, copy them and edit the current entry explicitly.

Copy preserved edits
{candidate.content}
)}
))}
)} {candidateRunId &&

Reviewing preserved edits. Save to apply your resolved draft and close this preserved edit.

Compare current instructions
{currentContent}
{draftBaseRevisionRef.current !== (selectedFileDetail?.revision?.id ?? null) &&

The current instructions changed after this draft was loaded. Refresh the current revision, compare the instructions, and save your resolved draft again.

} {(resolveCandidate.error || draftBaseRevisionRef.current !== (selectedFileDetail?.revision?.id ?? null)) && }
} {(candidates.error || loadCandidate.error || resolveCandidate.error || refreshCandidateBase.error) &&

{(candidates.error ?? loadCandidate.error ?? resolveCandidate.error ?? refreshCandidateBase.error)?.message} Your preserved edits remain available.

} {(saveFile.error || fileError || updateBundle.error) &&

{(saveFile.error ?? fileError ?? updateBundle.error)?.message} Your unsaved edits are retained.

} {selectedFileDetail?.receipt?.materialization === "pending" &&

Revision saved. The instruction file still needs to be rebuilt from the saved revision.

} {selectedFileDetail?.revision && currentMode === "managed" && bundle?.persistence !== "agent_files" && { setDraft(null); draftBaseRevisionRef.current = undefined; draftBaseHashRef.current = undefined; queryClient.setQueryData(queryKeys.agents.instructionsFile(agent.id, selectedOrEntryFile), file); queryClient.invalidateQueries({ queryKey: queryKeys.agents.instructionsBundle(agent.id) }); }} />} {selectedFileDetail?.binary ? (

This file is preserved with the agent directory. Download it to view its contents.

Download {selectedOrEntryFile}
) : selectedFileExists && fileLoading && !selectedFileDetail ? ( ) : instructionMode === "read" ? (
{displayValue.trim() ? ( useMarkdownEditor ? ( {displayValue} ) : (
{displayValue}
) ) : (

This instruction file is empty.

)}
) : instructionMode === "raw" ? (
              {displayValue}
            
) : useMarkdownEditor ? (
{ if (!editorInteractedRef.current) return; if (draftBaseRevisionRef.current === undefined) draftBaseRevisionRef.current = selectedFileDetail?.revision?.id ?? null; if (draftBaseHashRef.current === undefined) draftBaseHashRef.current = selectedFileDetail?.contentHash ?? null; setDraft(value ?? ""); }} placeholder="# Agent instructions" className="min-w-0 overflow-hidden" contentClassName="min-h-(--sz-420px) max-w-full break-words text-sm leading-7" imageUploadHandler={async (file) => { const namespace = `agents/${agent.id}/instructions/${selectedOrEntryFile.replaceAll("/", "-")}`; const asset = await uploadMarkdownImage.mutateAsync({ file, namespace }); return asset.contentPath; }} />
) : (