@burdenoff/microfe-vibecontrols 2026.530.7 → 2026.530.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pages/AgentsPage.js
CHANGED
|
@@ -124,7 +124,7 @@ function Ye() {
|
|
|
124
124
|
name: e.trim()
|
|
125
125
|
}
|
|
126
126
|
} });
|
|
127
|
-
if (t.error && n(t.error))
|
|
127
|
+
if (t.error && n(t.error)) throw r(), V("target"), t.error;
|
|
128
128
|
let i = t.data?.createTarget;
|
|
129
129
|
return i ? (kt(), Mt(), {
|
|
130
130
|
id: i.id,
|
|
@@ -133,7 +133,8 @@ function Ye() {
|
|
|
133
133
|
status: i.status ?? "unknown"
|
|
134
134
|
}) : null;
|
|
135
135
|
} catch (e) {
|
|
136
|
-
|
|
136
|
+
if (n(e)) throw r(), V("target"), e;
|
|
137
|
+
return null;
|
|
137
138
|
}
|
|
138
139
|
}, [
|
|
139
140
|
Lt,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentsPage.js","names":[],"sources":["../../src/pages/AgentsPage.tsx"],"sourcesContent":["import { useState, useMemo, useRef, useCallback, useEffect } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { Plus, Bot, Grid, List, RefreshCw, Trash2, CheckSquare, XSquare } from 'lucide-react';\nimport { MotionPage, MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { AgentCard, AgentList, AgentForm } from '@/components/agents';\nimport { EntityTagPicker } from '@/components/shared/EntityTagPicker';\nimport { PageSkeleton } from '@/components/shared/PageSkeleton';\nimport { useDraftEntityTags } from '@/hooks/useDraftEntityTags';\nimport type { Agent } from '@/components/agents';\nimport {\n PageHeader,\n SearchInput,\n FilterDropdown,\n TagFilter,\n EmptyState,\n LoadingState,\n ErrorState,\n AgentSetupBanner,\n QuotaExhaustedDialog,\n GetHelpButton,\n} from '@/components/shared';\nimport { useVibeControls, useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport {\n useListAgentsQuery,\n useVibecontrolsAgentHealthProbesQuery,\n useVibecontrolsAgentHealthProbesLazyQuery,\n useVibecontrolsAgentHealthProbeStreamSubscription,\n useCreateAgentMutation,\n useUpdateAgentMutation,\n useDeleteAgentMutation,\n useAgentHeartbeatMutation,\n useDeactivateAgentMutation,\n useCreateTargetMutation,\n useAddAgentFromTunnelMutation,\n useStartVibecontrolsAgentMutation,\n useStopVibecontrolsAgentMutation,\n useSetupAgentGatewayAuthMutation,\n useListTargetsQuery,\n} from '@/generated/wspace-operations';\nimport { PermissionButton, AccessDenied } from '@burdenoff/fe-libs/shared/components';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { VIBE_PERMISSIONS } from '@/constants/permissions';\nimport { deriveAgentStatus, type AgentHealthProbe } from '@/utils/agentStatus';\nimport {\n isQuotaExhaustedError,\n markQuotaHandledLocally,\n getQuotaDialogContent,\n type QuotaKind,\n} from '@/utils/quotaUtils';\nimport { useCachedInitialData } from '@/hooks/useCachedInitialData';\nimport { useVibeControlsStore } from '@/store/vibeControlsStore';\n\ntype ViewMode = 'grid' | 'list';\n\n/**\n * Agents management page with filtering, search, and CRUD operations\n */\nexport function AgentsPage() {\n const { onAgentSelect, workspaceId } = useVibeControls();\n const handleNavigate = useVibeNavigate();\n const { hasPermission, isLoading: permissionsLoading, permissions } = usePermissions();\n const { t } = useI18n();\n\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const translated = t(key, params);\n const template = translated === key ? fallback : translated;\n\n if (!params) return template;\n\n return Object.entries(params).reduce(\n (acc, [paramKey, paramValue]) => acc.replace(`{{${paramKey}}}`, String(paramValue)),\n template\n );\n },\n [t]\n );\n\n const statusOptions = [\n { value: 'all', label: tr('agentsPage.statusAll', 'All Statuses') },\n { value: 'ACTIVE', label: tr('agentsPage.statusActive', 'Active') },\n { value: 'STOPPED', label: tr('agentsPage.statusStopped', 'Stopped') },\n { value: 'OFFLINE', label: tr('agentsPage.statusOffline', 'Offline') },\n ];\n\n const platformOptions = [\n { value: 'all', label: tr('agentsPage.platformAll', 'All Platforms') },\n { value: 'darwin', label: tr('agentsPage.platformMac', 'macOS') },\n { value: 'linux', label: tr('agentsPage.platformLinux', 'Linux') },\n { value: 'windows', label: tr('agentsPage.platformWindows', 'Windows') },\n ];\n\n // Check permissions\n const canListAgents = hasPermission(VIBE_PERMISSIONS.AGENT_LIST);\n\n const [searchQuery, setSearchQuery] = useState('');\n const [statusFilter, setStatusFilter] = useState('all');\n const [platformFilter, setPlatformFilter] = useState('all');\n const [tagFilter, setTagFilter] = useState('all');\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [showCreateForm, setShowCreateForm] = useState(false);\n const [isCreating, setIsCreating] = useState(false);\n\n // Auto-open Add Agent modal when arriving via /agents/new or /agents?create=true\n // (dashboard widgets, deep links). Strip the param after opening so the back\n // button returns to the previous page instead of re-opening.\n const [searchParams, setSearchParams] = useSearchParams();\n useEffect(() => {\n if (searchParams.get('create') === 'true') {\n setShowCreateForm(true);\n const next = new URLSearchParams(searchParams);\n next.delete('create');\n setSearchParams(next, { replace: true });\n }\n }, [searchParams, setSearchParams]);\n const [editTunnelAgent, setEditTunnelAgent] = useState<Agent | null>(null);\n const [isSavingTunnel, setIsSavingTunnel] = useState(false);\n // Per-agent \"connecting\" overlay shown after Edit Tunnel save while the\n // backend pushes OAuth creds → agent finalize() → /health/ready flips to\n // ok. State lives in the global store (vibeControlsStore) so the overlay\n // survives navigation to AgentDetailsPage and back — the documented\n // recovery path navigates into the details page so pushAgentGatewayAuth\n // auto-recovery can fire, and storing the set in local React state used\n // to discard it on unmount, causing a CONNECTING → DEGRADED → ACTIVE\n // flicker on return.\n const connectingAgents = useVibeControlsStore((s) => s.connectingAgents);\n const markAgentConnecting = useVibeControlsStore((s) => s.markAgentConnecting);\n const clearAgentConnecting = useVibeControlsStore((s) => s.clearAgentConnecting);\n const connectingAgentIds = useMemo(\n () => new Set(Object.keys(connectingAgents)),\n [connectingAgents]\n );\n\n // 30s safety net — if a probe never reports healthy, drop the overlay\n // so the user eventually sees the real DEGRADED state.\n const connectingTimersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());\n useEffect(() => {\n const timers = connectingTimersRef.current;\n for (const agentId of Object.keys(connectingAgents)) {\n if (timers.has(agentId)) continue;\n const timer = setTimeout(() => {\n clearAgentConnecting(agentId);\n timers.delete(agentId);\n }, 30_000);\n timers.set(agentId, timer);\n }\n // Drop timers for agents no longer marked connecting.\n for (const [agentId, timer] of timers.entries()) {\n if (!(agentId in connectingAgents)) {\n clearTimeout(timer);\n timers.delete(agentId);\n }\n }\n return () => {\n for (const timer of timers.values()) clearTimeout(timer);\n timers.clear();\n };\n }, [connectingAgents, clearAgentConnecting]);\n const [disconnectConfirmAgent, setDisconnectConfirmAgent] = useState<Agent | null>(null);\n const [isDisconnecting, setIsDisconnecting] = useState(false);\n const tunnelUrlRef = useRef<HTMLInputElement>(null);\n\n // Bulk selection state\n const [selectedAgentIds, setSelectedAgentIds] = useState<Set<string>>(new Set());\n const [bulkLoading, setBulkLoading] = useState(false);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n // Per-kind so the dialog copy (\"Agent Quota Reached\" vs \"Target Quota\n // Reached\") matches what the user just tried to create. The page hosts\n // both the agent-create form and the inline target-create row, so a\n // single boolean would mislabel one of them.\n const [quotaExhaustedKind, setQuotaExhaustedKind] = useState<QuotaKind | null>(null);\n const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n return () => {\n if (toastTimerRef.current) clearTimeout(toastTimerRef.current);\n };\n }, []);\n\n const showSuccess = useCallback((msg: string) => {\n if (toastTimerRef.current) clearTimeout(toastTimerRef.current);\n setErrorMessage(null);\n setSuccessMessage(msg);\n toastTimerRef.current = setTimeout(() => setSuccessMessage(null), 3000);\n }, []);\n const showError = useCallback((msg: string) => {\n if (toastTimerRef.current) clearTimeout(toastTimerRef.current);\n setSuccessMessage(null);\n setErrorMessage(msg);\n toastTimerRef.current = setTimeout(() => setErrorMessage(null), 5000);\n }, []);\n\n const [createAgent] = useCreateAgentMutation({ refetchQueries: ['ListAgents'] });\n const { setDraftTagIds: setAgentDraftTagIds, flushTags: flushAgentTags } =\n useDraftEntityTags('VIBECONTROLS_AGENT');\n const [addAgentFromTunnel] = useAddAgentFromTunnelMutation({ refetchQueries: ['ListAgents'] });\n const [agentHeartbeat] = useAgentHeartbeatMutation({ refetchQueries: ['ListAgents'] });\n const [deactivateAgent] = useDeactivateAgentMutation({ refetchQueries: ['ListAgents'] });\n const [startAgentMutation] = useStartVibecontrolsAgentMutation({\n refetchQueries: ['ListAgents'],\n });\n const [stopAgentMutation] = useStopVibecontrolsAgentMutation({ refetchQueries: ['ListAgents'] });\n const [setupGatewayAuth] = useSetupAgentGatewayAuthMutation({\n refetchQueries: ['ListAgents'],\n });\n\n const { refetch: refetchInitialData } = useCachedInitialData();\n const {\n data: targetsData,\n loading: targetsLoading,\n refetch: refetchTargets,\n } = useListTargetsQuery({\n // Bound the list: this query hydrates per-target agents, so leaving it\n // unbounded was the heaviest contributor to slow Agents/Targets loads (the\n // 25s OPERATION_TIMEOUT class). AgentsPage only needs targets for the\n // \"Add Agent\" picker / hasTargets flag, so a page of 50 is plenty.\n variables: { workspaceId: workspaceId ?? '', limit: 50, offset: 0 },\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n });\n const targetsList = targetsData?.targets?.items ?? [];\n const hasTargets = targetsList.length > 0;\n const [updateAgent] = useUpdateAgentMutation({ refetchQueries: ['ListAgents'] });\n const [deleteAgent] = useDeleteAgentMutation({ refetchQueries: ['ListAgents'] });\n // refetchQueries + awaitRefetchQueries: the inline create flow used to fire\n // refetchInitialData() without awaiting it, so AgentForm closed and\n // re-rendered against stale `cachedTargets` — the new target only appeared\n // on full page reload. Awaiting the underlying queries here makes the\n // mutation resolve only after the initial-data + targets caches are fresh.\n const [createTargetMutation] = useCreateTargetMutation({\n refetchQueries: ['VibeControlsInitialData', 'ListTargets'],\n awaitRefetchQueries: true,\n });\n\n /**\n * Inline DIRECT target creation callback for the AgentForm. Returns\n * the newly created target so the form can auto-select it.\n */\n const handleInlineCreateTarget = useCallback(\n async (targetName: string) => {\n if (!workspaceId || !targetName.trim()) return null;\n try {\n // errorPolicy: 'all' (shell Apollo client) puts server errors on\n // `result.error` instead of rejecting — read both paths so quota\n // errors don't silently fall back to the generic inline copy.\n const result = await createTargetMutation({\n variables: {\n workspaceId,\n input: {\n type: 'DIRECT',\n name: targetName.trim(),\n },\n },\n });\n if (result.error && isQuotaExhaustedError(result.error)) {\n markQuotaHandledLocally();\n setQuotaExhaustedKind('target');\n return null;\n }\n const created = result.data?.createTarget;\n if (!created) return null;\n refetchInitialData();\n void refetchTargets();\n return {\n id: created.id,\n name: created.name,\n type: created.type ?? 'DIRECT',\n status: created.status ?? 'unknown',\n };\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setQuotaExhaustedKind('target');\n return null;\n }\n return null;\n }\n },\n [createTargetMutation, refetchInitialData, refetchTargets, workspaceId]\n );\n\n // Fetch agents from GraphQL API\n // Skip until a workspace is selected: the resolver requires\n // x-workspace-id and throws \"Workspace context required\" when fired\n // before the auth provider has resolved a workspace (URL with ?org but\n // no &workspace, post-context-switcher self-heal, etc.). Without this\n // skip, the page lands on \"Failed to load agents\" and never recovers\n // because Apollo's errorPolicy: 'all' returns the error and doesn't\n // auto-refetch when workspaceId becomes available.\n const { data, loading, error, refetch } = useListAgentsQuery({\n variables: {\n filter: {\n isActive: statusFilter === 'ACTIVE' ? true : statusFilter === 'OFFLINE' ? false : undefined,\n platform: platformFilter !== 'all' ? platformFilter : undefined,\n },\n pagination: { limit: 100 },\n },\n fetchPolicy: 'cache-first',\n skip: !workspaceId,\n });\n\n // Live deep-health probe — backend hits /health/ready on every agent and\n // returns per-agent { healthy, errorMessage }. Polled every 30s so stale\n // tunnels (cloudflared dead, agent crashed but `isActive` still true)\n // flip the badge to DEGRADED before users hit \"create session\" against\n // an unreachable agent.\n // Initial query primes the cache; the subscription below replaces the\n // prior 30s pollInterval so badges flip to/from DEGRADED in <1s.\n // Two probe sources combined:\n //\n // 1) `useVibecontrolsAgentHealthProbesQuery` — auto-fires on mount and\n // primes `probesById` with a complete probe array in one round-trip.\n // Without this every fresh mount shows REGISTERING until the first\n // subscription event arrives, which can be many seconds when the\n // graphql-ws upgrade is slow (browsers serialize the WS handshake\n // behind the in-flight Apollo HTTP queue). The query fetches once\n // on mount AND every 30s as a fallback for environments where the\n // subscription transport is unavailable.\n //\n // 2) `useVibecontrolsAgentHealthProbeStreamSubscription` — server pushes\n // the full probe array on every change (sub-second updates) plus a\n // 30s heartbeat. When the stream is live this overrides the query\n // result in `probesById` (newest wins).\n //\n // The lazy variant is kept solely so the Refresh button can force an\n // immediate fetch without changing the auto-fired hook's variables.\n // Defer the probe query + the WS subscription until after first paint so the\n // graphql-ws handshake + probe fetch leave the critical path. The agent list\n // renders immediately (deriveAgentStatus shows REGISTERING while probesLoading)\n // and statuses stream in a tick later.\n const [probesDeferred, setProbesDeferred] = useState(false);\n useEffect(() => {\n const t = window.setTimeout(() => setProbesDeferred(true), 0);\n return () => window.clearTimeout(t);\n }, []);\n const probeQuery = useVibecontrolsAgentHealthProbesQuery({\n fetchPolicy: 'cache-and-network',\n nextFetchPolicy: 'cache-first',\n notifyOnNetworkStatusChange: true,\n skip: !workspaceId || !probesDeferred,\n // WS subscription is the live source; this poll is just a fallback for\n // environments without WS, so 60s (was 30s) is plenty and halves the load.\n pollInterval: 60_000,\n });\n const [refetchProbes] = useVibecontrolsAgentHealthProbesLazyQuery({\n fetchPolicy: 'network-only',\n });\n const { data: probeStreamData } = useVibecontrolsAgentHealthProbeStreamSubscription({\n skip: !workspaceId || !probesDeferred,\n });\n // True when no probe data has been delivered for this mount cycle yet.\n // Used by deriveAgentStatus to show REGISTERING for agents we have not\n // yet probed (instead of optimistically ACTIVE).\n const probesLoading = probeQuery.data === undefined && probeStreamData === undefined;\n const probesById = useMemo(() => {\n const m = new Map<string, AgentHealthProbe>();\n // Subscription stream wins over the initial query — strictly newer.\n const probes =\n probeStreamData?.vibecontrolsAgentHealthProbeStream ??\n probeQuery.data?.vibecontrolsAgentHealthProbes ??\n [];\n for (const p of probes) {\n m.set(p.agentId, {\n agentId: p.agentId,\n healthy: p.healthy,\n errorMessage: p.errorMessage,\n });\n }\n return m;\n }, [probeQuery.data, probeStreamData]);\n\n // Map GraphQL response to Agent type\n const agents: Agent[] = useMemo(() => {\n const rawAgents = data?.vibecontrolsAgents ?? [];\n return rawAgents.map((agent: NonNullable<typeof rawAgents>[number]) => ({\n id: agent.id,\n workspaceId: agent.workspaceId,\n name: agent.name,\n hostname: agent.hostname,\n platform: agent.platform,\n architecture: agent.architecture,\n version: agent.version,\n isActive: agent.isActive,\n status: connectingAgentIds.has(agent.id)\n ? ('CONNECTING' as const)\n : deriveAgentStatus(\n {\n id: agent.id,\n isActive: agent.isActive,\n metadata: agent.metadata as Record<string, unknown> | null,\n },\n probesById,\n { probesLoading }\n ),\n // Prefer the live health probe error. When the live probe didn't run\n // (or returned ok) but the agent's most-recent gateway-auth push\n // failed, surface that instead — otherwise the only signal of a\n // failed onboarding is a yellow Degraded badge with no hover text.\n // `pending` is hidden: it just means the async finalize is still\n // running and the badge already shows the right transient state.\n healthError:\n probesById.get(agent.id)?.errorMessage ??\n (agent.lastGatewayAuthPushStatus === 'failed'\n ? (agent.lastGatewayAuthPushError ?? 'Gateway-auth push failed')\n : null),\n lastHeartbeat: agent.lastHeartbeat || null,\n tunnelUrl: agent.tunnelUrl ?? null,\n tunnelStatus: agent.tunnelStatus ?? null,\n configuration: (agent.configuration as Record<string, unknown>) || {},\n capabilities: Array.isArray(agent.capabilities)\n ? (agent.capabilities as string[])\n : typeof agent.capabilities === 'object' && agent.capabilities !== null\n ? (() => {\n const obj = agent.capabilities as Record<string, unknown>;\n const keys = Object.keys(obj);\n // When capabilities was serialized from an array, keys are numeric (\"0\",\"1\",\"2\")\n // In that case, extract values (the actual capability names) instead of keys\n const hasNumericKeys = keys.length > 0 && keys.every((k) => /^\\d+$/.test(k));\n return hasNumericKeys ? (Object.values(obj) as string[]) : (keys as string[]);\n })()\n : [],\n metadata: (agent.metadata as Record<string, unknown>) || {},\n createdAt: agent.createdAt,\n updatedAt: agent.updatedAt,\n tagAssignments: (agent.tagAssignments ?? []).map(\n (a: NonNullable<typeof agent.tagAssignments>[number]) => ({\n id: a.id,\n tagId: a.tagId,\n tag: a.tagInfo\n ? {\n id: a.tagInfo.id,\n key: a.tagInfo.key ?? null,\n label: a.tagInfo.label ?? null,\n color: a.tagInfo.color ?? null,\n }\n : null,\n })\n ),\n }));\n }, [data?.vibecontrolsAgents, probesById, probesLoading, connectingAgentIds]);\n\n // Clear CONNECTING overlay only when ALL of these hold:\n // 1. The live probe reports `healthy: true` (deep-health probe, which\n // already gates on session.ok + tunnel.ok + ttyd.ok server-side —\n // see probeAgentHealth in wspace-vibecontrols-svc).\n // 2. The grace-window timestamp stored at markAgentConnecting time has\n // elapsed. This defends against a STALE `healthy:true` probe\n // delivered from the Apollo cache during the in-flight refetch\n // that follows updateAgent — without the grace window, that stale\n // cache hit cleared CONNECTING immediately and the next (fresh,\n // transient) probe landing during cloudflared restart flipped the\n // badge to DEGRADED, producing the CONNECTING → DEGRADED → ACTIVE\n // flicker reported by users.\n //\n // Re-runs on `probesById` updates AND on a one-shot timer per pending\n // agent so the clear fires automatically once the grace window elapses\n // even if no new probe arrives.\n useEffect(() => {\n const pending = Object.keys(connectingAgents);\n if (pending.length === 0) return;\n const now = Date.now();\n let earliestPending = Infinity;\n for (const agentId of pending) {\n const minClearAt = connectingAgents[agentId];\n if (minClearAt === undefined) continue;\n const probe = probesById.get(agentId);\n if (probe?.healthy === true && now >= minClearAt) {\n clearAgentConnecting(agentId);\n } else if (minClearAt < earliestPending) {\n earliestPending = minClearAt;\n }\n }\n if (earliestPending === Infinity) return;\n const wait = Math.max(0, earliestPending - now) + 50;\n const tick = setTimeout(() => {\n // No-op state poke — the dependency on `connectingAgents` causes\n // this effect to re-run when state changes, but a healthy probe\n // arriving BEFORE the grace window elapses needs a re-evaluation\n // when the window finally closes. Re-marking with graceMs=0 is a\n // no-op when the entry is already past its window; if cleared in\n // the meantime, the early `pending.length === 0` short-circuit\n // makes this safe.\n const fresh = useVibeControlsStore.getState().connectingAgents;\n for (const agentId of Object.keys(fresh)) {\n const minClearAt = fresh[agentId];\n if (minClearAt === undefined) continue;\n const probe = probesById.get(agentId);\n if (probe?.healthy === true && Date.now() >= minClearAt) {\n clearAgentConnecting(agentId);\n }\n }\n }, wait);\n return () => clearTimeout(tick);\n }, [probesById, connectingAgents, clearAgentConnecting]);\n\n // Filter agents based on search and tag (client-side filtering).\n // Status / platform filters are applied server-side via the GraphQL filter\n // input above; these client-side filters compose on top of that result set.\n const filteredAgents = useMemo(() => {\n return agents.filter((agent) => {\n // Search filter\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n const matchesSearch =\n agent.name.toLowerCase().includes(query) ||\n agent.hostname?.toLowerCase().includes(query) ||\n agent.id.toLowerCase().includes(query);\n if (!matchesSearch) return false;\n }\n\n // Tag filter — match against structured tagAssignments (UUIDs).\n // Never falls back to the legacy free-text agent.tags array because\n // those values are not the same as tagFilterIds and would silently\n // produce empty results (the failure mode that got the previous\n // TagFilter implementation removed in commit 4e101ca).\n if (tagFilter !== 'all') {\n const hasTag = (agent.tagAssignments ?? []).some(\n (a: { tagId: string }) => a.tagId === tagFilter\n );\n if (!hasTag) return false;\n }\n\n return true;\n });\n }, [agents, searchQuery, tagFilter]);\n\n useEffect(() => {\n return () => {\n if (toastTimerRef.current) clearTimeout(toastTimerRef.current);\n };\n }, []);\n\n const handleAgentSelect = (agent: Agent) => {\n // Navigate to agent details page\n handleNavigate(`/agents/${agent.id}`);\n // Also call the shell callback if provided\n onAgentSelect?.({\n id: agent.id,\n name: agent.name,\n type: agent.platform || 'unknown',\n status: agent.status || (agent.isActive ? 'ACTIVE' : 'OFFLINE'),\n });\n };\n\n const handleCreateAgent = () => {\n setShowCreateForm(true);\n };\n\n const handleCreateAgentSubmit = async (\n formData: Record<string, unknown>\n ): Promise<{ id: string } | void> => {\n try {\n setIsCreating(true);\n\n const tunnelUrl = (formData.tunnelUrl as string | undefined)?.trim();\n const agentApiKey = (formData.agentApiKey as string | undefined)?.trim();\n\n // Zero-config path: operator pasted the agent's tunnel URL + API key\n // shown in its startup banner. Backend probes the agent, registers it,\n // mints an OAuth app, and pushes credentials — the agent promotes\n // itself to `ready` inside that final POST /api/agent/gateway-auth.\n if (tunnelUrl && agentApiKey) {\n const result = await addAgentFromTunnel({\n variables: {\n input: {\n tunnelUrl,\n apiKey: agentApiKey,\n ...(formData.name ? { name: formData.name as string } : {}),\n ...(formData.targetId ? { targetId: formData.targetId as string } : {}),\n },\n },\n });\n if (result.error) throw result.error;\n const payload = result.data?.addVibecontrolsAgentFromTunnel;\n if (!payload?.success) {\n throw new Error(\n payload?.error || 'Failed to onboard agent — probe or OAuth setup failed.'\n );\n }\n if (payload.agent?.id) {\n await flushAgentTags(payload.agent.id);\n return { id: payload.agent.id };\n }\n throw new Error('Agent onboarding returned no agent record.');\n }\n\n // Legacy manual path — operator typed hostname/platform/etc directly.\n const result = await createAgent({\n variables: {\n input: {\n name: formData.name as string,\n hostname: formData.hostname as string,\n platform: formData.platform as string,\n architecture: formData.architecture as string,\n version: formData.version as string,\n ...(formData.targetId ? { targetId: formData.targetId as string } : {}),\n ...(formData.tunnelUrl ? { tunnelUrl: formData.tunnelUrl as string } : {}),\n ...(formData.tunnelStatus ? { tunnelStatus: formData.tunnelStatus as string } : {}),\n },\n },\n });\n\n const createdAgent = result.data?.createVibecontrolsAgent;\n if (createdAgent?.id) {\n await flushAgentTags(createdAgent.id);\n return { id: createdAgent.id };\n }\n\n if (result.error) {\n throw result.error;\n }\n\n throw new Error('Agent creation failed — no data returned from the server.');\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setQuotaExhaustedKind('agent');\n setShowCreateForm(false);\n return;\n }\n\n console.error('Failed to create agent:', err);\n throw err;\n } finally {\n setIsCreating(false);\n }\n };\n\n const handleStartAgent = async (agent: Agent) => {\n try {\n const result = await startAgentMutation({ variables: { agentId: agent.id } });\n const payload = result.data?.startVibecontrolsAgent;\n\n // Smart fallback: an agent in bootState=awaiting-config rejects\n // lifecycle/start with \"Agent not yet configured\" because it has\n // no OAuth credentials yet. The right recovery is\n // setupAgentGatewayAuth (the same mutation behind the\n // \"Re-push credentials\" button on the Security tab). Fire it\n // automatically so a single Reconnect click handles both the\n // common case and the awaiting-config case.\n if (!payload?.success) {\n const needsConfig = /not yet configured|awaiting[-\\s_]config/i.test(payload?.error ?? '');\n if (needsConfig) {\n const setupResult = await setupGatewayAuth({\n variables: { input: { agentId: agent.id } },\n });\n const setup = setupResult.data?.setupAgentGatewayAuth;\n if (setup?.error) {\n throw new Error(\n `Agent needs credentials. Auto re-push failed: ${setup.error}. Try \"Re-push credentials\" in Security tab.`\n );\n }\n if (!setup) {\n throw new Error(\n 'Agent needs credentials. Auto re-push returned no result. Try \"Re-push credentials\" in Security tab.'\n );\n }\n // `setupAgentGatewayAuth.error` is null when the OAuth app +\n // devportal app are created successfully, but the credentials\n // PUSH to the agent's tunnel can still fail downstream (the\n // svc records this as `lastGatewayAuthPushStatus: \"failed\"` +\n // `lastGatewayAuthPushError` on the agent row). The push fails\n // when the agent's finalize() hangs trying to authenticate\n // against gateway URLs it can't reach — most commonly when an\n // agent is registered against a workspace whose gateway URLs\n // are dev-local (e.g. *.local.burdenoff.com) and the agent\n // runs on a different machine. Surface that specific failure\n // rather than claiming success.\n const pushStatus = setup.agent?.lastGatewayAuthPushStatus;\n const pushError = setup.agent?.lastGatewayAuthPushError;\n if (pushStatus === 'failed') {\n throw new Error(\n `Credentials created but push to agent tunnel failed: ${\n pushError || 'unknown error'\n }. Check that the agent can reach the workspace's gateway URLs from its host network.`\n );\n }\n // PR #136 review (claude MEDIUM BUG): setupAgentGatewayAuth\n // pushes creds → agent finalize → bootState=ready. But that\n // doesn't call lifecycle/start. If the agent was previously\n // STOPPED (not just awaiting-config), it'd be credentialed\n // but not running, while the toast claimed \"and started\".\n // Retry the original lifecycle/start now that the agent\n // should have left awaiting-config; surface its outcome\n // explicitly so the toast doesn't lie either way.\n const retryResult = await startAgentMutation({\n variables: { agentId: agent.id },\n });\n const retryPayload = retryResult.data?.startVibecontrolsAgent;\n if (!retryPayload?.success) {\n throw new Error(\n `Credentials pushed but lifecycle/start failed: ${\n retryPayload?.error || 'unknown error'\n }. The agent has fresh credentials; try clicking Reconnect again or check vibe status on the host.`\n );\n }\n await agentHeartbeat({ variables: { id: agent.id } });\n showSuccess(`Agent \"${agent.name}\" re-credentialed and started.`);\n return;\n }\n throw new Error(payload?.error || 'Failed to start agent');\n }\n\n await agentHeartbeat({ variables: { id: agent.id } });\n showSuccess(`Agent \"${agent.name}\" started`);\n } catch (err) {\n console.error('Failed to start agent:', err);\n showError(err instanceof Error ? err.message : 'Failed to start agent');\n }\n };\n\n // NOTE: Stop and Disconnect both call deactivateVibecontrolsAgent and produce the same\n // server-side list state (isActive: false). The distinction is that Disconnect additionally\n // clears tunnel fields (tunnelUrl, tunnelStatus). When a dedicated stopVibecontrolsAgent\n // mutation succeeds, we still deactivate in workspace state so the list reflects the agent as offline.\n const handleStopAgent = async (agent: Agent) => {\n try {\n const result = await stopAgentMutation({ variables: { agentId: agent.id } });\n const payload = result.data?.stopVibecontrolsAgent;\n\n if (!payload?.success) {\n throw new Error(payload?.error || 'Failed to stop agent');\n }\n\n await deactivateAgent({ variables: { id: agent.id } });\n showSuccess(`Agent \"${agent.name}\" stopped`);\n } catch (err) {\n console.error('Failed to stop agent:', err);\n showError(err instanceof Error ? err.message : 'Failed to stop agent');\n }\n };\n\n const handleDisconnectAgent = (agent: Agent) => {\n setDisconnectConfirmAgent(agent);\n };\n\n const handleConfirmDisconnect = async () => {\n if (!disconnectConfirmAgent) return;\n const agent = disconnectConfirmAgent;\n setIsDisconnecting(true);\n // Deactivate + clear tunnel info so the agent appears fully disconnected.\n // Two sequential mutations with refetchQueries suppressed — a single refetch()\n // is called after both complete so the list doesn't flash with stale tunnel fields.\n // A single atomic disconnectVibecontrolsAgent backend mutation that handles\n // both deactivation and tunnel field clearing is tracked in vibecontrols-svc\n // backlog; until then the two-step approach below is the supported flow.\n try {\n await deactivateAgent({ variables: { id: agent.id }, refetchQueries: [] });\n } catch (err) {\n console.error('Failed to deactivate agent:', err);\n showError(err instanceof Error ? err.message : 'Failed to disconnect agent');\n setIsDisconnecting(false);\n setDisconnectConfirmAgent(null);\n // Early-return path: refetch here so the list reflects the deactivated state,\n // then return so the bottom refetch below does not also fire.\n await refetch();\n return;\n }\n try {\n // F4 Wave B: `agentApiKey` is no longer part of\n // `UpdateVibecontrolsAgentInput`. Clearing the tunnel fields is\n // enough — the secret is owned by the agent itself, not the svc.\n await updateAgent({\n variables: {\n id: agent.id,\n input: { tunnelUrl: null, tunnelStatus: null },\n },\n refetchQueries: [],\n });\n showSuccess(`Agent \"${agent.name}\" disconnected`);\n } catch (err) {\n console.error('Failed to clear tunnel fields:', err);\n showError(\n `Agent marked offline, but failed to clear connection fields — clear them manually in Settings.`\n );\n }\n setIsDisconnecting(false);\n setDisconnectConfirmAgent(null);\n // Both updateAgent success and updateAgent failure reach here — single refetch covers both.\n await refetch();\n };\n\n const handleConfigureAgent = (agent: Agent) => {\n // Navigate to agent details page (settings tab)\n handleNavigate(`/agents/${agent.id}`);\n };\n\n const handleEditTunnelUrl = (agent: Agent) => {\n setEditTunnelAgent(agent);\n };\n\n const handleSaveTunnelSettings = async () => {\n if (!editTunnelAgent) return;\n const agentId = editTunnelAgent.id;\n const newUrl = tunnelUrlRef.current?.value?.trim() || '';\n try {\n setIsSavingTunnel(true);\n // TODO(svc): rotate-API-key flow needs a dedicated svc mutation —\n // `UpdateVibecontrolsAgentInput.agentApiKey` was dropped by F4\n // Wave B. The Edit Tunnel dialog only updates the URL now.\n await updateAgent({\n variables: {\n id: agentId,\n input: {\n tunnelUrl: newUrl || null,\n tunnelStatus: newUrl ? 'ACTIVE' : null,\n },\n },\n // Force a fresh probe immediately so the badge resolves to the\n // real state (instead of relying on the 30s subscription cadence).\n refetchQueries: ['ListAgents', 'VibecontrolsAgentHealthProbes'],\n });\n // Begin the \"Connecting…\" overlay BEFORE clearing the dialog so the\n // operator sees an unbroken progress signal: dialog spinner →\n // CONNECTING badge with spinner → ACTIVE / DEGRADED.\n if (newUrl) markAgentConnecting(agentId);\n setEditTunnelAgent(null);\n } catch (err) {\n console.error('Failed to update tunnel settings:', err);\n } finally {\n setIsSavingTunnel(false);\n }\n };\n\n // ── Bulk Selection ─────────────────────────────────────────────────\n const toggleAgentSelection = useCallback((agentId: string) => {\n setSelectedAgentIds((prev) => {\n const next = new Set(prev);\n if (next.has(agentId)) {\n next.delete(agentId);\n } else {\n next.add(agentId);\n }\n return next;\n });\n }, []);\n\n const selectAllAgents = useCallback(() => {\n setSelectedAgentIds(new Set(filteredAgents.map((a) => a.id)));\n }, [filteredAgents]);\n\n const deselectAllAgents = useCallback(() => {\n setSelectedAgentIds(new Set());\n }, []);\n\n const isAllSelected =\n filteredAgents.length > 0 && selectedAgentIds.size === filteredAgents.length;\n const hasSelection = selectedAgentIds.size > 0;\n\n // ── Bulk Delete ──────────────────────────────────────────────────\n const handleBulkDelete = useCallback(async () => {\n if (selectedAgentIds.size === 0) return;\n if (\n !window.confirm(\n tr(\n 'agentsPage.bulkDeleteConfirm',\n 'Are you sure you want to remove {{count}} agent(s)? This cannot be undone.',\n { count: selectedAgentIds.size }\n )\n )\n )\n return;\n\n setBulkLoading(true);\n try {\n const results = await Promise.allSettled(\n Array.from(selectedAgentIds).map((id) => deleteAgent({ variables: { id } }))\n );\n const succeeded = results.filter((r) => r.status === 'fulfilled').length;\n const failed = results.filter((r) => r.status === 'rejected').length;\n if (failed > 0) {\n showError(\n `Removed ${succeeded}, failed to remove ${failed} agent${failed !== 1 ? 's' : ''}`\n );\n } else {\n showSuccess(`Removed ${succeeded} agent${succeeded !== 1 ? 's' : ''}`);\n }\n setSelectedAgentIds(new Set());\n } catch (err) {\n showError(err instanceof Error ? err.message : 'Failed to remove agents');\n } finally {\n setBulkLoading(false);\n }\n }, [selectedAgentIds, deleteAgent, showError, showSuccess]);\n\n // Stats\n //\n // \"online\" must mean the same thing the per-agent badge shows. The badge is\n // driven by `deriveAgentStatus` (live /health/ready probe + lifecycleState),\n // not by the persisted `isActive` flag — which is sticky: the backend sets it\n // true on every heartbeat but never flips it false when heartbeats stop, so a\n // stopped/crashed agent stays `isActive: true` indefinitely. Counting\n // `isActive` here is what produced \"2 of 2 agents online\" while both cards\n // showed DEGRADED. Count the derived status instead so the header and the\n // cards can never disagree.\n const stats = {\n total: agents.length,\n active: agents.filter((a) => a.status === 'ACTIVE').length,\n offline: agents.filter((a) => a.status !== 'ACTIVE').length,\n };\n\n // Guard states rendered inside MotionPage to prevent unmount/remount animation replay\n if (permissionsLoading) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <LoadingState message={tr('agentsPage.loadingPermissions', 'Loading permissions...')} />\n </MotionPage>\n );\n }\n\n if (permissions && permissions.permissionStrings?.length > 0 && !canListAgents) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <AccessDenied\n title={tr('agentsPage.accessDeniedTitle', 'Access Denied')}\n description={tr(\n 'agentsPage.accessDeniedDescription',\n \"You don't have permission to view agents. Contact your workspace administrator for access.\"\n )}\n />\n </MotionPage>\n );\n }\n\n if (error && agents.length === 0) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <ErrorState\n title={tr('agentsPage.failedLoad', 'Failed to load agents')}\n message={error.message}\n onRetry={() => refetch()}\n />\n </MotionPage>\n );\n }\n\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n {/* Toasts — z-[101] lifts above the fe-libs HeaderBar (z-[100] at\n chrome/HeaderBar.tsx:158). The previous z-50 sat *behind* the\n header strip, hiding \"Failed to start agent\" / \"Reconnect\"\n errors that were the operator's only feedback on a stuck agent. */}\n {successMessage && (\n <div className=\"fixed top-4 left-4 right-4 sm:left-auto z-[101] bg-status-success-bg text-status-success-text px-4 py-3 rounded-md shadow-lg text-sm\">\n {successMessage}\n </div>\n )}\n {errorMessage && (\n <div className=\"fixed top-4 right-4 z-[101] bg-status-error-bg text-status-error-text px-4 py-3 rounded-md shadow-lg text-sm\">\n {errorMessage}\n </div>\n )}\n\n <PageHeader\n title={tr('agentsPage.title', 'Agents')}\n description={tr('agentsPage.descriptionOnline', '{{active}} of {{total}} agents online', {\n active: stats.active,\n total: stats.total,\n })}\n actions={\n <div className=\"flex w-full flex-col items-end gap-2 sm:w-auto sm:flex-row sm:items-center\">\n <button\n type=\"button\"\n onClick={() => {\n // Refresh both the agent list AND the live deep-health\n // probe so the badge reflects current state, not a stale\n // initial probe captured while the agent was still\n // booting. Without re-probing here the user clicks the\n // refresh icon and the row's name/tunnel reload but the\n // DEGRADED badge stays stuck until the next 30s tick.\n refetch();\n void refetchProbes();\n }}\n disabled={loading || probeQuery.loading}\n className=\"flex items-center justify-center gap-2 px-3 py-2 text-text-secondary hover:bg-bg-sunken rounded-md transition-colors disabled:opacity-50\"\n title={tr('agentsPage.refreshTitle', 'Refresh agents')}\n >\n <RefreshCw\n className={`size-4 ${loading || probeQuery.loading ? 'animate-spin' : ''}`}\n />\n </button>\n {hasTargets ? (\n <PermissionButton\n permission={VIBE_PERMISSIONS.AGENT_CREATE}\n deniedBehavior=\"disable\"\n deniedTooltip={tr(\n 'agentsPage.deniedCreateTooltip',\n \"You don't have permission to create agents\"\n )}\n >\n <button\n type=\"button\"\n onClick={handleCreateAgent}\n className=\"flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <Plus className=\"size-4\" />\n {tr('agentsPage.addAgent', 'Add Agent')}\n </button>\n </PermissionButton>\n ) : (\n /* No targets yet — still open Add Agent; the form now\n supports inline target creation. */\n <button\n type=\"button\"\n onClick={handleCreateAgent}\n className=\"flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n <Plus className=\"size-4\" />\n {tr('agentsPage.addAgent', 'Add Agent')}\n </button>\n )}\n </div>\n }\n />\n\n {/* Agent CLI Setup Banner — shown when no agents exist */}\n {agents.length === 0 && !loading && (\n <AgentSetupBanner variant=\"full\" dismissKey=\"vibecontrols-agents-setup-banner\" />\n )}\n\n {/* Compact reminder when agents exist but shown once */}\n {agents.length > 0 && (\n <AgentSetupBanner variant=\"compact\" dismissKey=\"vibecontrols-agents-compact-banner\" />\n )}\n\n {/* Filters Bar */}\n <div className=\"space-y-3\">\n {/* Row 1: Search + View Toggle */}\n <div className=\"flex items-center gap-2\">\n <SearchInput\n value={searchQuery}\n onChange={setSearchQuery}\n placeholder={tr('agentsPage.searchPlaceholder', 'Search agents...')}\n className=\"flex-1 sm:w-64 sm:flex-none\"\n />\n {/* View Toggle */}\n <div className=\"flex items-center gap-1 bg-bg-sunken rounded-md p-1 flex-shrink-0\">\n <button\n type=\"button\"\n onClick={() => {\n setViewMode('grid');\n setSelectedAgentIds(new Set());\n }}\n className={`p-1.5 sm:p-2 rounded ${viewMode === 'grid' ? 'bg-bg-surface shadow-sm' : 'hover:bg-bg-surface/50'}`}\n title={tr('agentsPage.gridViewTitle', 'Grid view')}\n >\n <Grid className=\"size-4 text-text-secondary\" />\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setViewMode('list');\n setSelectedAgentIds(new Set());\n }}\n className={`p-1.5 sm:p-2 rounded ${viewMode === 'list' ? 'bg-bg-surface shadow-sm' : 'hover:bg-bg-surface/50'}`}\n title={tr('agentsPage.listViewTitle', 'List view')}\n >\n <List className=\"size-4 text-text-secondary\" />\n </button>\n </div>\n </div>\n\n {/* Row 2: Filters - wrap on all screens */}\n <div className=\"flex items-center gap-2 sm:gap-3 flex-wrap\">\n <FilterDropdown\n label={tr('agentsPage.filterStatus', 'Status')}\n value={statusFilter}\n options={statusOptions}\n onChange={setStatusFilter}\n />\n <FilterDropdown\n label={tr('agentsPage.filterPlatform', 'Platform')}\n value={platformFilter}\n options={platformOptions}\n onChange={setPlatformFilter}\n />\n <TagFilter\n label={tr('agentsPage.filterTag', 'Tag')}\n value={tagFilter}\n onChange={setTagFilter}\n />\n </div>\n </div>\n\n {/* Select All & Bulk Actions */}\n {filteredAgents.length > 0 && (\n <div className=\"flex items-center gap-2 flex-wrap\">\n <div className=\"ml-auto flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={isAllSelected ? deselectAllAgents : selectAllAgents}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs text-text-secondary hover:text-text-primary transition-colors\"\n >\n <CheckSquare className=\"size-3.5\" />\n {isAllSelected\n ? tr('agentsPage.deselectAll', 'Deselect All')\n : tr('agentsPage.selectAll', 'Select All')}\n </button>\n </div>\n </div>\n )}\n\n {/* Bulk Action Bar */}\n {hasSelection && (\n <div className=\"flex flex-col sm:flex-row items-start sm:items-center gap-3 px-4 py-2 bg-action-primary-bg/10 border border-action-primary-bg/20 rounded-lg\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('agentsPage.selectedCount', '{{count}} agents selected', {\n count: selectedAgentIds.size,\n })}\n </span>\n <div className=\"flex items-center gap-1.5 sm:ml-auto\">\n <button\n type=\"button\"\n onClick={handleBulkDelete}\n disabled={bulkLoading}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-status-error-text bg-status-error-bg border border-status-error-text/20 rounded-md hover:bg-status-error-bg/80 transition-colors disabled:opacity-50\"\n >\n <Trash2 className=\"size-3\" /> {tr('agentsPage.remove', 'Remove')}\n </button>\n <button\n type=\"button\"\n onClick={deselectAllAgents}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs text-text-secondary hover:text-text-primary transition-colors\"\n >\n <XSquare className=\"size-3\" /> {tr('agentsPage.deselect', 'Deselect')}\n </button>\n </div>\n </div>\n )}\n\n {/* Results Count */}\n {searchQuery || statusFilter !== 'all' || platformFilter !== 'all' ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('agentsPage.showingResults', 'Showing {{filtered}} of {{total}} agents', {\n filtered: filteredAgents.length,\n total: agents.length,\n })}\n {loading && (\n <span className=\"ml-2 text-text-muted\">\n {tr('agentsPage.refreshing', '(refreshing...)')}\n </span>\n )}\n </p>\n ) : null}\n\n {/* Agents Display */}\n {loading && agents.length === 0 ? (\n <PageSkeleton variant=\"rows\" count={6} />\n ) : filteredAgents.length > 0 ? (\n <AnimatePresence mode=\"wait\">\n {viewMode === 'grid' ? (\n <MotionList\n key=\"grid-view\"\n data-tour=\"agents-page-grid-header\"\n stagger={0.04}\n className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\"\n >\n {filteredAgents.map((agent) => (\n <MotionItem key={agent.id} className=\"h-full\">\n <AgentCard\n agent={agent}\n onSelect={handleAgentSelect}\n onStart={handleStartAgent}\n onStop={handleStopAgent}\n onDisconnect={handleDisconnectAgent}\n onConfigure={handleConfigureAgent}\n onEditTunnelUrl={handleEditTunnelUrl}\n selected={selectedAgentIds.has(agent.id)}\n onToggleSelect={() => toggleAgentSelection(agent.id)}\n healthErrorMessage={agent.healthError ?? null}\n />\n </MotionItem>\n ))}\n </MotionList>\n ) : (\n <MotionList key=\"list-view\" stagger={0.04}>\n <MotionItem variant=\"fadeRight\">\n {/* List view intentionally omits bulk-select props — checkboxes are\n only available in grid view. The bulk action toolbar is hidden\n when selectedAgentIds is empty, so this is safe. */}\n <AgentList\n agents={filteredAgents}\n onSelect={handleAgentSelect}\n onStart={handleStartAgent}\n onStop={handleStopAgent}\n onDisconnect={handleDisconnectAgent}\n onSettings={handleConfigureAgent}\n />\n </MotionItem>\n </MotionList>\n )}\n </AnimatePresence>\n ) : (\n <EmptyState\n icon={<Bot className=\"size-8 text-text-secondary\" />}\n title={\n searchQuery || statusFilter !== 'all' || platformFilter !== 'all'\n ? tr('agentsPage.emptyNoAgentsFound', 'No agents found')\n : tr('agentsPage.emptyNoAgentsConfigured', 'No agents configured')\n }\n description={\n searchQuery || statusFilter !== 'all' || platformFilter !== 'all'\n ? tr('agentsPage.emptyAdjustSearch', 'Try adjusting your search or filters')\n : tr(\n 'agentsPage.emptyNoAgentsDescription',\n 'Install the CLI agent on your machine, then add it here. You can create a target inline while adding the agent.'\n )\n }\n action={\n !searchQuery && statusFilter === 'all' && platformFilter === 'all' ? (\n <div className=\"flex flex-wrap items-center gap-3\">\n {/* Always open Add Agent — the form handles the \"no\n targets yet\" case via inline DIRECT target creation. */}\n <button\n type=\"button\"\n onClick={handleCreateAgent}\n className=\"flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n <Plus className=\"size-4\" />\n {tr('agentsPage.addAgent', 'Add Agent')}\n </button>\n <button\n type=\"button\"\n onClick={() => handleNavigate('/getting-started')}\n className=\"flex items-center gap-2 px-4 py-2 border border-border-default text-text-primary rounded-md hover:bg-bg-sunken transition-colors text-sm\"\n >\n {tr('agentsPage.setupGuide', 'Setup guide')}\n </button>\n <GetHelpButton prompt=\"What is an Agent in VibeControls and how do I add one?\" />\n </div>\n ) : undefined\n }\n />\n )}\n\n {/* Create Agent Modal */}\n {showCreateForm && (\n <AgentForm\n onSubmit={handleCreateAgentSubmit}\n onCancel={() => setShowCreateForm(false)}\n isLoading={isCreating}\n onNavigate={(path) => {\n setShowCreateForm(false);\n handleNavigate(path);\n }}\n targets={targetsList.map(\n (t: { id: string; name: string; type?: string; status: string }) => ({\n id: t.id,\n name: t.name,\n type: t.type || 'DIRECT',\n status: t.status,\n })\n )}\n targetsLoading={targetsLoading}\n onCreateTarget={handleInlineCreateTarget}\n extraSection={\n <div>\n <h3 className=\"text-sm font-medium text-text-secondary mb-2\">\n {tr('agents.tags', 'Tags')}\n </h3>\n <EntityTagPicker\n entityType=\"VIBECONTROLS_AGENT\"\n onDraftChange={setAgentDraftTagIds}\n />\n </div>\n }\n />\n )}\n\n {/* Disconnect Confirmation Modal */}\n <AnimatePresence>\n {disconnectConfirmAgent && (\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => !isDisconnecting && setDisconnectConfirmAgent(null))();\n }\n }}\n className=\"fixed inset-0 bg-bg-overlay flex items-center justify-center z-50 p-4 sm:p-6\"\n onClick={() => !isDisconnecting && setDisconnectConfirmAgent(null)}\n >\n <motion.div\n initial={{ opacity: 0, scale: 0.95 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.95 }}\n transition={{ duration: 0.15 }}\n className=\"bg-bg-surface border border-border-default rounded-lg shadow-xl w-full max-w-sm p-4 sm:p-6\"\n onClick={(e) => e.stopPropagation()}\n >\n <h3 className=\"text-lg font-semibold text-text-primary mb-1\">Disconnect Agent</h3>\n <p className=\"text-sm text-text-secondary mb-4\">\n {deriveAgentStatus(disconnectConfirmAgent) === 'OFFLINE' ? (\n <>\n Clear the stored tunnel connection for{' '}\n <strong>{disconnectConfirmAgent.name}</strong>? This will remove the tunnel URL\n and API key from this offline agent.\n </>\n ) : (\n <>\n Mark <strong>{disconnectConfirmAgent.name}</strong> as offline and clear its\n tunnel connection? The agent process will not be stopped automatically — you may\n need to stop it manually on the host machine.\n </>\n )}\n </p>\n <div className=\"flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => setDisconnectConfirmAgent(null)}\n disabled={isDisconnecting}\n className=\"px-4 py-2 text-sm text-text-secondary hover:bg-bg-sunken rounded-md transition-colors disabled:opacity-50\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleConfirmDisconnect}\n disabled={isDisconnecting}\n className=\"px-4 py-2 text-sm bg-action-danger-bg text-action-danger-text rounded-md hover:bg-action-danger-bg-hover transition-colors disabled:opacity-50\"\n >\n {isDisconnecting ? 'Disconnecting...' : 'Disconnect'}\n </button>\n </div>\n </motion.div>\n </div>\n )}\n </AnimatePresence>\n\n {/* Edit Tunnel Modal */}\n <AnimatePresence>\n {editTunnelAgent && (\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => setEditTunnelAgent(null))();\n }\n }}\n className=\"fixed inset-0 bg-bg-overlay flex items-center justify-center z-50 p-4 sm:p-6\"\n onClick={() => setEditTunnelAgent(null)}\n >\n <motion.div\n initial={{ opacity: 0, scale: 0.95 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.95 }}\n transition={{ duration: 0.15 }}\n className=\"bg-bg-surface border border-border-default rounded-lg shadow-xl w-full max-w-md p-4 sm:p-6\"\n onClick={(e) => e.stopPropagation()}\n >\n <h3 className=\"text-lg font-semibold text-text-primary mb-1\">\n {tr('agentsPage.editTunnelTitle', 'Edit Tunnel')}\n </h3>\n <p className=\"text-sm text-text-secondary mb-4\">\n {tr(\n 'agentsPage.editTunnelDescription',\n 'Configure the cloudflared tunnel URL and API key for {{name}}.',\n {\n name: editTunnelAgent.name,\n }\n )}\n </p>\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('agentsPage.tunnelUrlLabel', 'Tunnel URL')}\n </label>\n <input\n aria-label=\"control\"\n ref={tunnelUrlRef}\n type=\"url\"\n defaultValue={editTunnelAgent.tunnelUrl || ''}\n placeholder={tr(\n 'agentsPage.tunnelUrlPlaceholder',\n 'https://example.trycloudflare.com'\n )}\n className=\"w-full px-3 py-2 border border-border-default rounded-md bg-bg-sunken text-text-primary placeholder:text-text-placeholder focus:outline-none focus:border-border-strong\"\n />\n <p className=\"text-xs text-text-muted mt-1\">\n {tr(\n 'agentsPage.tunnelUrlHint',\n 'Leave empty to clear. Agents with auto-tunnel report this automatically on startup.'\n )}\n </p>\n </div>\n {/* F4 Wave B: API key rotation removed from this dialog —\n `UpdateVibecontrolsAgentInput.agentApiKey` was dropped\n by the svc and key rotation now requires a dedicated\n rotate mutation (TODO). The agent's key still surfaces\n on the agent details page via the lazy reveal query. */}\n </div>\n <div className=\"flex justify-end gap-2 mt-5\">\n <button\n type=\"button\"\n onClick={() => setEditTunnelAgent(null)}\n className=\"px-4 py-2 text-sm text-text-secondary hover:bg-bg-sunken rounded-md transition-colors\"\n >\n {tr('agentsPage.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={handleSaveTunnelSettings}\n disabled={isSavingTunnel}\n className=\"px-4 py-2 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n >\n {isSavingTunnel\n ? tr('agentsPage.saving', 'Saving...')\n : tr('agentsPage.save', 'Save')}\n </button>\n </div>\n </motion.div>\n </div>\n )}\n </AnimatePresence>\n\n {/* Quota Exhausted Dialog — copy switches based on the entity the\n user just tried to create (agent vs inline target). */}\n <QuotaExhaustedDialog\n isOpen={quotaExhaustedKind !== null}\n onClose={() => setQuotaExhaustedKind(null)}\n onConfirm={() => {\n setQuotaExhaustedKind(null);\n handleNavigate('/billing');\n }}\n title={getQuotaDialogContent(quotaExhaustedKind ?? 'generic').title}\n message={getQuotaDialogContent(quotaExhaustedKind ?? 'generic').message}\n />\n </MotionPage>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AgentsPage.js","names":[],"sources":["../../src/pages/AgentsPage.tsx"],"sourcesContent":["import { useState, useMemo, useRef, useCallback, useEffect } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { Plus, Bot, Grid, List, RefreshCw, Trash2, CheckSquare, XSquare } from 'lucide-react';\nimport { MotionPage, MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { AgentCard, AgentList, AgentForm } from '@/components/agents';\nimport { EntityTagPicker } from '@/components/shared/EntityTagPicker';\nimport { PageSkeleton } from '@/components/shared/PageSkeleton';\nimport { useDraftEntityTags } from '@/hooks/useDraftEntityTags';\nimport type { Agent } from '@/components/agents';\nimport {\n PageHeader,\n SearchInput,\n FilterDropdown,\n TagFilter,\n EmptyState,\n LoadingState,\n ErrorState,\n AgentSetupBanner,\n QuotaExhaustedDialog,\n GetHelpButton,\n} from '@/components/shared';\nimport { useVibeControls, useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport {\n useListAgentsQuery,\n useVibecontrolsAgentHealthProbesQuery,\n useVibecontrolsAgentHealthProbesLazyQuery,\n useVibecontrolsAgentHealthProbeStreamSubscription,\n useCreateAgentMutation,\n useUpdateAgentMutation,\n useDeleteAgentMutation,\n useAgentHeartbeatMutation,\n useDeactivateAgentMutation,\n useCreateTargetMutation,\n useAddAgentFromTunnelMutation,\n useStartVibecontrolsAgentMutation,\n useStopVibecontrolsAgentMutation,\n useSetupAgentGatewayAuthMutation,\n useListTargetsQuery,\n} from '@/generated/wspace-operations';\nimport { PermissionButton, AccessDenied } from '@burdenoff/fe-libs/shared/components';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { VIBE_PERMISSIONS } from '@/constants/permissions';\nimport { deriveAgentStatus, type AgentHealthProbe } from '@/utils/agentStatus';\nimport {\n isQuotaExhaustedError,\n markQuotaHandledLocally,\n getQuotaDialogContent,\n type QuotaKind,\n} from '@/utils/quotaUtils';\nimport { useCachedInitialData } from '@/hooks/useCachedInitialData';\nimport { useVibeControlsStore } from '@/store/vibeControlsStore';\n\ntype ViewMode = 'grid' | 'list';\n\n/**\n * Agents management page with filtering, search, and CRUD operations\n */\nexport function AgentsPage() {\n const { onAgentSelect, workspaceId } = useVibeControls();\n const handleNavigate = useVibeNavigate();\n const { hasPermission, isLoading: permissionsLoading, permissions } = usePermissions();\n const { t } = useI18n();\n\n const tr = useCallback(\n (key: string, fallback: string, params?: Record<string, string | number>): string => {\n const translated = t(key, params);\n const template = translated === key ? fallback : translated;\n\n if (!params) return template;\n\n return Object.entries(params).reduce(\n (acc, [paramKey, paramValue]) => acc.replace(`{{${paramKey}}}`, String(paramValue)),\n template\n );\n },\n [t]\n );\n\n const statusOptions = [\n { value: 'all', label: tr('agentsPage.statusAll', 'All Statuses') },\n { value: 'ACTIVE', label: tr('agentsPage.statusActive', 'Active') },\n { value: 'STOPPED', label: tr('agentsPage.statusStopped', 'Stopped') },\n { value: 'OFFLINE', label: tr('agentsPage.statusOffline', 'Offline') },\n ];\n\n const platformOptions = [\n { value: 'all', label: tr('agentsPage.platformAll', 'All Platforms') },\n { value: 'darwin', label: tr('agentsPage.platformMac', 'macOS') },\n { value: 'linux', label: tr('agentsPage.platformLinux', 'Linux') },\n { value: 'windows', label: tr('agentsPage.platformWindows', 'Windows') },\n ];\n\n // Check permissions\n const canListAgents = hasPermission(VIBE_PERMISSIONS.AGENT_LIST);\n\n const [searchQuery, setSearchQuery] = useState('');\n const [statusFilter, setStatusFilter] = useState('all');\n const [platformFilter, setPlatformFilter] = useState('all');\n const [tagFilter, setTagFilter] = useState('all');\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [showCreateForm, setShowCreateForm] = useState(false);\n const [isCreating, setIsCreating] = useState(false);\n\n // Auto-open Add Agent modal when arriving via /agents/new or /agents?create=true\n // (dashboard widgets, deep links). Strip the param after opening so the back\n // button returns to the previous page instead of re-opening.\n const [searchParams, setSearchParams] = useSearchParams();\n useEffect(() => {\n if (searchParams.get('create') === 'true') {\n setShowCreateForm(true);\n const next = new URLSearchParams(searchParams);\n next.delete('create');\n setSearchParams(next, { replace: true });\n }\n }, [searchParams, setSearchParams]);\n const [editTunnelAgent, setEditTunnelAgent] = useState<Agent | null>(null);\n const [isSavingTunnel, setIsSavingTunnel] = useState(false);\n // Per-agent \"connecting\" overlay shown after Edit Tunnel save while the\n // backend pushes OAuth creds → agent finalize() → /health/ready flips to\n // ok. State lives in the global store (vibeControlsStore) so the overlay\n // survives navigation to AgentDetailsPage and back — the documented\n // recovery path navigates into the details page so pushAgentGatewayAuth\n // auto-recovery can fire, and storing the set in local React state used\n // to discard it on unmount, causing a CONNECTING → DEGRADED → ACTIVE\n // flicker on return.\n const connectingAgents = useVibeControlsStore((s) => s.connectingAgents);\n const markAgentConnecting = useVibeControlsStore((s) => s.markAgentConnecting);\n const clearAgentConnecting = useVibeControlsStore((s) => s.clearAgentConnecting);\n const connectingAgentIds = useMemo(\n () => new Set(Object.keys(connectingAgents)),\n [connectingAgents]\n );\n\n // 30s safety net — if a probe never reports healthy, drop the overlay\n // so the user eventually sees the real DEGRADED state.\n const connectingTimersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());\n useEffect(() => {\n const timers = connectingTimersRef.current;\n for (const agentId of Object.keys(connectingAgents)) {\n if (timers.has(agentId)) continue;\n const timer = setTimeout(() => {\n clearAgentConnecting(agentId);\n timers.delete(agentId);\n }, 30_000);\n timers.set(agentId, timer);\n }\n // Drop timers for agents no longer marked connecting.\n for (const [agentId, timer] of timers.entries()) {\n if (!(agentId in connectingAgents)) {\n clearTimeout(timer);\n timers.delete(agentId);\n }\n }\n return () => {\n for (const timer of timers.values()) clearTimeout(timer);\n timers.clear();\n };\n }, [connectingAgents, clearAgentConnecting]);\n const [disconnectConfirmAgent, setDisconnectConfirmAgent] = useState<Agent | null>(null);\n const [isDisconnecting, setIsDisconnecting] = useState(false);\n const tunnelUrlRef = useRef<HTMLInputElement>(null);\n\n // Bulk selection state\n const [selectedAgentIds, setSelectedAgentIds] = useState<Set<string>>(new Set());\n const [bulkLoading, setBulkLoading] = useState(false);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n // Per-kind so the dialog copy (\"Agent Quota Reached\" vs \"Target Quota\n // Reached\") matches what the user just tried to create. The page hosts\n // both the agent-create form and the inline target-create row, so a\n // single boolean would mislabel one of them.\n const [quotaExhaustedKind, setQuotaExhaustedKind] = useState<QuotaKind | null>(null);\n const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n return () => {\n if (toastTimerRef.current) clearTimeout(toastTimerRef.current);\n };\n }, []);\n\n const showSuccess = useCallback((msg: string) => {\n if (toastTimerRef.current) clearTimeout(toastTimerRef.current);\n setErrorMessage(null);\n setSuccessMessage(msg);\n toastTimerRef.current = setTimeout(() => setSuccessMessage(null), 3000);\n }, []);\n const showError = useCallback((msg: string) => {\n if (toastTimerRef.current) clearTimeout(toastTimerRef.current);\n setSuccessMessage(null);\n setErrorMessage(msg);\n toastTimerRef.current = setTimeout(() => setErrorMessage(null), 5000);\n }, []);\n\n const [createAgent] = useCreateAgentMutation({ refetchQueries: ['ListAgents'] });\n const { setDraftTagIds: setAgentDraftTagIds, flushTags: flushAgentTags } =\n useDraftEntityTags('VIBECONTROLS_AGENT');\n const [addAgentFromTunnel] = useAddAgentFromTunnelMutation({ refetchQueries: ['ListAgents'] });\n const [agentHeartbeat] = useAgentHeartbeatMutation({ refetchQueries: ['ListAgents'] });\n const [deactivateAgent] = useDeactivateAgentMutation({ refetchQueries: ['ListAgents'] });\n const [startAgentMutation] = useStartVibecontrolsAgentMutation({\n refetchQueries: ['ListAgents'],\n });\n const [stopAgentMutation] = useStopVibecontrolsAgentMutation({ refetchQueries: ['ListAgents'] });\n const [setupGatewayAuth] = useSetupAgentGatewayAuthMutation({\n refetchQueries: ['ListAgents'],\n });\n\n const { refetch: refetchInitialData } = useCachedInitialData();\n const {\n data: targetsData,\n loading: targetsLoading,\n refetch: refetchTargets,\n } = useListTargetsQuery({\n // Bound the list: this query hydrates per-target agents, so leaving it\n // unbounded was the heaviest contributor to slow Agents/Targets loads (the\n // 25s OPERATION_TIMEOUT class). AgentsPage only needs targets for the\n // \"Add Agent\" picker / hasTargets flag, so a page of 50 is plenty.\n variables: { workspaceId: workspaceId ?? '', limit: 50, offset: 0 },\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n });\n const targetsList = targetsData?.targets?.items ?? [];\n const hasTargets = targetsList.length > 0;\n const [updateAgent] = useUpdateAgentMutation({ refetchQueries: ['ListAgents'] });\n const [deleteAgent] = useDeleteAgentMutation({ refetchQueries: ['ListAgents'] });\n // refetchQueries + awaitRefetchQueries: the inline create flow used to fire\n // refetchInitialData() without awaiting it, so AgentForm closed and\n // re-rendered against stale `cachedTargets` — the new target only appeared\n // on full page reload. Awaiting the underlying queries here makes the\n // mutation resolve only after the initial-data + targets caches are fresh.\n const [createTargetMutation] = useCreateTargetMutation({\n refetchQueries: ['VibeControlsInitialData', 'ListTargets'],\n awaitRefetchQueries: true,\n });\n\n /**\n * Inline DIRECT target creation callback for the AgentForm. Returns\n * the newly created target so the form can auto-select it.\n */\n const handleInlineCreateTarget = useCallback(\n async (targetName: string) => {\n if (!workspaceId || !targetName.trim()) return null;\n try {\n // errorPolicy: 'all' (shell Apollo client) puts server errors on\n // `result.error` instead of rejecting — read both paths so quota\n // errors don't silently fall back to the generic inline copy.\n const result = await createTargetMutation({\n variables: {\n workspaceId,\n input: {\n type: 'DIRECT',\n name: targetName.trim(),\n },\n },\n });\n if (result.error && isQuotaExhaustedError(result.error)) {\n // Open the upgrade-your-plan modal at this level and re-throw\n // so AgentForm's catch sees a quota error rather than a\n // silent null (which it would otherwise render as a vague\n // \"Failed to create target. Please try again.\" inline string).\n markQuotaHandledLocally();\n setQuotaExhaustedKind('target');\n throw result.error;\n }\n const created = result.data?.createTarget;\n if (!created) return null;\n refetchInitialData();\n void refetchTargets();\n return {\n id: created.id,\n name: created.name,\n type: created.type ?? 'DIRECT',\n status: created.status ?? 'unknown',\n };\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setQuotaExhaustedKind('target');\n throw err;\n }\n return null;\n }\n },\n [createTargetMutation, refetchInitialData, refetchTargets, workspaceId]\n );\n\n // Fetch agents from GraphQL API\n // Skip until a workspace is selected: the resolver requires\n // x-workspace-id and throws \"Workspace context required\" when fired\n // before the auth provider has resolved a workspace (URL with ?org but\n // no &workspace, post-context-switcher self-heal, etc.). Without this\n // skip, the page lands on \"Failed to load agents\" and never recovers\n // because Apollo's errorPolicy: 'all' returns the error and doesn't\n // auto-refetch when workspaceId becomes available.\n const { data, loading, error, refetch } = useListAgentsQuery({\n variables: {\n filter: {\n isActive: statusFilter === 'ACTIVE' ? true : statusFilter === 'OFFLINE' ? false : undefined,\n platform: platformFilter !== 'all' ? platformFilter : undefined,\n },\n pagination: { limit: 100 },\n },\n fetchPolicy: 'cache-first',\n skip: !workspaceId,\n });\n\n // Live deep-health probe — backend hits /health/ready on every agent and\n // returns per-agent { healthy, errorMessage }. Polled every 30s so stale\n // tunnels (cloudflared dead, agent crashed but `isActive` still true)\n // flip the badge to DEGRADED before users hit \"create session\" against\n // an unreachable agent.\n // Initial query primes the cache; the subscription below replaces the\n // prior 30s pollInterval so badges flip to/from DEGRADED in <1s.\n // Two probe sources combined:\n //\n // 1) `useVibecontrolsAgentHealthProbesQuery` — auto-fires on mount and\n // primes `probesById` with a complete probe array in one round-trip.\n // Without this every fresh mount shows REGISTERING until the first\n // subscription event arrives, which can be many seconds when the\n // graphql-ws upgrade is slow (browsers serialize the WS handshake\n // behind the in-flight Apollo HTTP queue). The query fetches once\n // on mount AND every 30s as a fallback for environments where the\n // subscription transport is unavailable.\n //\n // 2) `useVibecontrolsAgentHealthProbeStreamSubscription` — server pushes\n // the full probe array on every change (sub-second updates) plus a\n // 30s heartbeat. When the stream is live this overrides the query\n // result in `probesById` (newest wins).\n //\n // The lazy variant is kept solely so the Refresh button can force an\n // immediate fetch without changing the auto-fired hook's variables.\n // Defer the probe query + the WS subscription until after first paint so the\n // graphql-ws handshake + probe fetch leave the critical path. The agent list\n // renders immediately (deriveAgentStatus shows REGISTERING while probesLoading)\n // and statuses stream in a tick later.\n const [probesDeferred, setProbesDeferred] = useState(false);\n useEffect(() => {\n const t = window.setTimeout(() => setProbesDeferred(true), 0);\n return () => window.clearTimeout(t);\n }, []);\n const probeQuery = useVibecontrolsAgentHealthProbesQuery({\n fetchPolicy: 'cache-and-network',\n nextFetchPolicy: 'cache-first',\n notifyOnNetworkStatusChange: true,\n skip: !workspaceId || !probesDeferred,\n // WS subscription is the live source; this poll is just a fallback for\n // environments without WS, so 60s (was 30s) is plenty and halves the load.\n pollInterval: 60_000,\n });\n const [refetchProbes] = useVibecontrolsAgentHealthProbesLazyQuery({\n fetchPolicy: 'network-only',\n });\n const { data: probeStreamData } = useVibecontrolsAgentHealthProbeStreamSubscription({\n skip: !workspaceId || !probesDeferred,\n });\n // True when no probe data has been delivered for this mount cycle yet.\n // Used by deriveAgentStatus to show REGISTERING for agents we have not\n // yet probed (instead of optimistically ACTIVE).\n const probesLoading = probeQuery.data === undefined && probeStreamData === undefined;\n const probesById = useMemo(() => {\n const m = new Map<string, AgentHealthProbe>();\n // Subscription stream wins over the initial query — strictly newer.\n const probes =\n probeStreamData?.vibecontrolsAgentHealthProbeStream ??\n probeQuery.data?.vibecontrolsAgentHealthProbes ??\n [];\n for (const p of probes) {\n m.set(p.agentId, {\n agentId: p.agentId,\n healthy: p.healthy,\n errorMessage: p.errorMessage,\n });\n }\n return m;\n }, [probeQuery.data, probeStreamData]);\n\n // Map GraphQL response to Agent type\n const agents: Agent[] = useMemo(() => {\n const rawAgents = data?.vibecontrolsAgents ?? [];\n return rawAgents.map((agent: NonNullable<typeof rawAgents>[number]) => ({\n id: agent.id,\n workspaceId: agent.workspaceId,\n name: agent.name,\n hostname: agent.hostname,\n platform: agent.platform,\n architecture: agent.architecture,\n version: agent.version,\n isActive: agent.isActive,\n status: connectingAgentIds.has(agent.id)\n ? ('CONNECTING' as const)\n : deriveAgentStatus(\n {\n id: agent.id,\n isActive: agent.isActive,\n metadata: agent.metadata as Record<string, unknown> | null,\n },\n probesById,\n { probesLoading }\n ),\n // Prefer the live health probe error. When the live probe didn't run\n // (or returned ok) but the agent's most-recent gateway-auth push\n // failed, surface that instead — otherwise the only signal of a\n // failed onboarding is a yellow Degraded badge with no hover text.\n // `pending` is hidden: it just means the async finalize is still\n // running and the badge already shows the right transient state.\n healthError:\n probesById.get(agent.id)?.errorMessage ??\n (agent.lastGatewayAuthPushStatus === 'failed'\n ? (agent.lastGatewayAuthPushError ?? 'Gateway-auth push failed')\n : null),\n lastHeartbeat: agent.lastHeartbeat || null,\n tunnelUrl: agent.tunnelUrl ?? null,\n tunnelStatus: agent.tunnelStatus ?? null,\n configuration: (agent.configuration as Record<string, unknown>) || {},\n capabilities: Array.isArray(agent.capabilities)\n ? (agent.capabilities as string[])\n : typeof agent.capabilities === 'object' && agent.capabilities !== null\n ? (() => {\n const obj = agent.capabilities as Record<string, unknown>;\n const keys = Object.keys(obj);\n // When capabilities was serialized from an array, keys are numeric (\"0\",\"1\",\"2\")\n // In that case, extract values (the actual capability names) instead of keys\n const hasNumericKeys = keys.length > 0 && keys.every((k) => /^\\d+$/.test(k));\n return hasNumericKeys ? (Object.values(obj) as string[]) : (keys as string[]);\n })()\n : [],\n metadata: (agent.metadata as Record<string, unknown>) || {},\n createdAt: agent.createdAt,\n updatedAt: agent.updatedAt,\n tagAssignments: (agent.tagAssignments ?? []).map(\n (a: NonNullable<typeof agent.tagAssignments>[number]) => ({\n id: a.id,\n tagId: a.tagId,\n tag: a.tagInfo\n ? {\n id: a.tagInfo.id,\n key: a.tagInfo.key ?? null,\n label: a.tagInfo.label ?? null,\n color: a.tagInfo.color ?? null,\n }\n : null,\n })\n ),\n }));\n }, [data?.vibecontrolsAgents, probesById, probesLoading, connectingAgentIds]);\n\n // Clear CONNECTING overlay only when ALL of these hold:\n // 1. The live probe reports `healthy: true` (deep-health probe, which\n // already gates on session.ok + tunnel.ok + ttyd.ok server-side —\n // see probeAgentHealth in wspace-vibecontrols-svc).\n // 2. The grace-window timestamp stored at markAgentConnecting time has\n // elapsed. This defends against a STALE `healthy:true` probe\n // delivered from the Apollo cache during the in-flight refetch\n // that follows updateAgent — without the grace window, that stale\n // cache hit cleared CONNECTING immediately and the next (fresh,\n // transient) probe landing during cloudflared restart flipped the\n // badge to DEGRADED, producing the CONNECTING → DEGRADED → ACTIVE\n // flicker reported by users.\n //\n // Re-runs on `probesById` updates AND on a one-shot timer per pending\n // agent so the clear fires automatically once the grace window elapses\n // even if no new probe arrives.\n useEffect(() => {\n const pending = Object.keys(connectingAgents);\n if (pending.length === 0) return;\n const now = Date.now();\n let earliestPending = Infinity;\n for (const agentId of pending) {\n const minClearAt = connectingAgents[agentId];\n if (minClearAt === undefined) continue;\n const probe = probesById.get(agentId);\n if (probe?.healthy === true && now >= minClearAt) {\n clearAgentConnecting(agentId);\n } else if (minClearAt < earliestPending) {\n earliestPending = minClearAt;\n }\n }\n if (earliestPending === Infinity) return;\n const wait = Math.max(0, earliestPending - now) + 50;\n const tick = setTimeout(() => {\n // No-op state poke — the dependency on `connectingAgents` causes\n // this effect to re-run when state changes, but a healthy probe\n // arriving BEFORE the grace window elapses needs a re-evaluation\n // when the window finally closes. Re-marking with graceMs=0 is a\n // no-op when the entry is already past its window; if cleared in\n // the meantime, the early `pending.length === 0` short-circuit\n // makes this safe.\n const fresh = useVibeControlsStore.getState().connectingAgents;\n for (const agentId of Object.keys(fresh)) {\n const minClearAt = fresh[agentId];\n if (minClearAt === undefined) continue;\n const probe = probesById.get(agentId);\n if (probe?.healthy === true && Date.now() >= minClearAt) {\n clearAgentConnecting(agentId);\n }\n }\n }, wait);\n return () => clearTimeout(tick);\n }, [probesById, connectingAgents, clearAgentConnecting]);\n\n // Filter agents based on search and tag (client-side filtering).\n // Status / platform filters are applied server-side via the GraphQL filter\n // input above; these client-side filters compose on top of that result set.\n const filteredAgents = useMemo(() => {\n return agents.filter((agent) => {\n // Search filter\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n const matchesSearch =\n agent.name.toLowerCase().includes(query) ||\n agent.hostname?.toLowerCase().includes(query) ||\n agent.id.toLowerCase().includes(query);\n if (!matchesSearch) return false;\n }\n\n // Tag filter — match against structured tagAssignments (UUIDs).\n // Never falls back to the legacy free-text agent.tags array because\n // those values are not the same as tagFilterIds and would silently\n // produce empty results (the failure mode that got the previous\n // TagFilter implementation removed in commit 4e101ca).\n if (tagFilter !== 'all') {\n const hasTag = (agent.tagAssignments ?? []).some(\n (a: { tagId: string }) => a.tagId === tagFilter\n );\n if (!hasTag) return false;\n }\n\n return true;\n });\n }, [agents, searchQuery, tagFilter]);\n\n useEffect(() => {\n return () => {\n if (toastTimerRef.current) clearTimeout(toastTimerRef.current);\n };\n }, []);\n\n const handleAgentSelect = (agent: Agent) => {\n // Navigate to agent details page\n handleNavigate(`/agents/${agent.id}`);\n // Also call the shell callback if provided\n onAgentSelect?.({\n id: agent.id,\n name: agent.name,\n type: agent.platform || 'unknown',\n status: agent.status || (agent.isActive ? 'ACTIVE' : 'OFFLINE'),\n });\n };\n\n const handleCreateAgent = () => {\n setShowCreateForm(true);\n };\n\n const handleCreateAgentSubmit = async (\n formData: Record<string, unknown>\n ): Promise<{ id: string } | void> => {\n try {\n setIsCreating(true);\n\n const tunnelUrl = (formData.tunnelUrl as string | undefined)?.trim();\n const agentApiKey = (formData.agentApiKey as string | undefined)?.trim();\n\n // Zero-config path: operator pasted the agent's tunnel URL + API key\n // shown in its startup banner. Backend probes the agent, registers it,\n // mints an OAuth app, and pushes credentials — the agent promotes\n // itself to `ready` inside that final POST /api/agent/gateway-auth.\n if (tunnelUrl && agentApiKey) {\n const result = await addAgentFromTunnel({\n variables: {\n input: {\n tunnelUrl,\n apiKey: agentApiKey,\n ...(formData.name ? { name: formData.name as string } : {}),\n ...(formData.targetId ? { targetId: formData.targetId as string } : {}),\n },\n },\n });\n if (result.error) throw result.error;\n const payload = result.data?.addVibecontrolsAgentFromTunnel;\n if (!payload?.success) {\n throw new Error(\n payload?.error || 'Failed to onboard agent — probe or OAuth setup failed.'\n );\n }\n if (payload.agent?.id) {\n await flushAgentTags(payload.agent.id);\n return { id: payload.agent.id };\n }\n throw new Error('Agent onboarding returned no agent record.');\n }\n\n // Legacy manual path — operator typed hostname/platform/etc directly.\n const result = await createAgent({\n variables: {\n input: {\n name: formData.name as string,\n hostname: formData.hostname as string,\n platform: formData.platform as string,\n architecture: formData.architecture as string,\n version: formData.version as string,\n ...(formData.targetId ? { targetId: formData.targetId as string } : {}),\n ...(formData.tunnelUrl ? { tunnelUrl: formData.tunnelUrl as string } : {}),\n ...(formData.tunnelStatus ? { tunnelStatus: formData.tunnelStatus as string } : {}),\n },\n },\n });\n\n const createdAgent = result.data?.createVibecontrolsAgent;\n if (createdAgent?.id) {\n await flushAgentTags(createdAgent.id);\n return { id: createdAgent.id };\n }\n\n if (result.error) {\n throw result.error;\n }\n\n throw new Error('Agent creation failed — no data returned from the server.');\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setQuotaExhaustedKind('agent');\n setShowCreateForm(false);\n return;\n }\n\n console.error('Failed to create agent:', err);\n throw err;\n } finally {\n setIsCreating(false);\n }\n };\n\n const handleStartAgent = async (agent: Agent) => {\n try {\n const result = await startAgentMutation({ variables: { agentId: agent.id } });\n const payload = result.data?.startVibecontrolsAgent;\n\n // Smart fallback: an agent in bootState=awaiting-config rejects\n // lifecycle/start with \"Agent not yet configured\" because it has\n // no OAuth credentials yet. The right recovery is\n // setupAgentGatewayAuth (the same mutation behind the\n // \"Re-push credentials\" button on the Security tab). Fire it\n // automatically so a single Reconnect click handles both the\n // common case and the awaiting-config case.\n if (!payload?.success) {\n const needsConfig = /not yet configured|awaiting[-\\s_]config/i.test(payload?.error ?? '');\n if (needsConfig) {\n const setupResult = await setupGatewayAuth({\n variables: { input: { agentId: agent.id } },\n });\n const setup = setupResult.data?.setupAgentGatewayAuth;\n if (setup?.error) {\n throw new Error(\n `Agent needs credentials. Auto re-push failed: ${setup.error}. Try \"Re-push credentials\" in Security tab.`\n );\n }\n if (!setup) {\n throw new Error(\n 'Agent needs credentials. Auto re-push returned no result. Try \"Re-push credentials\" in Security tab.'\n );\n }\n // `setupAgentGatewayAuth.error` is null when the OAuth app +\n // devportal app are created successfully, but the credentials\n // PUSH to the agent's tunnel can still fail downstream (the\n // svc records this as `lastGatewayAuthPushStatus: \"failed\"` +\n // `lastGatewayAuthPushError` on the agent row). The push fails\n // when the agent's finalize() hangs trying to authenticate\n // against gateway URLs it can't reach — most commonly when an\n // agent is registered against a workspace whose gateway URLs\n // are dev-local (e.g. *.local.burdenoff.com) and the agent\n // runs on a different machine. Surface that specific failure\n // rather than claiming success.\n const pushStatus = setup.agent?.lastGatewayAuthPushStatus;\n const pushError = setup.agent?.lastGatewayAuthPushError;\n if (pushStatus === 'failed') {\n throw new Error(\n `Credentials created but push to agent tunnel failed: ${\n pushError || 'unknown error'\n }. Check that the agent can reach the workspace's gateway URLs from its host network.`\n );\n }\n // PR #136 review (claude MEDIUM BUG): setupAgentGatewayAuth\n // pushes creds → agent finalize → bootState=ready. But that\n // doesn't call lifecycle/start. If the agent was previously\n // STOPPED (not just awaiting-config), it'd be credentialed\n // but not running, while the toast claimed \"and started\".\n // Retry the original lifecycle/start now that the agent\n // should have left awaiting-config; surface its outcome\n // explicitly so the toast doesn't lie either way.\n const retryResult = await startAgentMutation({\n variables: { agentId: agent.id },\n });\n const retryPayload = retryResult.data?.startVibecontrolsAgent;\n if (!retryPayload?.success) {\n throw new Error(\n `Credentials pushed but lifecycle/start failed: ${\n retryPayload?.error || 'unknown error'\n }. The agent has fresh credentials; try clicking Reconnect again or check vibe status on the host.`\n );\n }\n await agentHeartbeat({ variables: { id: agent.id } });\n showSuccess(`Agent \"${agent.name}\" re-credentialed and started.`);\n return;\n }\n throw new Error(payload?.error || 'Failed to start agent');\n }\n\n await agentHeartbeat({ variables: { id: agent.id } });\n showSuccess(`Agent \"${agent.name}\" started`);\n } catch (err) {\n console.error('Failed to start agent:', err);\n showError(err instanceof Error ? err.message : 'Failed to start agent');\n }\n };\n\n // NOTE: Stop and Disconnect both call deactivateVibecontrolsAgent and produce the same\n // server-side list state (isActive: false). The distinction is that Disconnect additionally\n // clears tunnel fields (tunnelUrl, tunnelStatus). When a dedicated stopVibecontrolsAgent\n // mutation succeeds, we still deactivate in workspace state so the list reflects the agent as offline.\n const handleStopAgent = async (agent: Agent) => {\n try {\n const result = await stopAgentMutation({ variables: { agentId: agent.id } });\n const payload = result.data?.stopVibecontrolsAgent;\n\n if (!payload?.success) {\n throw new Error(payload?.error || 'Failed to stop agent');\n }\n\n await deactivateAgent({ variables: { id: agent.id } });\n showSuccess(`Agent \"${agent.name}\" stopped`);\n } catch (err) {\n console.error('Failed to stop agent:', err);\n showError(err instanceof Error ? err.message : 'Failed to stop agent');\n }\n };\n\n const handleDisconnectAgent = (agent: Agent) => {\n setDisconnectConfirmAgent(agent);\n };\n\n const handleConfirmDisconnect = async () => {\n if (!disconnectConfirmAgent) return;\n const agent = disconnectConfirmAgent;\n setIsDisconnecting(true);\n // Deactivate + clear tunnel info so the agent appears fully disconnected.\n // Two sequential mutations with refetchQueries suppressed — a single refetch()\n // is called after both complete so the list doesn't flash with stale tunnel fields.\n // A single atomic disconnectVibecontrolsAgent backend mutation that handles\n // both deactivation and tunnel field clearing is tracked in vibecontrols-svc\n // backlog; until then the two-step approach below is the supported flow.\n try {\n await deactivateAgent({ variables: { id: agent.id }, refetchQueries: [] });\n } catch (err) {\n console.error('Failed to deactivate agent:', err);\n showError(err instanceof Error ? err.message : 'Failed to disconnect agent');\n setIsDisconnecting(false);\n setDisconnectConfirmAgent(null);\n // Early-return path: refetch here so the list reflects the deactivated state,\n // then return so the bottom refetch below does not also fire.\n await refetch();\n return;\n }\n try {\n // F4 Wave B: `agentApiKey` is no longer part of\n // `UpdateVibecontrolsAgentInput`. Clearing the tunnel fields is\n // enough — the secret is owned by the agent itself, not the svc.\n await updateAgent({\n variables: {\n id: agent.id,\n input: { tunnelUrl: null, tunnelStatus: null },\n },\n refetchQueries: [],\n });\n showSuccess(`Agent \"${agent.name}\" disconnected`);\n } catch (err) {\n console.error('Failed to clear tunnel fields:', err);\n showError(\n `Agent marked offline, but failed to clear connection fields — clear them manually in Settings.`\n );\n }\n setIsDisconnecting(false);\n setDisconnectConfirmAgent(null);\n // Both updateAgent success and updateAgent failure reach here — single refetch covers both.\n await refetch();\n };\n\n const handleConfigureAgent = (agent: Agent) => {\n // Navigate to agent details page (settings tab)\n handleNavigate(`/agents/${agent.id}`);\n };\n\n const handleEditTunnelUrl = (agent: Agent) => {\n setEditTunnelAgent(agent);\n };\n\n const handleSaveTunnelSettings = async () => {\n if (!editTunnelAgent) return;\n const agentId = editTunnelAgent.id;\n const newUrl = tunnelUrlRef.current?.value?.trim() || '';\n try {\n setIsSavingTunnel(true);\n // TODO(svc): rotate-API-key flow needs a dedicated svc mutation —\n // `UpdateVibecontrolsAgentInput.agentApiKey` was dropped by F4\n // Wave B. The Edit Tunnel dialog only updates the URL now.\n await updateAgent({\n variables: {\n id: agentId,\n input: {\n tunnelUrl: newUrl || null,\n tunnelStatus: newUrl ? 'ACTIVE' : null,\n },\n },\n // Force a fresh probe immediately so the badge resolves to the\n // real state (instead of relying on the 30s subscription cadence).\n refetchQueries: ['ListAgents', 'VibecontrolsAgentHealthProbes'],\n });\n // Begin the \"Connecting…\" overlay BEFORE clearing the dialog so the\n // operator sees an unbroken progress signal: dialog spinner →\n // CONNECTING badge with spinner → ACTIVE / DEGRADED.\n if (newUrl) markAgentConnecting(agentId);\n setEditTunnelAgent(null);\n } catch (err) {\n console.error('Failed to update tunnel settings:', err);\n } finally {\n setIsSavingTunnel(false);\n }\n };\n\n // ── Bulk Selection ─────────────────────────────────────────────────\n const toggleAgentSelection = useCallback((agentId: string) => {\n setSelectedAgentIds((prev) => {\n const next = new Set(prev);\n if (next.has(agentId)) {\n next.delete(agentId);\n } else {\n next.add(agentId);\n }\n return next;\n });\n }, []);\n\n const selectAllAgents = useCallback(() => {\n setSelectedAgentIds(new Set(filteredAgents.map((a) => a.id)));\n }, [filteredAgents]);\n\n const deselectAllAgents = useCallback(() => {\n setSelectedAgentIds(new Set());\n }, []);\n\n const isAllSelected =\n filteredAgents.length > 0 && selectedAgentIds.size === filteredAgents.length;\n const hasSelection = selectedAgentIds.size > 0;\n\n // ── Bulk Delete ──────────────────────────────────────────────────\n const handleBulkDelete = useCallback(async () => {\n if (selectedAgentIds.size === 0) return;\n if (\n !window.confirm(\n tr(\n 'agentsPage.bulkDeleteConfirm',\n 'Are you sure you want to remove {{count}} agent(s)? This cannot be undone.',\n { count: selectedAgentIds.size }\n )\n )\n )\n return;\n\n setBulkLoading(true);\n try {\n const results = await Promise.allSettled(\n Array.from(selectedAgentIds).map((id) => deleteAgent({ variables: { id } }))\n );\n const succeeded = results.filter((r) => r.status === 'fulfilled').length;\n const failed = results.filter((r) => r.status === 'rejected').length;\n if (failed > 0) {\n showError(\n `Removed ${succeeded}, failed to remove ${failed} agent${failed !== 1 ? 's' : ''}`\n );\n } else {\n showSuccess(`Removed ${succeeded} agent${succeeded !== 1 ? 's' : ''}`);\n }\n setSelectedAgentIds(new Set());\n } catch (err) {\n showError(err instanceof Error ? err.message : 'Failed to remove agents');\n } finally {\n setBulkLoading(false);\n }\n }, [selectedAgentIds, deleteAgent, showError, showSuccess]);\n\n // Stats\n //\n // \"online\" must mean the same thing the per-agent badge shows. The badge is\n // driven by `deriveAgentStatus` (live /health/ready probe + lifecycleState),\n // not by the persisted `isActive` flag — which is sticky: the backend sets it\n // true on every heartbeat but never flips it false when heartbeats stop, so a\n // stopped/crashed agent stays `isActive: true` indefinitely. Counting\n // `isActive` here is what produced \"2 of 2 agents online\" while both cards\n // showed DEGRADED. Count the derived status instead so the header and the\n // cards can never disagree.\n const stats = {\n total: agents.length,\n active: agents.filter((a) => a.status === 'ACTIVE').length,\n offline: agents.filter((a) => a.status !== 'ACTIVE').length,\n };\n\n // Guard states rendered inside MotionPage to prevent unmount/remount animation replay\n if (permissionsLoading) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <LoadingState message={tr('agentsPage.loadingPermissions', 'Loading permissions...')} />\n </MotionPage>\n );\n }\n\n if (permissions && permissions.permissionStrings?.length > 0 && !canListAgents) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <AccessDenied\n title={tr('agentsPage.accessDeniedTitle', 'Access Denied')}\n description={tr(\n 'agentsPage.accessDeniedDescription',\n \"You don't have permission to view agents. Contact your workspace administrator for access.\"\n )}\n />\n </MotionPage>\n );\n }\n\n if (error && agents.length === 0) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <ErrorState\n title={tr('agentsPage.failedLoad', 'Failed to load agents')}\n message={error.message}\n onRetry={() => refetch()}\n />\n </MotionPage>\n );\n }\n\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n {/* Toasts — z-[101] lifts above the fe-libs HeaderBar (z-[100] at\n chrome/HeaderBar.tsx:158). The previous z-50 sat *behind* the\n header strip, hiding \"Failed to start agent\" / \"Reconnect\"\n errors that were the operator's only feedback on a stuck agent. */}\n {successMessage && (\n <div className=\"fixed top-4 left-4 right-4 sm:left-auto z-[101] bg-status-success-bg text-status-success-text px-4 py-3 rounded-md shadow-lg text-sm\">\n {successMessage}\n </div>\n )}\n {errorMessage && (\n <div className=\"fixed top-4 right-4 z-[101] bg-status-error-bg text-status-error-text px-4 py-3 rounded-md shadow-lg text-sm\">\n {errorMessage}\n </div>\n )}\n\n <PageHeader\n title={tr('agentsPage.title', 'Agents')}\n description={tr('agentsPage.descriptionOnline', '{{active}} of {{total}} agents online', {\n active: stats.active,\n total: stats.total,\n })}\n actions={\n <div className=\"flex w-full flex-col items-end gap-2 sm:w-auto sm:flex-row sm:items-center\">\n <button\n type=\"button\"\n onClick={() => {\n // Refresh both the agent list AND the live deep-health\n // probe so the badge reflects current state, not a stale\n // initial probe captured while the agent was still\n // booting. Without re-probing here the user clicks the\n // refresh icon and the row's name/tunnel reload but the\n // DEGRADED badge stays stuck until the next 30s tick.\n refetch();\n void refetchProbes();\n }}\n disabled={loading || probeQuery.loading}\n className=\"flex items-center justify-center gap-2 px-3 py-2 text-text-secondary hover:bg-bg-sunken rounded-md transition-colors disabled:opacity-50\"\n title={tr('agentsPage.refreshTitle', 'Refresh agents')}\n >\n <RefreshCw\n className={`size-4 ${loading || probeQuery.loading ? 'animate-spin' : ''}`}\n />\n </button>\n {hasTargets ? (\n <PermissionButton\n permission={VIBE_PERMISSIONS.AGENT_CREATE}\n deniedBehavior=\"disable\"\n deniedTooltip={tr(\n 'agentsPage.deniedCreateTooltip',\n \"You don't have permission to create agents\"\n )}\n >\n <button\n type=\"button\"\n onClick={handleCreateAgent}\n className=\"flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <Plus className=\"size-4\" />\n {tr('agentsPage.addAgent', 'Add Agent')}\n </button>\n </PermissionButton>\n ) : (\n /* No targets yet — still open Add Agent; the form now\n supports inline target creation. */\n <button\n type=\"button\"\n onClick={handleCreateAgent}\n className=\"flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n <Plus className=\"size-4\" />\n {tr('agentsPage.addAgent', 'Add Agent')}\n </button>\n )}\n </div>\n }\n />\n\n {/* Agent CLI Setup Banner — shown when no agents exist */}\n {agents.length === 0 && !loading && (\n <AgentSetupBanner variant=\"full\" dismissKey=\"vibecontrols-agents-setup-banner\" />\n )}\n\n {/* Compact reminder when agents exist but shown once */}\n {agents.length > 0 && (\n <AgentSetupBanner variant=\"compact\" dismissKey=\"vibecontrols-agents-compact-banner\" />\n )}\n\n {/* Filters Bar */}\n <div className=\"space-y-3\">\n {/* Row 1: Search + View Toggle */}\n <div className=\"flex items-center gap-2\">\n <SearchInput\n value={searchQuery}\n onChange={setSearchQuery}\n placeholder={tr('agentsPage.searchPlaceholder', 'Search agents...')}\n className=\"flex-1 sm:w-64 sm:flex-none\"\n />\n {/* View Toggle */}\n <div className=\"flex items-center gap-1 bg-bg-sunken rounded-md p-1 flex-shrink-0\">\n <button\n type=\"button\"\n onClick={() => {\n setViewMode('grid');\n setSelectedAgentIds(new Set());\n }}\n className={`p-1.5 sm:p-2 rounded ${viewMode === 'grid' ? 'bg-bg-surface shadow-sm' : 'hover:bg-bg-surface/50'}`}\n title={tr('agentsPage.gridViewTitle', 'Grid view')}\n >\n <Grid className=\"size-4 text-text-secondary\" />\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setViewMode('list');\n setSelectedAgentIds(new Set());\n }}\n className={`p-1.5 sm:p-2 rounded ${viewMode === 'list' ? 'bg-bg-surface shadow-sm' : 'hover:bg-bg-surface/50'}`}\n title={tr('agentsPage.listViewTitle', 'List view')}\n >\n <List className=\"size-4 text-text-secondary\" />\n </button>\n </div>\n </div>\n\n {/* Row 2: Filters - wrap on all screens */}\n <div className=\"flex items-center gap-2 sm:gap-3 flex-wrap\">\n <FilterDropdown\n label={tr('agentsPage.filterStatus', 'Status')}\n value={statusFilter}\n options={statusOptions}\n onChange={setStatusFilter}\n />\n <FilterDropdown\n label={tr('agentsPage.filterPlatform', 'Platform')}\n value={platformFilter}\n options={platformOptions}\n onChange={setPlatformFilter}\n />\n <TagFilter\n label={tr('agentsPage.filterTag', 'Tag')}\n value={tagFilter}\n onChange={setTagFilter}\n />\n </div>\n </div>\n\n {/* Select All & Bulk Actions */}\n {filteredAgents.length > 0 && (\n <div className=\"flex items-center gap-2 flex-wrap\">\n <div className=\"ml-auto flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={isAllSelected ? deselectAllAgents : selectAllAgents}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs text-text-secondary hover:text-text-primary transition-colors\"\n >\n <CheckSquare className=\"size-3.5\" />\n {isAllSelected\n ? tr('agentsPage.deselectAll', 'Deselect All')\n : tr('agentsPage.selectAll', 'Select All')}\n </button>\n </div>\n </div>\n )}\n\n {/* Bulk Action Bar */}\n {hasSelection && (\n <div className=\"flex flex-col sm:flex-row items-start sm:items-center gap-3 px-4 py-2 bg-action-primary-bg/10 border border-action-primary-bg/20 rounded-lg\">\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('agentsPage.selectedCount', '{{count}} agents selected', {\n count: selectedAgentIds.size,\n })}\n </span>\n <div className=\"flex items-center gap-1.5 sm:ml-auto\">\n <button\n type=\"button\"\n onClick={handleBulkDelete}\n disabled={bulkLoading}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-status-error-text bg-status-error-bg border border-status-error-text/20 rounded-md hover:bg-status-error-bg/80 transition-colors disabled:opacity-50\"\n >\n <Trash2 className=\"size-3\" /> {tr('agentsPage.remove', 'Remove')}\n </button>\n <button\n type=\"button\"\n onClick={deselectAllAgents}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs text-text-secondary hover:text-text-primary transition-colors\"\n >\n <XSquare className=\"size-3\" /> {tr('agentsPage.deselect', 'Deselect')}\n </button>\n </div>\n </div>\n )}\n\n {/* Results Count */}\n {searchQuery || statusFilter !== 'all' || platformFilter !== 'all' ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('agentsPage.showingResults', 'Showing {{filtered}} of {{total}} agents', {\n filtered: filteredAgents.length,\n total: agents.length,\n })}\n {loading && (\n <span className=\"ml-2 text-text-muted\">\n {tr('agentsPage.refreshing', '(refreshing...)')}\n </span>\n )}\n </p>\n ) : null}\n\n {/* Agents Display */}\n {loading && agents.length === 0 ? (\n <PageSkeleton variant=\"rows\" count={6} />\n ) : filteredAgents.length > 0 ? (\n <AnimatePresence mode=\"wait\">\n {viewMode === 'grid' ? (\n <MotionList\n key=\"grid-view\"\n data-tour=\"agents-page-grid-header\"\n stagger={0.04}\n className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\"\n >\n {filteredAgents.map((agent) => (\n <MotionItem key={agent.id} className=\"h-full\">\n <AgentCard\n agent={agent}\n onSelect={handleAgentSelect}\n onStart={handleStartAgent}\n onStop={handleStopAgent}\n onDisconnect={handleDisconnectAgent}\n onConfigure={handleConfigureAgent}\n onEditTunnelUrl={handleEditTunnelUrl}\n selected={selectedAgentIds.has(agent.id)}\n onToggleSelect={() => toggleAgentSelection(agent.id)}\n healthErrorMessage={agent.healthError ?? null}\n />\n </MotionItem>\n ))}\n </MotionList>\n ) : (\n <MotionList key=\"list-view\" stagger={0.04}>\n <MotionItem variant=\"fadeRight\">\n {/* List view intentionally omits bulk-select props — checkboxes are\n only available in grid view. The bulk action toolbar is hidden\n when selectedAgentIds is empty, so this is safe. */}\n <AgentList\n agents={filteredAgents}\n onSelect={handleAgentSelect}\n onStart={handleStartAgent}\n onStop={handleStopAgent}\n onDisconnect={handleDisconnectAgent}\n onSettings={handleConfigureAgent}\n />\n </MotionItem>\n </MotionList>\n )}\n </AnimatePresence>\n ) : (\n <EmptyState\n icon={<Bot className=\"size-8 text-text-secondary\" />}\n title={\n searchQuery || statusFilter !== 'all' || platformFilter !== 'all'\n ? tr('agentsPage.emptyNoAgentsFound', 'No agents found')\n : tr('agentsPage.emptyNoAgentsConfigured', 'No agents configured')\n }\n description={\n searchQuery || statusFilter !== 'all' || platformFilter !== 'all'\n ? tr('agentsPage.emptyAdjustSearch', 'Try adjusting your search or filters')\n : tr(\n 'agentsPage.emptyNoAgentsDescription',\n 'Install the CLI agent on your machine, then add it here. You can create a target inline while adding the agent.'\n )\n }\n action={\n !searchQuery && statusFilter === 'all' && platformFilter === 'all' ? (\n <div className=\"flex flex-wrap items-center gap-3\">\n {/* Always open Add Agent — the form handles the \"no\n targets yet\" case via inline DIRECT target creation. */}\n <button\n type=\"button\"\n onClick={handleCreateAgent}\n className=\"flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n <Plus className=\"size-4\" />\n {tr('agentsPage.addAgent', 'Add Agent')}\n </button>\n <button\n type=\"button\"\n onClick={() => handleNavigate('/getting-started')}\n className=\"flex items-center gap-2 px-4 py-2 border border-border-default text-text-primary rounded-md hover:bg-bg-sunken transition-colors text-sm\"\n >\n {tr('agentsPage.setupGuide', 'Setup guide')}\n </button>\n <GetHelpButton prompt=\"What is an Agent in VibeControls and how do I add one?\" />\n </div>\n ) : undefined\n }\n />\n )}\n\n {/* Create Agent Modal */}\n {showCreateForm && (\n <AgentForm\n onSubmit={handleCreateAgentSubmit}\n onCancel={() => setShowCreateForm(false)}\n isLoading={isCreating}\n onNavigate={(path) => {\n setShowCreateForm(false);\n handleNavigate(path);\n }}\n targets={targetsList.map(\n (t: { id: string; name: string; type?: string; status: string }) => ({\n id: t.id,\n name: t.name,\n type: t.type || 'DIRECT',\n status: t.status,\n })\n )}\n targetsLoading={targetsLoading}\n onCreateTarget={handleInlineCreateTarget}\n extraSection={\n <div>\n <h3 className=\"text-sm font-medium text-text-secondary mb-2\">\n {tr('agents.tags', 'Tags')}\n </h3>\n <EntityTagPicker\n entityType=\"VIBECONTROLS_AGENT\"\n onDraftChange={setAgentDraftTagIds}\n />\n </div>\n }\n />\n )}\n\n {/* Disconnect Confirmation Modal */}\n <AnimatePresence>\n {disconnectConfirmAgent && (\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => !isDisconnecting && setDisconnectConfirmAgent(null))();\n }\n }}\n className=\"fixed inset-0 bg-bg-overlay flex items-center justify-center z-50 p-4 sm:p-6\"\n onClick={() => !isDisconnecting && setDisconnectConfirmAgent(null)}\n >\n <motion.div\n initial={{ opacity: 0, scale: 0.95 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.95 }}\n transition={{ duration: 0.15 }}\n className=\"bg-bg-surface border border-border-default rounded-lg shadow-xl w-full max-w-sm p-4 sm:p-6\"\n onClick={(e) => e.stopPropagation()}\n >\n <h3 className=\"text-lg font-semibold text-text-primary mb-1\">Disconnect Agent</h3>\n <p className=\"text-sm text-text-secondary mb-4\">\n {deriveAgentStatus(disconnectConfirmAgent) === 'OFFLINE' ? (\n <>\n Clear the stored tunnel connection for{' '}\n <strong>{disconnectConfirmAgent.name}</strong>? This will remove the tunnel URL\n and API key from this offline agent.\n </>\n ) : (\n <>\n Mark <strong>{disconnectConfirmAgent.name}</strong> as offline and clear its\n tunnel connection? The agent process will not be stopped automatically — you may\n need to stop it manually on the host machine.\n </>\n )}\n </p>\n <div className=\"flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => setDisconnectConfirmAgent(null)}\n disabled={isDisconnecting}\n className=\"px-4 py-2 text-sm text-text-secondary hover:bg-bg-sunken rounded-md transition-colors disabled:opacity-50\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleConfirmDisconnect}\n disabled={isDisconnecting}\n className=\"px-4 py-2 text-sm bg-action-danger-bg text-action-danger-text rounded-md hover:bg-action-danger-bg-hover transition-colors disabled:opacity-50\"\n >\n {isDisconnecting ? 'Disconnecting...' : 'Disconnect'}\n </button>\n </div>\n </motion.div>\n </div>\n )}\n </AnimatePresence>\n\n {/* Edit Tunnel Modal */}\n <AnimatePresence>\n {editTunnelAgent && (\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => setEditTunnelAgent(null))();\n }\n }}\n className=\"fixed inset-0 bg-bg-overlay flex items-center justify-center z-50 p-4 sm:p-6\"\n onClick={() => setEditTunnelAgent(null)}\n >\n <motion.div\n initial={{ opacity: 0, scale: 0.95 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.95 }}\n transition={{ duration: 0.15 }}\n className=\"bg-bg-surface border border-border-default rounded-lg shadow-xl w-full max-w-md p-4 sm:p-6\"\n onClick={(e) => e.stopPropagation()}\n >\n <h3 className=\"text-lg font-semibold text-text-primary mb-1\">\n {tr('agentsPage.editTunnelTitle', 'Edit Tunnel')}\n </h3>\n <p className=\"text-sm text-text-secondary mb-4\">\n {tr(\n 'agentsPage.editTunnelDescription',\n 'Configure the cloudflared tunnel URL and API key for {{name}}.',\n {\n name: editTunnelAgent.name,\n }\n )}\n </p>\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('agentsPage.tunnelUrlLabel', 'Tunnel URL')}\n </label>\n <input\n aria-label=\"control\"\n ref={tunnelUrlRef}\n type=\"url\"\n defaultValue={editTunnelAgent.tunnelUrl || ''}\n placeholder={tr(\n 'agentsPage.tunnelUrlPlaceholder',\n 'https://example.trycloudflare.com'\n )}\n className=\"w-full px-3 py-2 border border-border-default rounded-md bg-bg-sunken text-text-primary placeholder:text-text-placeholder focus:outline-none focus:border-border-strong\"\n />\n <p className=\"text-xs text-text-muted mt-1\">\n {tr(\n 'agentsPage.tunnelUrlHint',\n 'Leave empty to clear. Agents with auto-tunnel report this automatically on startup.'\n )}\n </p>\n </div>\n {/* F4 Wave B: API key rotation removed from this dialog —\n `UpdateVibecontrolsAgentInput.agentApiKey` was dropped\n by the svc and key rotation now requires a dedicated\n rotate mutation (TODO). The agent's key still surfaces\n on the agent details page via the lazy reveal query. */}\n </div>\n <div className=\"flex justify-end gap-2 mt-5\">\n <button\n type=\"button\"\n onClick={() => setEditTunnelAgent(null)}\n className=\"px-4 py-2 text-sm text-text-secondary hover:bg-bg-sunken rounded-md transition-colors\"\n >\n {tr('agentsPage.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={handleSaveTunnelSettings}\n disabled={isSavingTunnel}\n className=\"px-4 py-2 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n >\n {isSavingTunnel\n ? tr('agentsPage.saving', 'Saving...')\n : tr('agentsPage.save', 'Save')}\n </button>\n </div>\n </motion.div>\n </div>\n )}\n </AnimatePresence>\n\n {/* Quota Exhausted Dialog — copy switches based on the entity the\n user just tried to create (agent vs inline target). */}\n <QuotaExhaustedDialog\n isOpen={quotaExhaustedKind !== null}\n onClose={() => setQuotaExhaustedKind(null)}\n onConfirm={() => {\n setQuotaExhaustedKind(null);\n handleNavigate('/billing');\n }}\n title={getQuotaDialogContent(quotaExhaustedKind ?? 'generic').title}\n message={getQuotaDialogContent(quotaExhaustedKind ?? 'generic').message}\n />\n </MotionPage>\n );\n}\n"],"mappings":"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"}
|
package/dist/utils/quotaUtils.js
CHANGED
|
@@ -2,10 +2,32 @@
|
|
|
2
2
|
function e(e) {
|
|
3
3
|
return e?.some((e) => e.extensions?.code === "QUOTA_EXHAUSTED" || e.extensions?.errorCode === "QUOTA_EXHAUSTED" || e.message?.includes("QUOTA_EXHAUSTED")) ?? !1;
|
|
4
4
|
}
|
|
5
|
-
function t(t) {
|
|
6
|
-
|
|
5
|
+
function t(e, t) {
|
|
6
|
+
if (!e || typeof e != "object") return;
|
|
7
|
+
let n = e[t];
|
|
8
|
+
return Array.isArray(n) ? n : void 0;
|
|
7
9
|
}
|
|
8
|
-
function n(
|
|
10
|
+
function n(n) {
|
|
11
|
+
if (n instanceof Error && (n.message.includes("QUOTA_EXHAUSTED") || n.message.toLowerCase().includes("quota exhausted"))) return !0;
|
|
12
|
+
if (!n || typeof n != "object") return !1;
|
|
13
|
+
let r = n.statusCode;
|
|
14
|
+
if (typeof r == "number" && r === 429) return !0;
|
|
15
|
+
let i = n.bodyText;
|
|
16
|
+
if (typeof i == "string" && i.includes("QUOTA_EXHAUSTED") || e("graphQLErrors" in n && Array.isArray(n.graphQLErrors) ? n.graphQLErrors : void 0) || e(t(n, "errors"))) return !0;
|
|
17
|
+
let a = n.result;
|
|
18
|
+
if (e(t(a, "errors"))) return !0;
|
|
19
|
+
let o = n.networkError;
|
|
20
|
+
if (o && typeof o == "object") {
|
|
21
|
+
let n = o.result;
|
|
22
|
+
if (e(t(n, "errors"))) return !0;
|
|
23
|
+
let r = o.statusCode;
|
|
24
|
+
if (typeof r == "number" && r === 429) return !0;
|
|
25
|
+
let i = o.bodyText;
|
|
26
|
+
if (typeof i == "string" && i.includes("QUOTA_EXHAUSTED")) return !0;
|
|
27
|
+
}
|
|
28
|
+
return !1;
|
|
29
|
+
}
|
|
30
|
+
function r(e) {
|
|
9
31
|
let t = {
|
|
10
32
|
session: {
|
|
11
33
|
title: "Session Quota Reached",
|
|
@@ -118,7 +140,7 @@ function n(e) {
|
|
|
118
140
|
};
|
|
119
141
|
return t[e] ?? t.generic;
|
|
120
142
|
}
|
|
121
|
-
var
|
|
143
|
+
var i = [
|
|
122
144
|
[/tunneldomain|subdomain|customtunneldomain/i, "tunnel-subdomain"],
|
|
123
145
|
[/starttunnel|issuetunnelsession/i, "tunnel-start"],
|
|
124
146
|
[/tunnel/i, "tunnel"],
|
|
@@ -146,21 +168,21 @@ var r = [
|
|
|
146
168
|
[/action/i, "action"],
|
|
147
169
|
[/secret|vault/i, "secret"]
|
|
148
170
|
];
|
|
149
|
-
function
|
|
171
|
+
function a(e) {
|
|
150
172
|
if (!e) return "generic";
|
|
151
|
-
for (let [t, n] of
|
|
173
|
+
for (let [t, n] of i) if (t.test(e)) return n;
|
|
152
174
|
return "generic";
|
|
153
175
|
}
|
|
154
|
-
var
|
|
155
|
-
function s() {
|
|
156
|
-
typeof window > "u" || (window[o] = Date.now());
|
|
157
|
-
}
|
|
176
|
+
var o = "vibecontrols:quota-exhausted", s = "__vibecontrolsQuotaLocalHandled";
|
|
158
177
|
function c() {
|
|
178
|
+
typeof window > "u" || (window[s] = Date.now());
|
|
179
|
+
}
|
|
180
|
+
function l() {
|
|
159
181
|
if (typeof window > "u") return !1;
|
|
160
|
-
let e = window[
|
|
182
|
+
let e = window[s];
|
|
161
183
|
return e ? Date.now() - e < 250 : !1;
|
|
162
184
|
}
|
|
163
185
|
//#endregion
|
|
164
|
-
export {
|
|
186
|
+
export { o as QUOTA_EXHAUSTED_EVENT, r as getQuotaDialogContent, a as inferQuotaKindFromOperation, n as isQuotaExhaustedError, c as markQuotaHandledLocally, l as wasQuotaHandledLocally };
|
|
165
187
|
|
|
166
188
|
//# sourceMappingURL=quotaUtils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quotaUtils.js","names":[],"sources":["../../src/utils/quotaUtils.ts"],"sourcesContent":["type GraphQLErrorLike = {\n message?: string;\n extensions?: {\n code?: string;\n errorCode?: string;\n };\n};\n\nfunction hasQuotaCode(errors: GraphQLErrorLike[] | undefined): boolean {\n return (\n errors?.some(\n (error) =>\n error.extensions?.code === 'QUOTA_EXHAUSTED' ||\n error.extensions?.errorCode === 'QUOTA_EXHAUSTED' ||\n error.message?.includes('QUOTA_EXHAUSTED')\n ) ?? false\n );\n}\n\nexport function isQuotaExhaustedError(error: unknown): boolean {\n if (\n error instanceof Error &&\n (error.message.includes('QUOTA_EXHAUSTED') ||\n error.message.toLowerCase().includes('quota exhausted'))\n ) {\n return true;\n }\n\n if (!error || typeof error !== 'object') {\n return false;\n }\n\n const graphQLErrors =\n 'graphQLErrors' in error && Array.isArray(error.graphQLErrors)\n ? (error.graphQLErrors as GraphQLErrorLike[])\n : undefined;\n\n if (hasQuotaCode(graphQLErrors)) {\n return true;\n }\n\n const errors =\n 'errors' in error && Array.isArray(error.errors)\n ? (error.errors as GraphQLErrorLike[])\n : undefined;\n\n if (hasQuotaCode(errors)) {\n return true;\n }\n\n const networkErrors =\n 'networkError' in error &&\n error.networkError &&\n typeof error.networkError === 'object' &&\n 'result' in error.networkError &&\n error.networkError.result &&\n typeof error.networkError.result === 'object' &&\n 'errors' in error.networkError.result &&\n Array.isArray(error.networkError.result.errors)\n ? (error.networkError.result.errors as GraphQLErrorLike[])\n : undefined;\n\n return hasQuotaCode(networkErrors);\n}\n\nexport type QuotaKind =\n | 'session'\n | 'environment'\n | 'tunnel'\n | 'tunnel-start'\n | 'tunnel-subdomain'\n | 'action'\n | 'component'\n | 'catalog'\n | 'docs-compilation'\n | 'gitops-integration'\n | 'note'\n | 'vibe'\n | 'vibedeck'\n | 'webhook'\n | 'plugin'\n | 'template'\n | 'docs-site'\n | 'command-execution'\n | 'target'\n | 'agent'\n | 'sandbox'\n | 'profile'\n | 'backup'\n | 'alias'\n | 'task'\n | 'secret'\n | 'generic';\n\nexport function getQuotaDialogContent(kind: QuotaKind) {\n const messages: Record<QuotaKind, { title: string; message: string }> = {\n session: {\n title: 'Session Quota Reached',\n message:\n \"You've used all available session quota on your current plan. Upgrade your plan to create more sessions or start additional session runtime flows.\",\n },\n environment: {\n title: 'Environment Quota Reached',\n message:\n \"You've used all available environment quota on your current plan. Upgrade your plan to create more environments or start additional terminal flows.\",\n },\n tunnel: {\n title: 'Tunnel Quota Reached',\n message:\n \"You've reached the maximum number of tunnels on your current plan. Upgrade your plan to create more tunnels.\",\n },\n 'tunnel-start': {\n title: 'Tunnel Start Quota Reached',\n message:\n \"You've used all available tunnel starts this billing cycle. Upgrade your plan to start more tunnels.\",\n },\n 'tunnel-subdomain': {\n title: 'Subdomain Quota Reached',\n message:\n \"You've reached the maximum number of reserved tunnel subdomains on your current plan. Upgrade to reserve more subdomains.\",\n },\n action: {\n title: 'Action Quota Reached',\n message:\n \"You've reached the maximum number of automation actions on your current plan. Upgrade to create more actions.\",\n },\n component: {\n title: 'Component Quota Reached',\n message:\n \"You've reached the maximum number of registered components on your current plan. Upgrade to register more components.\",\n },\n catalog: {\n title: 'Catalog Quota Reached',\n message:\n \"You've reached the maximum number of service catalogs on your current plan. Upgrade to create more catalogs.\",\n },\n 'docs-compilation': {\n title: 'Docs Compilation Quota Reached',\n message:\n \"You've used all available docs compilations this billing cycle. Upgrade your plan to compile more docs sites.\",\n },\n 'gitops-integration': {\n title: 'GitOps Integration Quota Reached',\n message:\n \"You've reached the maximum number of GitOps integrations on your current plan. Upgrade to add more integrations.\",\n },\n note: {\n title: 'Note Quota Reached',\n message:\n \"You've reached the maximum number of notes on your current plan. Upgrade to create more notes.\",\n },\n vibe: {\n title: 'Vibe Quota Reached',\n message:\n \"You've reached the maximum number of vibes on your current plan. Upgrade to create more vibes.\",\n },\n vibedeck: {\n title: 'VibeDeck Quota Reached',\n message:\n \"You've reached the maximum number of VibeDeck boards on your current plan. Upgrade to create more boards.\",\n },\n webhook: {\n title: 'Webhook Quota Reached',\n message:\n \"You've reached the maximum number of registered webhooks on your current plan. Upgrade to add more webhooks.\",\n },\n plugin: {\n title: 'Plugin Quota Reached',\n message:\n \"You've reached the maximum number of installed plugins on your current plan. Upgrade to install more plugins.\",\n },\n template: {\n title: 'Template Quota Reached',\n message:\n \"You've reached the maximum number of published templates on your current plan. Upgrade to publish more templates.\",\n },\n 'docs-site': {\n title: 'Docs Site Quota Reached',\n message:\n \"You've reached the maximum number of docs sites on your current plan. Upgrade to create more docs sites.\",\n },\n 'command-execution': {\n title: 'Command Execution Quota Reached',\n message:\n \"You've used all available command executions this billing cycle. Upgrade your plan to run more commands.\",\n },\n target: {\n title: 'Target Quota Reached',\n message:\n \"You've reached the maximum number of targets on your current plan. Upgrade to add more targets.\",\n },\n agent: {\n title: 'Agent Quota Reached',\n message:\n \"You've reached the maximum number of agents on your current plan. Upgrade to register more agents.\",\n },\n sandbox: {\n title: 'Sandbox Quota Reached',\n message:\n \"You've reached the maximum number of sandboxes on your current plan. Upgrade to spin up more sandboxes.\",\n },\n profile: {\n title: 'Profile Quota Reached',\n message:\n \"You've reached the maximum number of agent profiles on your current plan. Upgrade to create more profiles.\",\n },\n backup: {\n title: 'Backup Quota Reached',\n message:\n \"You've reached the maximum number of backups on your current plan. Upgrade to keep more backups.\",\n },\n alias: {\n title: 'Alias Quota Reached',\n message:\n \"You've reached the maximum number of aliases on your current plan. Upgrade to register more aliases.\",\n },\n task: {\n title: 'Task Quota Reached',\n message:\n \"You've reached the maximum number of scheduled tasks on your current plan. Upgrade to schedule more tasks.\",\n },\n secret: {\n title: 'Vault Quota Reached',\n message:\n \"You've reached the maximum number of secrets on your current plan. Upgrade to store more secrets.\",\n },\n generic: {\n title: 'Plan Quota Reached',\n message:\n \"You've reached a limit on your current plan. Upgrade to continue using this feature.\",\n },\n };\n\n return messages[kind] ?? messages['generic'];\n}\n\n// ---------------------------------------------------------------------------\n// Operation-name → quota-kind inference\n// ---------------------------------------------------------------------------\n//\n// Used by the global Apollo error link (see VibeControlsProvider) so the\n// quota dialog can show entity-specific copy without each callsite passing\n// a kind. We match by case-insensitive substring on the operation name so a\n// new mutation like `CreateTargetFromTemplate` falls into the `target` bucket\n// for free. Order matters — more specific patterns first.\n\nconst QUOTA_KIND_PATTERNS: ReadonlyArray<readonly [RegExp, QuotaKind]> = [\n // Order matters: more-specific patterns first. Plugin must come before\n // agent because `InstallAgentPlugin` would otherwise match the\n // `installagent` alternation. Profile + backup must also come before\n // agent so `agentprofile` / `agentbackup` don't get bucketed as `agent`.\n [/tunneldomain|subdomain|customtunneldomain/i, 'tunnel-subdomain'],\n [/starttunnel|issuetunnelsession/i, 'tunnel-start'],\n [/tunnel/i, 'tunnel'],\n [/compiledocs|docsbuild/i, 'docs-compilation'],\n [/docssite|docspage|docs/i, 'docs-site'],\n [/gitops/i, 'gitops-integration'],\n [/executeaction|executesharedvibedeckbutton|executevibedeckbutton/i, 'command-execution'],\n [/sandbox/i, 'sandbox'],\n [/installagentplugin|removeagentplugin|agentplugin|plugin/i, 'plugin'],\n [/agentprofile/i, 'profile'],\n [/agentbackup|restorebackup|createbackup|backup/i, 'backup'],\n [/installagent|registerinstalledagent|createagent/i, 'agent'],\n [/alias/i, 'alias'],\n [/target/i, 'target'],\n [/session|terminal/i, 'session'],\n // `environment` matches a QuotaKind that has its own dialog copy but\n // wasn't being routed here before; needs to come after `session` so\n // it doesn't shadow more specific matches.\n [/environment/i, 'environment'],\n [/note/i, 'note'],\n [/vibedeck/i, 'vibedeck'],\n [/vibecalendartask|scheduledtask|calendartask/i, 'task'],\n [/vibe/i, 'vibe'],\n [/webhook/i, 'webhook'],\n [/component/i, 'component'],\n [/catalog/i, 'catalog'],\n [/template/i, 'template'],\n [/action/i, 'action'],\n [/secret|vault/i, 'secret'],\n];\n\nexport function inferQuotaKindFromOperation(operationName?: string | null): QuotaKind {\n if (!operationName) return 'generic';\n for (const [pattern, kind] of QUOTA_KIND_PATTERNS) {\n if (pattern.test(operationName)) return kind;\n }\n return 'generic';\n}\n\n// Event name used by the global Apollo error link to broadcast quota errors\n// to the QuotaExhaustedProvider listener mounted inside the MFE tree.\nexport const QUOTA_EXHAUSTED_EVENT = 'vibecontrols:quota-exhausted';\n\nexport interface QuotaExhaustedEventDetail {\n kind: QuotaKind;\n message?: string;\n operationName?: string | null;\n}\n\n// Coordination flag for pages that still own a local QuotaExhaustedDialog\n// (e.g. AgentsPage, VibesPage, SessionsPage). When a page opens its own\n// dialog in response to `isQuotaExhaustedError(result.error)`, it sets\n// this flag for one tick. The QuotaExhaustedProvider checks the flag\n// before opening so we don't stack two modals for the same failure.\n//\n// Pages don't have to opt in — the flag only needs to be set on pages\n// that still render their own QuotaExhaustedDialog. New code should rely\n// on the global provider and leave this alone.\nconst QUOTA_LOCAL_OWNER_KEY = '__vibecontrolsQuotaLocalHandled';\n\ninterface QuotaCoordinationWindow {\n [QUOTA_LOCAL_OWNER_KEY]?: number;\n}\n\n/**\n * Pages that open a local QuotaExhaustedDialog should call this right\n * before doing so. The global provider will then skip the next event\n * within a short coordination window.\n */\nexport function markQuotaHandledLocally(): void {\n if (typeof window === 'undefined') return;\n (window as Window & QuotaCoordinationWindow)[QUOTA_LOCAL_OWNER_KEY] = Date.now();\n}\n\n/**\n * The global QuotaExhaustedProvider calls this to decide whether to skip\n * opening its own dialog. Returns true if a page-level handler claimed\n * the failure in the last 250ms.\n */\nexport function wasQuotaHandledLocally(): boolean {\n if (typeof window === 'undefined') return false;\n const at = (window as Window & QuotaCoordinationWindow)[QUOTA_LOCAL_OWNER_KEY];\n if (!at) return false;\n return Date.now() - at < 250;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"quotaUtils.js","names":[],"sources":["../../src/utils/quotaUtils.ts"],"sourcesContent":["type GraphQLErrorLike = {\n message?: string;\n extensions?: {\n code?: string;\n errorCode?: string;\n };\n};\n\nfunction hasQuotaCode(errors: GraphQLErrorLike[] | undefined): boolean {\n return (\n errors?.some(\n (error) =>\n error.extensions?.code === 'QUOTA_EXHAUSTED' ||\n error.extensions?.errorCode === 'QUOTA_EXHAUSTED' ||\n error.message?.includes('QUOTA_EXHAUSTED')\n ) ?? false\n );\n}\n\nfunction arrayOfErrorsAt(host: object | undefined, key: 'errors'): GraphQLErrorLike[] | undefined {\n if (!host || typeof host !== 'object') return undefined;\n const value = (host as Record<string, unknown>)[key];\n return Array.isArray(value) ? (value as GraphQLErrorLike[]) : undefined;\n}\n\nexport function isQuotaExhaustedError(error: unknown): boolean {\n if (\n error instanceof Error &&\n (error.message.includes('QUOTA_EXHAUSTED') ||\n error.message.toLowerCase().includes('quota exhausted'))\n ) {\n return true;\n }\n\n if (!error || typeof error !== 'object') {\n return false;\n }\n\n // HTTP-level quota signal: the wspace public gateway's @usage middleware\n // short-circuits with `res.statusCode = 429` + `errors[].extensions.code\n // = QUOTA_EXHAUSTED` (handlers.ts:sendQuotaExhausted), so the response\n // never reaches the GraphQL layer. Apollo v4 wraps that as a thrown\n // `ServerError` with `.statusCode`, `.bodyText`, and `.result` — none of\n // which were covered by the legacy `graphQLErrors`/`networkError.result`\n // shapes below. Treat HTTP 429 as the quota signal first, then fall back\n // to body-parsed signals so we still catch transports that surface\n // server errors differently.\n const statusCode = (error as { statusCode?: unknown }).statusCode;\n if (typeof statusCode === 'number' && statusCode === 429) {\n return true;\n }\n\n const bodyText = (error as { bodyText?: unknown }).bodyText;\n if (typeof bodyText === 'string' && bodyText.includes('QUOTA_EXHAUSTED')) {\n return true;\n }\n\n const graphQLErrors =\n 'graphQLErrors' in error && Array.isArray(error.graphQLErrors)\n ? (error.graphQLErrors as GraphQLErrorLike[])\n : undefined;\n\n if (hasQuotaCode(graphQLErrors)) {\n return true;\n }\n\n if (hasQuotaCode(arrayOfErrorsAt(error as object, 'errors'))) {\n return true;\n }\n\n // Apollo v4 ServerError surfaces the parsed JSON body on `.result` (when\n // the body parsed cleanly); v3 used `.networkError.result.errors`.\n // Cover both so the helper stays portable across link variants.\n const result = (error as { result?: unknown }).result;\n if (hasQuotaCode(arrayOfErrorsAt(result as object, 'errors'))) {\n return true;\n }\n\n const networkError = (error as { networkError?: unknown }).networkError;\n if (networkError && typeof networkError === 'object') {\n const netResult = (networkError as { result?: unknown }).result;\n if (hasQuotaCode(arrayOfErrorsAt(netResult as object, 'errors'))) {\n return true;\n }\n const netStatus = (networkError as { statusCode?: unknown }).statusCode;\n if (typeof netStatus === 'number' && netStatus === 429) {\n return true;\n }\n const netBody = (networkError as { bodyText?: unknown }).bodyText;\n if (typeof netBody === 'string' && netBody.includes('QUOTA_EXHAUSTED')) {\n return true;\n }\n }\n\n return false;\n}\n\nexport type QuotaKind =\n | 'session'\n | 'environment'\n | 'tunnel'\n | 'tunnel-start'\n | 'tunnel-subdomain'\n | 'action'\n | 'component'\n | 'catalog'\n | 'docs-compilation'\n | 'gitops-integration'\n | 'note'\n | 'vibe'\n | 'vibedeck'\n | 'webhook'\n | 'plugin'\n | 'template'\n | 'docs-site'\n | 'command-execution'\n | 'target'\n | 'agent'\n | 'sandbox'\n | 'profile'\n | 'backup'\n | 'alias'\n | 'task'\n | 'secret'\n | 'generic';\n\nexport function getQuotaDialogContent(kind: QuotaKind) {\n const messages: Record<QuotaKind, { title: string; message: string }> = {\n session: {\n title: 'Session Quota Reached',\n message:\n \"You've used all available session quota on your current plan. Upgrade your plan to create more sessions or start additional session runtime flows.\",\n },\n environment: {\n title: 'Environment Quota Reached',\n message:\n \"You've used all available environment quota on your current plan. Upgrade your plan to create more environments or start additional terminal flows.\",\n },\n tunnel: {\n title: 'Tunnel Quota Reached',\n message:\n \"You've reached the maximum number of tunnels on your current plan. Upgrade your plan to create more tunnels.\",\n },\n 'tunnel-start': {\n title: 'Tunnel Start Quota Reached',\n message:\n \"You've used all available tunnel starts this billing cycle. Upgrade your plan to start more tunnels.\",\n },\n 'tunnel-subdomain': {\n title: 'Subdomain Quota Reached',\n message:\n \"You've reached the maximum number of reserved tunnel subdomains on your current plan. Upgrade to reserve more subdomains.\",\n },\n action: {\n title: 'Action Quota Reached',\n message:\n \"You've reached the maximum number of automation actions on your current plan. Upgrade to create more actions.\",\n },\n component: {\n title: 'Component Quota Reached',\n message:\n \"You've reached the maximum number of registered components on your current plan. Upgrade to register more components.\",\n },\n catalog: {\n title: 'Catalog Quota Reached',\n message:\n \"You've reached the maximum number of service catalogs on your current plan. Upgrade to create more catalogs.\",\n },\n 'docs-compilation': {\n title: 'Docs Compilation Quota Reached',\n message:\n \"You've used all available docs compilations this billing cycle. Upgrade your plan to compile more docs sites.\",\n },\n 'gitops-integration': {\n title: 'GitOps Integration Quota Reached',\n message:\n \"You've reached the maximum number of GitOps integrations on your current plan. Upgrade to add more integrations.\",\n },\n note: {\n title: 'Note Quota Reached',\n message:\n \"You've reached the maximum number of notes on your current plan. Upgrade to create more notes.\",\n },\n vibe: {\n title: 'Vibe Quota Reached',\n message:\n \"You've reached the maximum number of vibes on your current plan. Upgrade to create more vibes.\",\n },\n vibedeck: {\n title: 'VibeDeck Quota Reached',\n message:\n \"You've reached the maximum number of VibeDeck boards on your current plan. Upgrade to create more boards.\",\n },\n webhook: {\n title: 'Webhook Quota Reached',\n message:\n \"You've reached the maximum number of registered webhooks on your current plan. Upgrade to add more webhooks.\",\n },\n plugin: {\n title: 'Plugin Quota Reached',\n message:\n \"You've reached the maximum number of installed plugins on your current plan. Upgrade to install more plugins.\",\n },\n template: {\n title: 'Template Quota Reached',\n message:\n \"You've reached the maximum number of published templates on your current plan. Upgrade to publish more templates.\",\n },\n 'docs-site': {\n title: 'Docs Site Quota Reached',\n message:\n \"You've reached the maximum number of docs sites on your current plan. Upgrade to create more docs sites.\",\n },\n 'command-execution': {\n title: 'Command Execution Quota Reached',\n message:\n \"You've used all available command executions this billing cycle. Upgrade your plan to run more commands.\",\n },\n target: {\n title: 'Target Quota Reached',\n message:\n \"You've reached the maximum number of targets on your current plan. Upgrade to add more targets.\",\n },\n agent: {\n title: 'Agent Quota Reached',\n message:\n \"You've reached the maximum number of agents on your current plan. Upgrade to register more agents.\",\n },\n sandbox: {\n title: 'Sandbox Quota Reached',\n message:\n \"You've reached the maximum number of sandboxes on your current plan. Upgrade to spin up more sandboxes.\",\n },\n profile: {\n title: 'Profile Quota Reached',\n message:\n \"You've reached the maximum number of agent profiles on your current plan. Upgrade to create more profiles.\",\n },\n backup: {\n title: 'Backup Quota Reached',\n message:\n \"You've reached the maximum number of backups on your current plan. Upgrade to keep more backups.\",\n },\n alias: {\n title: 'Alias Quota Reached',\n message:\n \"You've reached the maximum number of aliases on your current plan. Upgrade to register more aliases.\",\n },\n task: {\n title: 'Task Quota Reached',\n message:\n \"You've reached the maximum number of scheduled tasks on your current plan. Upgrade to schedule more tasks.\",\n },\n secret: {\n title: 'Vault Quota Reached',\n message:\n \"You've reached the maximum number of secrets on your current plan. Upgrade to store more secrets.\",\n },\n generic: {\n title: 'Plan Quota Reached',\n message:\n \"You've reached a limit on your current plan. Upgrade to continue using this feature.\",\n },\n };\n\n return messages[kind] ?? messages['generic'];\n}\n\n// ---------------------------------------------------------------------------\n// Operation-name → quota-kind inference\n// ---------------------------------------------------------------------------\n//\n// Used by the global Apollo error link (see VibeControlsProvider) so the\n// quota dialog can show entity-specific copy without each callsite passing\n// a kind. We match by case-insensitive substring on the operation name so a\n// new mutation like `CreateTargetFromTemplate` falls into the `target` bucket\n// for free. Order matters — more specific patterns first.\n\nconst QUOTA_KIND_PATTERNS: ReadonlyArray<readonly [RegExp, QuotaKind]> = [\n // Order matters: more-specific patterns first. Plugin must come before\n // agent because `InstallAgentPlugin` would otherwise match the\n // `installagent` alternation. Profile + backup must also come before\n // agent so `agentprofile` / `agentbackup` don't get bucketed as `agent`.\n [/tunneldomain|subdomain|customtunneldomain/i, 'tunnel-subdomain'],\n [/starttunnel|issuetunnelsession/i, 'tunnel-start'],\n [/tunnel/i, 'tunnel'],\n [/compiledocs|docsbuild/i, 'docs-compilation'],\n [/docssite|docspage|docs/i, 'docs-site'],\n [/gitops/i, 'gitops-integration'],\n [/executeaction|executesharedvibedeckbutton|executevibedeckbutton/i, 'command-execution'],\n [/sandbox/i, 'sandbox'],\n [/installagentplugin|removeagentplugin|agentplugin|plugin/i, 'plugin'],\n [/agentprofile/i, 'profile'],\n [/agentbackup|restorebackup|createbackup|backup/i, 'backup'],\n [/installagent|registerinstalledagent|createagent/i, 'agent'],\n [/alias/i, 'alias'],\n [/target/i, 'target'],\n [/session|terminal/i, 'session'],\n // `environment` matches a QuotaKind that has its own dialog copy but\n // wasn't being routed here before; needs to come after `session` so\n // it doesn't shadow more specific matches.\n [/environment/i, 'environment'],\n [/note/i, 'note'],\n [/vibedeck/i, 'vibedeck'],\n [/vibecalendartask|scheduledtask|calendartask/i, 'task'],\n [/vibe/i, 'vibe'],\n [/webhook/i, 'webhook'],\n [/component/i, 'component'],\n [/catalog/i, 'catalog'],\n [/template/i, 'template'],\n [/action/i, 'action'],\n [/secret|vault/i, 'secret'],\n];\n\nexport function inferQuotaKindFromOperation(operationName?: string | null): QuotaKind {\n if (!operationName) return 'generic';\n for (const [pattern, kind] of QUOTA_KIND_PATTERNS) {\n if (pattern.test(operationName)) return kind;\n }\n return 'generic';\n}\n\n// Event name used by the global Apollo error link to broadcast quota errors\n// to the QuotaExhaustedProvider listener mounted inside the MFE tree.\nexport const QUOTA_EXHAUSTED_EVENT = 'vibecontrols:quota-exhausted';\n\nexport interface QuotaExhaustedEventDetail {\n kind: QuotaKind;\n message?: string;\n operationName?: string | null;\n}\n\n// Coordination flag for pages that still own a local QuotaExhaustedDialog\n// (e.g. AgentsPage, VibesPage, SessionsPage). When a page opens its own\n// dialog in response to `isQuotaExhaustedError(result.error)`, it sets\n// this flag for one tick. The QuotaExhaustedProvider checks the flag\n// before opening so we don't stack two modals for the same failure.\n//\n// Pages don't have to opt in — the flag only needs to be set on pages\n// that still render their own QuotaExhaustedDialog. New code should rely\n// on the global provider and leave this alone.\nconst QUOTA_LOCAL_OWNER_KEY = '__vibecontrolsQuotaLocalHandled';\n\ninterface QuotaCoordinationWindow {\n [QUOTA_LOCAL_OWNER_KEY]?: number;\n}\n\n/**\n * Pages that open a local QuotaExhaustedDialog should call this right\n * before doing so. The global provider will then skip the next event\n * within a short coordination window.\n */\nexport function markQuotaHandledLocally(): void {\n if (typeof window === 'undefined') return;\n (window as Window & QuotaCoordinationWindow)[QUOTA_LOCAL_OWNER_KEY] = Date.now();\n}\n\n/**\n * The global QuotaExhaustedProvider calls this to decide whether to skip\n * opening its own dialog. Returns true if a page-level handler claimed\n * the failure in the last 250ms.\n */\nexport function wasQuotaHandledLocally(): boolean {\n if (typeof window === 'undefined') return false;\n const at = (window as Window & QuotaCoordinationWindow)[QUOTA_LOCAL_OWNER_KEY];\n if (!at) return false;\n return Date.now() - at < 250;\n}\n"],"mappings":";AAQA,SAAS,EAAa,GAAiD;AACrE,QACE,GAAQ,MACL,MACC,EAAM,YAAY,SAAS,qBAC3B,EAAM,YAAY,cAAc,qBAChC,EAAM,SAAS,SAAS,kBAAkB,CAC7C,IAAI;;AAIT,SAAS,EAAgB,GAA0B,GAA+C;AAChG,KAAI,CAAC,KAAQ,OAAO,KAAS,SAAU;CACvC,IAAM,IAAS,EAAiC;AAChD,QAAO,MAAM,QAAQ,EAAM,GAAI,IAA+B,KAAA;;AAGhE,SAAgB,EAAsB,GAAyB;AAC7D,KACE,aAAiB,UAChB,EAAM,QAAQ,SAAS,kBAAkB,IACxC,EAAM,QAAQ,aAAa,CAAC,SAAS,kBAAkB,EAEzD,QAAO;AAGT,KAAI,CAAC,KAAS,OAAO,KAAU,SAC7B,QAAO;CAYT,IAAM,IAAc,EAAmC;AACvD,KAAI,OAAO,KAAe,YAAY,MAAe,IACnD,QAAO;CAGT,IAAM,IAAY,EAAiC;AAcnD,KAbI,OAAO,KAAa,YAAY,EAAS,SAAS,kBAAkB,IASpE,EAJF,mBAAmB,KAAS,MAAM,QAAQ,EAAM,cAAc,GACzD,EAAM,gBACP,KAAA,EAEyB,IAI3B,EAAa,EAAgB,GAAiB,SAAS,CAAC,CAC1D,QAAO;CAMT,IAAM,IAAU,EAA+B;AAC/C,KAAI,EAAa,EAAgB,GAAkB,SAAS,CAAC,CAC3D,QAAO;CAGT,IAAM,IAAgB,EAAqC;AAC3D,KAAI,KAAgB,OAAO,KAAiB,UAAU;EACpD,IAAM,IAAa,EAAsC;AACzD,MAAI,EAAa,EAAgB,GAAqB,SAAS,CAAC,CAC9D,QAAO;EAET,IAAM,IAAa,EAA0C;AAC7D,MAAI,OAAO,KAAc,YAAY,MAAc,IACjD,QAAO;EAET,IAAM,IAAW,EAAwC;AACzD,MAAI,OAAO,KAAY,YAAY,EAAQ,SAAS,kBAAkB,CACpE,QAAO;;AAIX,QAAO;;AAgCT,SAAgB,EAAsB,GAAiB;CACrD,IAAM,IAAkE;EACtE,SAAS;GACP,OAAO;GACP,SACE;GACH;EACD,aAAa;GACX,OAAO;GACP,SACE;GACH;EACD,QAAQ;GACN,OAAO;GACP,SACE;GACH;EACD,gBAAgB;GACd,OAAO;GACP,SACE;GACH;EACD,oBAAoB;GAClB,OAAO;GACP,SACE;GACH;EACD,QAAQ;GACN,OAAO;GACP,SACE;GACH;EACD,WAAW;GACT,OAAO;GACP,SACE;GACH;EACD,SAAS;GACP,OAAO;GACP,SACE;GACH;EACD,oBAAoB;GAClB,OAAO;GACP,SACE;GACH;EACD,sBAAsB;GACpB,OAAO;GACP,SACE;GACH;EACD,MAAM;GACJ,OAAO;GACP,SACE;GACH;EACD,MAAM;GACJ,OAAO;GACP,SACE;GACH;EACD,UAAU;GACR,OAAO;GACP,SACE;GACH;EACD,SAAS;GACP,OAAO;GACP,SACE;GACH;EACD,QAAQ;GACN,OAAO;GACP,SACE;GACH;EACD,UAAU;GACR,OAAO;GACP,SACE;GACH;EACD,aAAa;GACX,OAAO;GACP,SACE;GACH;EACD,qBAAqB;GACnB,OAAO;GACP,SACE;GACH;EACD,QAAQ;GACN,OAAO;GACP,SACE;GACH;EACD,OAAO;GACL,OAAO;GACP,SACE;GACH;EACD,SAAS;GACP,OAAO;GACP,SACE;GACH;EACD,SAAS;GACP,OAAO;GACP,SACE;GACH;EACD,QAAQ;GACN,OAAO;GACP,SACE;GACH;EACD,OAAO;GACL,OAAO;GACP,SACE;GACH;EACD,MAAM;GACJ,OAAO;GACP,SACE;GACH;EACD,QAAQ;GACN,OAAO;GACP,SACE;GACH;EACD,SAAS;GACP,OAAO;GACP,SACE;GACH;EACF;AAED,QAAO,EAAS,MAAS,EAAS;;AAapC,IAAM,IAAmE;CAKvE,CAAC,8CAA8C,mBAAmB;CAClE,CAAC,mCAAmC,eAAe;CACnD,CAAC,WAAW,SAAS;CACrB,CAAC,0BAA0B,mBAAmB;CAC9C,CAAC,2BAA2B,YAAY;CACxC,CAAC,WAAW,qBAAqB;CACjC,CAAC,oEAAoE,oBAAoB;CACzF,CAAC,YAAY,UAAU;CACvB,CAAC,4DAA4D,SAAS;CACtE,CAAC,iBAAiB,UAAU;CAC5B,CAAC,kDAAkD,SAAS;CAC5D,CAAC,oDAAoD,QAAQ;CAC7D,CAAC,UAAU,QAAQ;CACnB,CAAC,WAAW,SAAS;CACrB,CAAC,qBAAqB,UAAU;CAIhC,CAAC,gBAAgB,cAAc;CAC/B,CAAC,SAAS,OAAO;CACjB,CAAC,aAAa,WAAW;CACzB,CAAC,gDAAgD,OAAO;CACxD,CAAC,SAAS,OAAO;CACjB,CAAC,YAAY,UAAU;CACvB,CAAC,cAAc,YAAY;CAC3B,CAAC,YAAY,UAAU;CACvB,CAAC,aAAa,WAAW;CACzB,CAAC,WAAW,SAAS;CACrB,CAAC,iBAAiB,SAAS;CAC5B;AAED,SAAgB,EAA4B,GAA0C;AACpF,KAAI,CAAC,EAAe,QAAO;AAC3B,MAAK,IAAM,CAAC,GAAS,MAAS,EAC5B,KAAI,EAAQ,KAAK,EAAc,CAAE,QAAO;AAE1C,QAAO;;AAKT,IAAa,IAAwB,gCAiB/B,IAAwB;AAW9B,SAAgB,IAAgC;AAC1C,QAAO,SAAW,QACrB,OAA4C,KAAyB,KAAK,KAAK;;AAQlF,SAAgB,IAAkC;AAChD,KAAI,OAAO,SAAW,IAAa,QAAO;CAC1C,IAAM,IAAM,OAA4C;AAExD,QADK,IACE,KAAK,KAAK,GAAG,IAAK,MADT"}
|