@burdenoff/microfe-vibecontrols 2026.623.2 → 2026.623.3
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/components/actions/ActionTypeBadge.js +1 -1
- package/dist/components/actions/ActionTypeBadge.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerAdminBar.js +1 -1
- package/dist/components/agent-manager/AgentManagerAdminBar.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerChatPane.js +1 -1
- package/dist/components/agent-manager/AgentManagerChatPane.js.map +1 -1
- package/dist/components/agent-manager/AgentManagerInputArea.js +1 -1
- package/dist/components/agent-manager/AgentManagerInputArea.js.map +1 -1
- package/dist/components/agent-manager/BridleConfigPanel.js +2 -2
- package/dist/components/agent-manager/BridleConfigPanel.js.map +1 -1
- package/dist/components/agent-manager/ProfilesPanel.js +1 -1
- package/dist/components/agent-manager/ProfilesPanel.js.map +1 -1
- package/dist/components/agent-manager/controls/AutonomySelector.js +1 -1
- package/dist/components/agent-manager/controls/AutonomySelector.js.map +1 -1
- package/dist/components/agent-manager/controls/SdkSelector.js +1 -1
- package/dist/components/agent-manager/controls/SdkSelector.js.map +1 -1
- package/dist/components/agent-manager/controls/VoiceToggle.js +1 -1
- package/dist/components/agent-manager/controls/VoiceToggle.js.map +1 -1
- package/dist/components/agent-manager/detail/SessionLogsPanel.js +2 -2
- package/dist/components/agent-manager/detail/SessionLogsPanel.js.map +1 -1
- package/dist/components/ai/AIContextsTab.js +1 -1
- package/dist/components/ai/AIContextsTab.js.map +1 -1
- package/dist/components/ai/AIOverviewTab.js +1 -1
- package/dist/components/ai/AIOverviewTab.js.map +1 -1
- package/dist/components/ai/AIPromptsTab.js +1 -1
- package/dist/components/ai/AIPromptsTab.js.map +1 -1
- package/dist/components/ai/AIQueueTab.js +1 -1
- package/dist/components/ai/AIQueueTab.js.map +1 -1
- package/dist/components/ai/AISessionsTab.js +1 -1
- package/dist/components/ai/AISessionsTab.js.map +1 -1
- package/dist/components/audit/AuditLogTable.js +1 -1
- package/dist/components/audit/AuditLogTable.js.map +1 -1
- package/dist/components/docs/DocsPageTree.js +1 -1
- package/dist/components/docs/DocsPageTree.js.map +1 -1
- package/dist/components/graph/ConnectionPermissionsDialog.js +2 -2
- package/dist/components/graph/ConnectionPermissionsDialog.js.map +1 -1
- package/dist/components/graph/MeshDeployWizard.js +3 -3
- package/dist/components/graph/MeshDeployWizard.js.map +1 -1
- package/dist/components/graph/MeshRequestPanel.js +2 -2
- package/dist/components/graph/MeshRequestPanel.js.map +1 -1
- package/dist/components/notes/NoteCard.js +1 -1
- package/dist/components/notes/NoteCard.js.map +1 -1
- package/dist/components/notes/NoteEditor.js +1 -1
- package/dist/components/notes/NoteEditor.js.map +1 -1
- package/dist/components/notes/NoteList.js +1 -1
- package/dist/components/notes/NoteList.js.map +1 -1
- package/dist/components/shared/AgentSetupBanner.js +3 -3
- package/dist/components/shared/AgentSetupBanner.js.map +1 -1
- package/dist/components/templates/TemplateCard.js +1 -1
- package/dist/components/templates/TemplateCard.js.map +1 -1
- package/dist/components/vibecalendar/types.js +1 -1
- package/dist/components/vibecalendar/types.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckButtonForm.js +1 -1
- package/dist/components/vibedeck/VibeDeckButtonForm.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckExecutionLog.js +2 -2
- package/dist/components/vibedeck/VibeDeckExecutionLog.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckForm.js +1 -1
- package/dist/components/vibedeck/VibeDeckForm.js.map +1 -1
- package/dist/components/vibedeck/VibeDeckGrid.js +1 -1
- package/dist/components/vibedeck/VibeDeckGrid.js.map +1 -1
- package/dist/pages/AgentsPage.js +2 -2
- package/dist/pages/AgentsPage.js.map +1 -1
- package/dist/pages/CatalogDetailPage.js +1 -1
- package/dist/pages/CatalogDetailPage.js.map +1 -1
- package/dist/pages/ComponentDetailPage.js +1 -1
- package/dist/pages/ComponentDetailPage.js.map +1 -1
- package/dist/pages/OverviewPage.js +10 -10
- package/dist/pages/OverviewPage.js.map +1 -1
- package/dist/pages/PluginsCatalogPage.js +1 -1
- package/dist/pages/PluginsCatalogPage.js.map +1 -1
- package/dist/pages/SessionsPage.js +3 -3
- package/dist/pages/SessionsPage.js.map +1 -1
- package/dist/pages/SharedSessionPage.js +2 -2
- package/dist/pages/SharedSessionPage.js.map +1 -1
- package/dist/pages/TargetsPage.js +2 -2
- package/dist/pages/TargetsPage.js.map +1 -1
- package/dist/pages/TemplateDetailPage.js +2 -2
- package/dist/pages/TemplateDetailPage.js.map +1 -1
- package/dist/pages/VibesPage.js +2 -2
- package/dist/pages/VibesPage.js.map +1 -1
- package/dist/pages/WebhooksPage.js +2 -2
- package/dist/pages/WebhooksPage.js.map +1 -1
- package/dist/pages/ai/AISessionDetailPage.js +2 -2
- package/dist/pages/ai/AISessionDetailPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -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 { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { MotionPage, MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { AnimatePresence } from 'framer-motion';\nimport {\n Button,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n} from '@burdenoff/fe-libs/ui';\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 ConfirmDialog,\n} from '@/components/shared';\nimport { notify } from '@/lib/toast';\nimport { useField } from '@/shared/hooks/useField';\nimport { useTr } from '@/shared/hooks/useTr';\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 { 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 bus = useEventBus();\n const { hasPermission, isLoading: permissionsLoading, permissions } = usePermissions();\n const tr = useTr();\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 const tunnelUrlField = useField({\n hint: tr(\n 'agentsPage.tunnelUrlHint',\n 'Leave empty to clear. Agents with auto-tunnel report this automatically on startup.'\n ),\n });\n\n // Bulk selection state\n const [selectedAgentIds, setSelectedAgentIds] = useState<Set<string>>(new Set());\n const [bulkLoading, setBulkLoading] = useState(false);\n const [bulkDeleteConfirmOpen, setBulkDeleteConfirmOpen] = useState(false);\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\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 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 bus.emitCustom('vibecontrols.agent.register_clicked', {\n workspaceId: workspaceId ?? undefined,\n });\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 bus.emitCustom('vibecontrols.agent.registered', {\n workspaceId: workspaceId ?? undefined,\n agentId: payload.agent.id,\n });\n bus.emitCustom('vibecontrols.agent.gateway_auth_setup', {\n workspaceId: workspaceId ?? undefined,\n agentId: payload.agent.id,\n });\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 bus.emitCustom('vibecontrols.agent.registered', {\n workspaceId: workspaceId ?? undefined,\n agentId: createdAgent.id,\n });\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 bus.emitCustom('vibecontrols.quota.agent_limit_hit', {\n workspaceId: workspaceId ?? undefined,\n });\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 bus.emitCustom('vibecontrols.agent.gateway_auth_setup', {\n workspaceId: workspaceId ?? undefined,\n agentId: agent.id,\n });\n await agentHeartbeat({ variables: { id: agent.id } });\n notify.success(`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 notify.success(`Agent \"${agent.name}\" started`);\n } catch (err) {\n console.error('Failed to start agent:', err);\n notify.error(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 notify.success(`Agent \"${agent.name}\" stopped`);\n } catch (err) {\n console.error('Failed to stop agent:', err);\n notify.error(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 notify.error(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 notify.success(`Agent \"${agent.name}\" disconnected`);\n } catch (err) {\n console.error('Failed to clear tunnel fields:', err);\n notify.error(\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 // Confirmation happens through the ConfirmDialog rendered below\n // (bulkDeleteConfirmOpen) — this handler runs only after the user\n // confirms there.\n const handleBulkDelete = useCallback(async () => {\n if (selectedAgentIds.size === 0) 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 notify.error(\n `Removed ${succeeded}, failed to remove ${failed} agent${failed !== 1 ? 's' : ''}`\n );\n } else {\n notify.success(`Removed ${succeeded} agent${succeeded !== 1 ? 's' : ''}`);\n }\n setSelectedAgentIds(new Set());\n } catch (err) {\n notify.error(err instanceof Error ? err.message : 'Failed to remove agents');\n } finally {\n setBulkLoading(false);\n setBulkDeleteConfirmOpen(false);\n }\n }, [selectedAgentIds, deleteAgent]);\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 px-6 lg:px-8 py-6 bg-bg-canvas min-h-full\">\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 px-6 lg:px-8 py-6 bg-bg-canvas min-h-full\">\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 px-6 lg:px-8 py-6 bg-bg-canvas min-h-full\">\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 px-6 lg:px-8 py-6 bg-bg-canvas min-h-full\">\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 max-md:min-h-11 max-md:min-w-11 text-text-secondary hover:bg-bg-sunken hover:text-text-primary rounded-button transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n aria-label={tr('agentsPage.refreshTitle', 'Refresh agents')}\n title={tr('agentsPage.refreshTitle', 'Refresh agents')}\n >\n <RefreshCw\n aria-hidden=\"true\"\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-button hover:bg-action-primary-bgHover transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\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-button hover:bg-action-primary-bgHover transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\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-button p-1 flex-shrink-0 border border-border-subtle\">\n <button\n type=\"button\"\n onClick={() => {\n setViewMode('grid');\n setSelectedAgentIds(new Set());\n }}\n className={`flex items-center justify-center p-1.5 sm:p-2 max-md:min-h-11 max-md:min-w-11 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus ${viewMode === 'grid' ? 'bg-[var(--color-accent-soft)] text-action-primary-bg' : 'text-text-secondary hover:bg-bg-surface/60'}`}\n aria-label={tr('agentsPage.gridViewTitle', 'Grid view')}\n aria-pressed={viewMode === 'grid'}\n title={tr('agentsPage.gridViewTitle', 'Grid view')}\n >\n <Grid aria-hidden=\"true\" 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={`flex items-center justify-center p-1.5 sm:p-2 max-md:min-h-11 max-md:min-w-11 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus ${viewMode === 'list' ? 'bg-[var(--color-accent-soft)] text-action-primary-bg' : 'text-text-secondary hover:bg-bg-surface/60'}`}\n aria-label={tr('agentsPage.listViewTitle', 'List view')}\n aria-pressed={viewMode === 'list'}\n title={tr('agentsPage.listViewTitle', 'List view')}\n >\n <List aria-hidden=\"true\" 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 (grid view only — list view renders its own select-all\n affordance via AgentList's header checkbox / mobile button) */}\n {viewMode === 'grid' && filteredAgents.length > 0 && (\n <div className=\"flex justify-end\">\n <button\n type=\"button\"\n onClick={isAllSelected ? deselectAllAgents : selectAllAgents}\n aria-pressed={isAllSelected}\n className=\"flex items-center gap-1.5 px-3 py-1.5 max-md:min-h-11 text-xs text-text-secondary hover:text-text-primary rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n >\n <CheckSquare aria-hidden=\"true\" className=\"size-3.5\" />\n {isAllSelected\n ? tr('agentsPage.deselectAll', 'Deselect All')\n : tr('agentsPage.selectAll', 'Select All')}\n </button>\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.5 bg-[var(--color-accent-soft)] border border-border-subtle rounded-card\">\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={() => setBulkDeleteConfirmOpen(true)}\n disabled={bulkLoading}\n className=\"flex items-center gap-1.5 px-3 py-1.5 max-md:min-h-11 text-xs font-medium text-status-error-text bg-status-error-bg-subtle border border-status-error-border rounded-button hover:bg-status-error-bg-subtle/80 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n >\n <Trash2 aria-hidden=\"true\" 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 max-md:min-h-11 text-xs text-text-secondary hover:text-text-primary rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n >\n <XSquare aria-hidden=\"true\" className=\"size-3\" />{' '}\n {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 <AgentList\n agents={filteredAgents}\n onSelect={handleAgentSelect}\n onStart={handleStartAgent}\n onStop={handleStopAgent}\n onDisconnect={handleDisconnectAgent}\n onSettings={handleConfigureAgent}\n selectedIds={selectedAgentIds}\n onToggleSelect={toggleAgentSelection}\n onSelectAll={selectAllAgents}\n onDeselectAll={deselectAllAgents}\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-button hover:bg-action-primary-bgHover transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\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-subtle bg-bg-surface text-text-primary rounded-button hover:bg-bg-sunken hover:border-border-strong transition-all duration-200 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\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 Dialog */}\n <ConfirmDialog\n isOpen={disconnectConfirmAgent !== null}\n onClose={() => {\n if (!isDisconnecting) setDisconnectConfirmAgent(null);\n }}\n onConfirm={handleConfirmDisconnect}\n title={tr('agentsPage.disconnectTitle', 'Disconnect Agent')}\n message={\n disconnectConfirmAgent\n ? deriveAgentStatus(disconnectConfirmAgent) === 'OFFLINE'\n ? tr(\n 'agentsPage.disconnectOfflineMessage',\n 'Clear the stored tunnel connection for {{name}}? This will remove the tunnel URL and API key from this offline agent.',\n { name: disconnectConfirmAgent.name }\n )\n : tr(\n 'agentsPage.disconnectActiveMessage',\n 'Mark {{name}} as offline and clear its tunnel connection? The agent process will not be stopped automatically — you may need to stop it manually on the host machine.',\n { name: disconnectConfirmAgent.name }\n )\n : ''\n }\n confirmLabel={tr('agentsPage.disconnectConfirm', 'Disconnect')}\n variant=\"danger\"\n isLoading={isDisconnecting}\n />\n\n {/* Edit Tunnel Dialog */}\n <ResponsiveDialog\n open={editTunnelAgent !== null}\n onOpenChange={(open) => {\n if (!open) setEditTunnelAgent(null);\n }}\n >\n {editTunnelAgent ? (\n <ResponsiveDialogContent className=\"sm:max-w-md\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('agentsPage.editTunnelTitle', 'Edit Tunnel')}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {tr(\n 'agentsPage.editTunnelDescription',\n 'Configure the cloudflared tunnel URL and API key for {{name}}.',\n {\n name: editTunnelAgent.name,\n }\n )}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"py-4\">\n <label\n htmlFor={tunnelUrlField.id}\n className=\"mb-1.5 block text-sm font-medium text-text-primary\"\n >\n {tr('agentsPage.tunnelUrlLabel', 'Tunnel URL')}\n </label>\n <Input\n {...tunnelUrlField.inputProps}\n ref={tunnelUrlRef}\n type=\"url\"\n defaultValue={editTunnelAgent.tunnelUrl || ''}\n placeholder={tr(\n 'agentsPage.tunnelUrlPlaceholder',\n 'https://example.trycloudflare.com'\n )}\n className=\"text-base md:text-sm\"\n />\n <p id={tunnelUrlField.hintId} className=\"mt-1.5 text-xs text-text-muted\">\n {tr(\n 'agentsPage.tunnelUrlHint',\n 'Leave empty to clear. Agents with auto-tunnel report this automatically on startup.'\n )}\n </p>\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 <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setEditTunnelAgent(null)}\n className=\"max-md:min-h-11\"\n >\n {tr('agentsPage.cancel', 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n onClick={handleSaveTunnelSettings}\n disabled={isSavingTunnel}\n aria-busy={isSavingTunnel}\n className=\"max-md:min-h-11\"\n >\n {isSavingTunnel\n ? tr('agentsPage.saving', 'Saving...')\n : tr('agentsPage.save', 'Save')}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n ) : null}\n </ResponsiveDialog>\n\n {/* Bulk Delete Confirmation Dialog (replaces window.confirm) */}\n <ConfirmDialog\n isOpen={bulkDeleteConfirmOpen}\n onClose={() => {\n if (!bulkLoading) setBulkDeleteConfirmOpen(false);\n }}\n onConfirm={handleBulkDelete}\n title={tr('agentsPage.bulkDeleteTitle', 'Remove agents')}\n message={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 confirmLabel={tr('agentsPage.remove', 'Remove')}\n variant=\"danger\"\n isLoading={bulkLoading}\n />\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/plans');\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 { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { MotionPage, MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { AnimatePresence } from 'framer-motion';\nimport {\n Button,\n Input,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n} from '@burdenoff/fe-libs/ui';\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 ConfirmDialog,\n} from '@/components/shared';\nimport { notify } from '@/lib/toast';\nimport { useField } from '@/shared/hooks/useField';\nimport { useTr } from '@/shared/hooks/useTr';\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 { 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 bus = useEventBus();\n const { hasPermission, isLoading: permissionsLoading, permissions } = usePermissions();\n const tr = useTr();\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 const tunnelUrlField = useField({\n hint: tr(\n 'agentsPage.tunnelUrlHint',\n 'Leave empty to clear. Agents with auto-tunnel report this automatically on startup.'\n ),\n });\n\n // Bulk selection state\n const [selectedAgentIds, setSelectedAgentIds] = useState<Set<string>>(new Set());\n const [bulkLoading, setBulkLoading] = useState(false);\n const [bulkDeleteConfirmOpen, setBulkDeleteConfirmOpen] = useState(false);\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\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 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 bus.emitCustom('vibecontrols.agent.register_clicked', {\n workspaceId: workspaceId ?? undefined,\n });\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 bus.emitCustom('vibecontrols.agent.registered', {\n workspaceId: workspaceId ?? undefined,\n agentId: payload.agent.id,\n });\n bus.emitCustom('vibecontrols.agent.gateway_auth_setup', {\n workspaceId: workspaceId ?? undefined,\n agentId: payload.agent.id,\n });\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 bus.emitCustom('vibecontrols.agent.registered', {\n workspaceId: workspaceId ?? undefined,\n agentId: createdAgent.id,\n });\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 bus.emitCustom('vibecontrols.quota.agent_limit_hit', {\n workspaceId: workspaceId ?? undefined,\n });\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 bus.emitCustom('vibecontrols.agent.gateway_auth_setup', {\n workspaceId: workspaceId ?? undefined,\n agentId: agent.id,\n });\n await agentHeartbeat({ variables: { id: agent.id } });\n notify.success(`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 notify.success(`Agent \"${agent.name}\" started`);\n } catch (err) {\n console.error('Failed to start agent:', err);\n notify.error(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 notify.success(`Agent \"${agent.name}\" stopped`);\n } catch (err) {\n console.error('Failed to stop agent:', err);\n notify.error(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 notify.error(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 notify.success(`Agent \"${agent.name}\" disconnected`);\n } catch (err) {\n console.error('Failed to clear tunnel fields:', err);\n notify.error(\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 // Confirmation happens through the ConfirmDialog rendered below\n // (bulkDeleteConfirmOpen) — this handler runs only after the user\n // confirms there.\n const handleBulkDelete = useCallback(async () => {\n if (selectedAgentIds.size === 0) 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 notify.error(\n `Removed ${succeeded}, failed to remove ${failed} agent${failed !== 1 ? 's' : ''}`\n );\n } else {\n notify.success(`Removed ${succeeded} agent${succeeded !== 1 ? 's' : ''}`);\n }\n setSelectedAgentIds(new Set());\n } catch (err) {\n notify.error(err instanceof Error ? err.message : 'Failed to remove agents');\n } finally {\n setBulkLoading(false);\n setBulkDeleteConfirmOpen(false);\n }\n }, [selectedAgentIds, deleteAgent]);\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 px-6 lg:px-8 py-6 bg-bg-canvas min-h-full\">\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 px-6 lg:px-8 py-6 bg-bg-canvas min-h-full\">\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 px-6 lg:px-8 py-6 bg-bg-canvas min-h-full\">\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 px-6 lg:px-8 py-6 bg-bg-canvas min-h-full\">\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 max-md:min-h-11 max-md:min-w-11 text-text-secondary hover:bg-bg-sunken hover:text-text-primary rounded-button transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n aria-label={tr('agentsPage.refreshTitle', 'Refresh agents')}\n title={tr('agentsPage.refreshTitle', 'Refresh agents')}\n >\n <RefreshCw\n aria-hidden=\"true\"\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-button hover:bg-action-primary-bgHover transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\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-button hover:bg-action-primary-bgHover transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\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-button p-1 flex-shrink-0 border border-border-subtle\">\n <button\n type=\"button\"\n onClick={() => {\n setViewMode('grid');\n setSelectedAgentIds(new Set());\n }}\n className={`flex items-center justify-center p-1.5 sm:p-2 max-md:min-h-11 max-md:min-w-11 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus ${viewMode === 'grid' ? 'bg-[var(--color-accent-soft)] text-text-primary' : 'text-text-secondary hover:bg-bg-surface/60'}`}\n aria-label={tr('agentsPage.gridViewTitle', 'Grid view')}\n aria-pressed={viewMode === 'grid'}\n title={tr('agentsPage.gridViewTitle', 'Grid view')}\n >\n <Grid aria-hidden=\"true\" 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={`flex items-center justify-center p-1.5 sm:p-2 max-md:min-h-11 max-md:min-w-11 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus ${viewMode === 'list' ? 'bg-[var(--color-accent-soft)] text-text-primary' : 'text-text-secondary hover:bg-bg-surface/60'}`}\n aria-label={tr('agentsPage.listViewTitle', 'List view')}\n aria-pressed={viewMode === 'list'}\n title={tr('agentsPage.listViewTitle', 'List view')}\n >\n <List aria-hidden=\"true\" 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 (grid view only — list view renders its own select-all\n affordance via AgentList's header checkbox / mobile button) */}\n {viewMode === 'grid' && filteredAgents.length > 0 && (\n <div className=\"flex justify-end\">\n <button\n type=\"button\"\n onClick={isAllSelected ? deselectAllAgents : selectAllAgents}\n aria-pressed={isAllSelected}\n className=\"flex items-center gap-1.5 px-3 py-1.5 max-md:min-h-11 text-xs text-text-secondary hover:text-text-primary rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n >\n <CheckSquare aria-hidden=\"true\" className=\"size-3.5\" />\n {isAllSelected\n ? tr('agentsPage.deselectAll', 'Deselect All')\n : tr('agentsPage.selectAll', 'Select All')}\n </button>\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.5 bg-[var(--color-accent-soft)] border border-border-subtle rounded-card\">\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={() => setBulkDeleteConfirmOpen(true)}\n disabled={bulkLoading}\n className=\"flex items-center gap-1.5 px-3 py-1.5 max-md:min-h-11 text-xs font-medium text-status-error-text bg-status-error-bg-subtle border border-status-error-border rounded-button hover:bg-status-error-bg-subtle/80 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n >\n <Trash2 aria-hidden=\"true\" 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 max-md:min-h-11 text-xs text-text-secondary hover:text-text-primary rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n >\n <XSquare aria-hidden=\"true\" className=\"size-3\" />{' '}\n {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 <AgentList\n agents={filteredAgents}\n onSelect={handleAgentSelect}\n onStart={handleStartAgent}\n onStop={handleStopAgent}\n onDisconnect={handleDisconnectAgent}\n onSettings={handleConfigureAgent}\n selectedIds={selectedAgentIds}\n onToggleSelect={toggleAgentSelection}\n onSelectAll={selectAllAgents}\n onDeselectAll={deselectAllAgents}\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-button hover:bg-action-primary-bgHover transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\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-subtle bg-bg-surface text-text-primary rounded-button hover:bg-bg-sunken hover:border-border-strong transition-all duration-200 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\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 Dialog */}\n <ConfirmDialog\n isOpen={disconnectConfirmAgent !== null}\n onClose={() => {\n if (!isDisconnecting) setDisconnectConfirmAgent(null);\n }}\n onConfirm={handleConfirmDisconnect}\n title={tr('agentsPage.disconnectTitle', 'Disconnect Agent')}\n message={\n disconnectConfirmAgent\n ? deriveAgentStatus(disconnectConfirmAgent) === 'OFFLINE'\n ? tr(\n 'agentsPage.disconnectOfflineMessage',\n 'Clear the stored tunnel connection for {{name}}? This will remove the tunnel URL and API key from this offline agent.',\n { name: disconnectConfirmAgent.name }\n )\n : tr(\n 'agentsPage.disconnectActiveMessage',\n 'Mark {{name}} as offline and clear its tunnel connection? The agent process will not be stopped automatically — you may need to stop it manually on the host machine.',\n { name: disconnectConfirmAgent.name }\n )\n : ''\n }\n confirmLabel={tr('agentsPage.disconnectConfirm', 'Disconnect')}\n variant=\"danger\"\n isLoading={isDisconnecting}\n />\n\n {/* Edit Tunnel Dialog */}\n <ResponsiveDialog\n open={editTunnelAgent !== null}\n onOpenChange={(open) => {\n if (!open) setEditTunnelAgent(null);\n }}\n >\n {editTunnelAgent ? (\n <ResponsiveDialogContent className=\"sm:max-w-md\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('agentsPage.editTunnelTitle', 'Edit Tunnel')}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {tr(\n 'agentsPage.editTunnelDescription',\n 'Configure the cloudflared tunnel URL and API key for {{name}}.',\n {\n name: editTunnelAgent.name,\n }\n )}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"py-4\">\n <label\n htmlFor={tunnelUrlField.id}\n className=\"mb-1.5 block text-sm font-medium text-text-primary\"\n >\n {tr('agentsPage.tunnelUrlLabel', 'Tunnel URL')}\n </label>\n <Input\n {...tunnelUrlField.inputProps}\n ref={tunnelUrlRef}\n type=\"url\"\n defaultValue={editTunnelAgent.tunnelUrl || ''}\n placeholder={tr(\n 'agentsPage.tunnelUrlPlaceholder',\n 'https://example.trycloudflare.com'\n )}\n className=\"text-base md:text-sm\"\n />\n <p id={tunnelUrlField.hintId} className=\"mt-1.5 text-xs text-text-muted\">\n {tr(\n 'agentsPage.tunnelUrlHint',\n 'Leave empty to clear. Agents with auto-tunnel report this automatically on startup.'\n )}\n </p>\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 <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setEditTunnelAgent(null)}\n className=\"max-md:min-h-11\"\n >\n {tr('agentsPage.cancel', 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n onClick={handleSaveTunnelSettings}\n disabled={isSavingTunnel}\n aria-busy={isSavingTunnel}\n className=\"max-md:min-h-11\"\n >\n {isSavingTunnel\n ? tr('agentsPage.saving', 'Saving...')\n : tr('agentsPage.save', 'Save')}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n ) : null}\n </ResponsiveDialog>\n\n {/* Bulk Delete Confirmation Dialog (replaces window.confirm) */}\n <ConfirmDialog\n isOpen={bulkDeleteConfirmOpen}\n onClose={() => {\n if (!bulkLoading) setBulkDeleteConfirmOpen(false);\n }}\n onConfirm={handleBulkDelete}\n title={tr('agentsPage.bulkDeleteTitle', 'Remove agents')}\n message={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 confirmLabel={tr('agentsPage.remove', 'Remove')}\n variant=\"danger\"\n isLoading={bulkLoading}\n />\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/plans');\n }}\n title={getQuotaDialogContent(quotaExhaustedKind ?? 'generic').title}\n message={getQuotaDialogContent(quotaExhaustedKind ?? 'generic').message}\n />\n </MotionPage>\n );\n}\n"],"mappings":"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"}
|
|
@@ -256,7 +256,7 @@ function W() {
|
|
|
256
256
|
/* @__PURE__ */ A(t, { className: "size-4" }),
|
|
257
257
|
e.label,
|
|
258
258
|
e.badge && /* @__PURE__ */ A("span", {
|
|
259
|
-
className: `hidden rounded-full px-1.5 py-0.5 text-[10px] sm:inline-flex sm:text-xs ${n ? "bg-[var(--color-accent-soft)] text-
|
|
259
|
+
className: `hidden rounded-full px-1.5 py-0.5 text-[10px] sm:inline-flex sm:text-xs ${n ? "bg-[var(--color-accent-soft)] text-text-primary" : "bg-bg-sunken text-text-muted"}`,
|
|
260
260
|
children: e.badge
|
|
261
261
|
})
|
|
262
262
|
]
|