@burdenoff/microfe-vibecontrols 2026.601.6 → 2026.601.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/agents/AgentForm.js +1 -1
- package/dist/components/agents/AgentForm.js.map +1 -1
- package/dist/components/sessions/EntityInfoPopover.js +183 -0
- package/dist/components/sessions/EntityInfoPopover.js.map +1 -0
- package/dist/components/sessions/SessionCard.js +80 -72
- package/dist/components/sessions/SessionCard.js.map +1 -1
- package/dist/components/sessions/SessionGroup.js +49 -0
- package/dist/components/sessions/SessionGroup.js.map +1 -0
- package/dist/components/sessions/SessionInfoPanel.js +128 -0
- package/dist/components/sessions/SessionInfoPanel.js.map +1 -0
- package/dist/components/sessions/SessionList.js +109 -96
- package/dist/components/sessions/SessionList.js.map +1 -1
- package/dist/components/sessions/SessionsCommandPalette.js +36 -0
- package/dist/components/sessions/SessionsCommandPalette.js.map +1 -0
- package/dist/components/sessions/ShortcutCheatsheet.js +73 -0
- package/dist/components/sessions/ShortcutCheatsheet.js.map +1 -0
- package/dist/components/sessions/TerminalPanel.js +640 -537
- package/dist/components/sessions/TerminalPanel.js.map +1 -1
- package/dist/components/sessions/groupSessions.js +46 -0
- package/dist/components/sessions/groupSessions.js.map +1 -0
- package/dist/components/sessions/index.js +7 -0
- package/dist/components/sessions/sessionShortcuts.js +149 -0
- package/dist/components/sessions/sessionShortcuts.js.map +1 -0
- package/dist/components/shared/AgentSetupBanner.js +1 -1
- package/dist/components/shared/AgentSetupBanner.js.map +1 -1
- package/dist/components/shared/FilterDropdown.js +14 -13
- package/dist/components/shared/FilterDropdown.js.map +1 -1
- package/dist/components/shared/InstallTabs.js +2 -2
- package/dist/components/shared/InstallTabs.js.map +1 -1
- package/dist/components/shared/LinkToVibePanel.js.map +1 -1
- package/dist/components/vibe/VibeLinkedEntitiesPanel.js.map +1 -1
- package/dist/generated/wspace-operations.js +21 -0
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/pages/SessionsPage.js +1189 -920
- package/dist/pages/SessionsPage.js.map +1 -1
- package/dist/pages/ai/AgentManagerPage.js.map +1 -1
- package/dist/store/vibeControlsStore.js +16 -1
- package/dist/store/vibeControlsStore.js.map +1 -1
- package/dist/translations/en.js +97 -2
- package/dist/translations/en.js.map +1 -1
- package/package.json +1 -1
|
@@ -191,7 +191,7 @@ function m({ onSubmit: m, onCancel: h, isLoading: me, onNavigate: g, targets: _,
|
|
|
191
191
|
className: "space-y-2",
|
|
192
192
|
children: [[{
|
|
193
193
|
label: b("agentsPage.installStep1", "1. Install the agent CLI"),
|
|
194
|
-
cmd: "curl -fsSL https://app.vibecontrols.com/install | bash"
|
|
194
|
+
cmd: "curl -fsSL https://app.vibecontrols.com/install | bash && hash -r 2>/dev/null"
|
|
195
195
|
}, {
|
|
196
196
|
label: b("agentsPage.installStep2", "2. Start the agent (prints tunnel URL + API key)"),
|
|
197
197
|
cmd: "vibe start"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentForm.js","names":[],"sources":["../../../src/components/agents/AgentForm.tsx"],"sourcesContent":["import { useEffect, useRef, useState, useCallback } from 'react';\nimport type { ReactNode } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n X,\n Eye,\n EyeOff,\n CheckCircle2,\n XCircle,\n Loader2,\n ChevronRight,\n ChevronLeft,\n ChevronDown,\n Copy,\n Check,\n Terminal,\n Download,\n RefreshCw,\n Plus,\n Settings2,\n} from 'lucide-react';\nimport { useProbeAgentTunnelLazyQuery } from '@/generated/wspace-operations';\nimport { SearchableSelect } from '@/components/shared';\nimport type { SearchableSelectOption } from '@/components/shared';\nimport { isQuotaExhaustedError, wasQuotaHandledLocally } from '@/utils/quotaUtils';\n\ntype WizardStage = 'details' | 'validate';\n\ninterface TargetOption {\n id: string;\n name: string;\n type: string;\n status: string;\n}\n\ninterface AgentFormProps {\n onSubmit: (data: Record<string, unknown>) => Promise<{ id: string } | void>;\n onCancel: () => void;\n isLoading?: boolean;\n /** Navigate to a path (used to go to agent details after creation) */\n onNavigate?: (path: string) => void;\n /** Available targets for the agent to be installed on */\n targets?: TargetOption[];\n /** Whether targets are still loading */\n targetsLoading?: boolean;\n /**\n * Create a new target inline. When provided, the form shows a\n * \"+ Create new target\" affordance that lets users create a DIRECT\n * target on the fly without leaving the dialog. Must return the new\n * target so the form can auto-select it.\n */\n onCreateTarget?: (name: string) => Promise<TargetOption | null>;\n /** Optional extra content (e.g. a draft EntityTagPicker) rendered above the configure-stage actions. */\n extraSection?: ReactNode;\n}\n\nexport function AgentForm({\n onSubmit,\n onCancel,\n isLoading,\n onNavigate,\n targets,\n targetsLoading,\n onCreateTarget,\n extraSection,\n}: AgentFormProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n const [stage, setStage] = useState<WizardStage>('details');\n const [showApiKey, setShowApiKey] = useState(false);\n const [showInstallInstructions, setShowInstallInstructions] = useState(false);\n const [copiedCommand, setCopiedCommand] = useState<string | null>(null);\n\n // Stage 1: Basic details\n const [name, setName] = useState('');\n // Tracks whether the operator has typed into the name field. Once true,\n // we stop overwriting the value from auto-fill sources (target select +\n // identity probe) so we never clobber their input.\n const [nameEdited, setNameEdited] = useState(false);\n const [tunnelUrl, setTunnelUrl] = useState('');\n const [agentApiKey, setAgentApiKey] = useState('');\n const [selectedTargetId, setSelectedTargetId] = useState('');\n\n // When the operator picks a target, autofill the agent name with the\n // target's display name. Skip if they've already typed something into\n // the name field. The svc append a `-2`/`-3` suffix on collision so\n // it's safe to suggest a duplicate; the actual stored name comes back\n // on the create response.\n useEffect(() => {\n if (nameEdited) return;\n if (!selectedTargetId) return;\n const picked = (targets ?? []).find((t) => t.id === selectedTargetId);\n if (picked?.name) setName(picked.name);\n }, [selectedTargetId, targets, nameEdited]);\n\n // Inline target creation state\n const [showInlineTarget, setShowInlineTarget] = useState(false);\n const [inlineTargetName, setInlineTargetName] = useState('');\n const [creatingTarget, setCreatingTarget] = useState(false);\n const [inlineTargetError, setInlineTargetError] = useState<string | null>(null);\n\n // Stage 2: Validation (via backend-proxied GraphQL query)\n const [validating, setValidating] = useState(false);\n const [healthResult, setHealthResult] = useState<{\n ok: boolean;\n data?: { status: string; version: string; uptime: number };\n error?: string;\n } | null>(null);\n const [identityResult, setIdentityResult] = useState<{\n ok: boolean;\n data?: { hostname: string; platform: string; arch: string };\n error?: string;\n } | null>(null);\n\n // Auto-detected fields (editable overrides)\n const [detectedHostname, setDetectedHostname] = useState('');\n const [detectedPlatform, setDetectedPlatform] = useState('linux');\n const [detectedArch, setDetectedArch] = useState('x86_64');\n const [detectedVersion, setDetectedVersion] = useState('1.0.0');\n\n // Error state for creation failures\n const [createError, setCreateError] = useState<string | null>(null);\n\n // Advanced options (Phase 5B: per-daemon profile name).\n // Defaults to \"default\" (autofilled but editable). Disclosure is closed\n // by default — users only need to touch this for multi-profile setups.\n const [showAdvanced, setShowAdvanced] = useState(false);\n const [profile, setProfile] = useState('default');\n\n const copyToClipboard = useCallback((text: string, id: string) => {\n navigator.clipboard.writeText(text);\n setCopiedCommand(id);\n setTimeout(() => setCopiedCommand(null), 2000);\n }, []);\n\n const normalizeUrl = (url: string) => {\n let u = url.trim();\n if (u.endsWith('/')) u = u.slice(0, -1);\n return u;\n };\n\n const [runProbe] = useProbeAgentTunnelLazyQuery({ fetchPolicy: 'network-only' });\n\n // Validate the agent through the svc-proxied `probeAgentTunnel` query.\n // svc fans out to /health + /api/profiles/default/agent/identity over\n // its own egress and returns a combined AgentProbeResult — the\n // operator's API key never touches the browser→tunnel path.\n const validateAgent = async () => {\n const baseUrl = normalizeUrl(tunnelUrl);\n if (!baseUrl) return;\n\n setValidating(true);\n setHealthResult(null);\n setIdentityResult(null);\n\n try {\n const { data, error } = await runProbe({\n variables: { url: baseUrl, agentApiKey: agentApiKey || null },\n });\n const probe = data?.probeAgentTunnel;\n if (error || !probe) {\n const msg = error?.message ?? 'Probe failed';\n setHealthResult({ ok: false, error: msg });\n setIdentityResult({ ok: false, error: msg });\n } else {\n setHealthResult(\n probe.healthOk\n ? {\n ok: true,\n data: {\n status: probe.healthStatus ?? 'healthy',\n version: probe.agentVersion ?? 'unknown',\n uptime: probe.uptime ?? 0,\n },\n }\n : { ok: false, error: probe.healthError ?? 'Health check failed' }\n );\n if (probe.agentVersion) setDetectedVersion(probe.agentVersion);\n\n setIdentityResult(\n probe.identityOk\n ? {\n ok: true,\n data: {\n hostname: probe.hostname ?? '',\n platform: probe.platform ?? 'linux',\n arch: probe.architecture ?? 'x86_64',\n },\n }\n : { ok: false, error: probe.identityError ?? 'Identity check failed' }\n );\n if (probe.identityOk) {\n if (probe.platform) setDetectedPlatform(probe.platform);\n if (probe.architecture) setDetectedArch(probe.architecture);\n // Auto-fill name from probe hostname unless (a) the operator\n // has manually typed one, or (b) a target is already selected\n // (target wins as the canonical autogen value).\n if (probe.hostname) {\n setDetectedHostname(probe.hostname);\n if (!nameEdited && !selectedTargetId) setName(probe.hostname);\n }\n }\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Probe failed';\n setHealthResult({ ok: false, error: msg });\n setIdentityResult({ ok: false, error: msg });\n }\n\n setValidating(false);\n };\n\n // Refs let us short-circuit double-submits before React re-renders\n // the button as disabled. The parent passes `isLoading` (driven by\n // `useState`) and we wire it to the button's `disabled` prop, but\n // `setState` is asynchronous — between the user's first click and\n // React's next commit there is a microtask window in which the button\n // is still clickable. Without this ref a fast double-click fires\n // `addAgentFromTunnel` twice in parallel; the svc sees one in-flight\n // and one fresh call, the second one wins the (workspaceId, name)\n // unique-resolution race, and the workspace ends up with two agent\n // rows (the original autogen + the operator's custom name).\n const submittingRef = useRef(false);\n\n // Create the agent and close the modal\n const handleCreateAgent = async () => {\n if (submittingRef.current) return;\n submittingRef.current = true;\n setCreateError(null);\n\n // Pipe the chosen profile through `metadata.profile`. Phase 5A (svc) is\n // adding first-class AgentProfile rows; in the meantime the backend\n // also reads `metadata.profile` so the create path keeps working\n // regardless of which side of the cut-over the cluster is on.\n const trimmedProfile = profile.trim() || 'default';\n const data: Record<string, unknown> = {\n name: name || detectedHostname || 'Unnamed Agent',\n hostname: detectedHostname || 'unknown',\n platform: detectedPlatform,\n architecture: detectedArch,\n version: detectedVersion,\n tunnelUrl: normalizeUrl(tunnelUrl),\n tunnelStatus: 'ACTIVE',\n agentApiKey,\n metadata: { profile: trimmedProfile },\n ...(selectedTargetId ? { targetId: selectedTargetId } : {}),\n };\n\n try {\n const result = await onSubmit(data);\n const newAgentId = result?.id || null;\n\n if (!newAgentId) {\n // Mutation succeeded but returned no agent ID — treat as failure\n setCreateError('Agent creation returned no ID. The agent may already exist.');\n return;\n }\n\n // Close the modal — the agent list will refetch automatically\n if (onNavigate) {\n onNavigate(`/agents/${newAgentId}`);\n } else {\n onCancel();\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to create agent';\n setCreateError(message);\n } finally {\n submittingRef.current = false;\n }\n };\n\n const isValidationPassed = healthResult?.ok && identityResult?.ok;\n const hasTargets = targets && targets.length > 0;\n // The user must either pick or inline-create a target if inline creation\n // is enabled; otherwise we only require it when targets already exist.\n const targetRequiredButMissing = onCreateTarget\n ? selectedTargetId.length === 0\n : hasTargets && selectedTargetId.length === 0;\n const canProceedToValidate = tunnelUrl.trim().length > 0 && !targetRequiredButMissing;\n\n const handleInlineCreateTarget = useCallback(async () => {\n if (!onCreateTarget || !inlineTargetName.trim()) return;\n setCreatingTarget(true);\n setInlineTargetError(null);\n try {\n const created = await onCreateTarget(inlineTargetName.trim());\n if (created) {\n setSelectedTargetId(created.id);\n setShowInlineTarget(false);\n setInlineTargetName('');\n } else if (wasQuotaHandledLocally()) {\n // Parent surfaced the quota modal — collapse the inline row\n // quietly so the user isn't left with a vague \"try again\"\n // message under an explicit upgrade-your-plan dialog.\n setShowInlineTarget(false);\n setInlineTargetName('');\n } else {\n setInlineTargetError(\n tr('agentsPage.inlineTargetFailed', 'Failed to create target. Please try again.')\n );\n }\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n // Quota dialog will (or already has) opened at the parent;\n // suppress the generic inline error so the two don't compete.\n setShowInlineTarget(false);\n setInlineTargetName('');\n } else {\n setInlineTargetError(err instanceof Error ? err.message : 'Failed to create target');\n }\n } finally {\n setCreatingTarget(false);\n }\n }, [onCreateTarget, inlineTargetName, tr]);\n\n const renderCopyButton = (text: string, id: string) => (\n <button\n type=\"button\"\n onClick={() => copyToClipboard(text, id)}\n className=\"p-1 rounded hover:bg-bg-elevated transition-colors\"\n title={tr('agentsPage.copy', 'Copy')}\n >\n {copiedCommand === id ? (\n <Check className=\"size-3.5 text-status-success-text\" />\n ) : (\n <Copy className=\"size-3.5 text-text-secondary\" />\n )}\n </button>\n );\n\n // Stepper\n const steps = [\n { key: 'details', label: 'Setup' },\n { key: 'validate', label: 'Validate' },\n ];\n\n const currentStepIndex = steps.findIndex((s) => s.key === stage);\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay\">\n <div className=\"w-full max-w-xl bg-bg-surface rounded-lg shadow-xl max-h-[90vh] overflow-y-auto\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-border-default sticky top-0 bg-bg-surface z-10\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('agentsPage.addNewAgent', 'Add New Agent')}\n </h2>\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"p-1 rounded hover:bg-bg-sunken transition-colors\"\n >\n <X className=\"size-5 text-text-secondary\" />\n </button>\n </div>\n\n {/* Stepper */}\n <div className=\"flex items-center gap-2 px-6 pt-4 pb-2\">\n {steps.map((step, i) => (\n <div key={step.key} className=\"flex items-center gap-2\">\n <div\n className={`flex items-center justify-center size-7 rounded-full text-xs font-semibold transition-colors ${\n i < currentStepIndex\n ? 'bg-status-success-bg text-status-success-text'\n : i === currentStepIndex\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary'\n }`}\n >\n {i < currentStepIndex ? <CheckCircle2 className=\"size-4\" /> : i + 1}\n </div>\n <span\n className={`text-sm ${i === currentStepIndex ? 'font-medium text-text-primary' : 'text-text-secondary'}`}\n >\n {step.label}\n </span>\n {i < steps.length - 1 && <ChevronRight className=\"size-4 text-text-muted\" />}\n </div>\n ))}\n </div>\n\n <div className=\"p-6\">\n {/* Stage 1: Setup */}\n {stage === 'details' && (\n <div className=\"space-y-4\">\n {/* Install instructions (collapsible) */}\n <div className=\"border border-border-default rounded-lg overflow-hidden\">\n <button\n type=\"button\"\n onClick={() => setShowInstallInstructions(!showInstallInstructions)}\n className=\"w-full flex items-center justify-between px-4 py-3 bg-bg-sunken hover:bg-bg-sunken/80 transition-colors\"\n >\n <div className=\"flex items-center gap-2\">\n <Download className=\"size-4 text-action-primary-bg\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('agentsPage.installInstructions', 'Agent Install Instructions')}\n </span>\n </div>\n <ChevronRight\n className={`size-4 text-text-secondary transition-transform ${showInstallInstructions ? 'rotate-90' : ''}`}\n />\n </button>\n {showInstallInstructions && (\n <div className=\"px-4 py-3 space-y-3 border-t border-border-default\">\n <div className=\"space-y-2\">\n {[\n {\n label: tr('agentsPage.installStep1', '1. Install the agent CLI'),\n cmd: 'curl -fsSL https://app.vibecontrols.com/install | bash',\n },\n {\n label: tr(\n 'agentsPage.installStep2',\n '2. Start the agent (prints tunnel URL + API key)'\n ),\n cmd: 'vibe start',\n },\n ].map((step) => (\n <div key={step.cmd}>\n <p className=\"text-xs text-text-secondary mb-1\">{step.label}</p>\n <div className=\"flex items-center gap-2 bg-bg-sunken rounded-md px-3 py-1.5 font-mono text-xs text-text-primary\">\n <Terminal className=\"size-3 text-text-muted flex-shrink-0\" />\n <code className=\"flex-1\">{step.cmd}</code>\n {renderCopyButton(step.cmd, step.cmd)}\n </div>\n </div>\n ))}\n <p className=\"text-xs text-text-secondary pt-1\">\n {tr(\n 'agentsPage.installHint',\n 'After running vibe start, copy the printed tunnel URL and API key into the fields above.'\n )}\n </p>\n </div>\n </div>\n )}\n </div>\n\n {/* Agent Name — visible upfront so operators see (and can\n override) the autogen before validation. The autofill\n chain still runs in the background: picking a target\n pulls in its name; clicking Validate falls back to the\n probed hostname when the user hasn't typed anything.\n The svc dedupes inside the workspace via\n resolveUniqueAgentName(), so suffixes like `-2` may\n still get appended at submit time even when the user\n picked their own string. */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('agentsPage.name', 'Name')}\n </label>\n <input\n aria-label={tr('agentsPage.name', 'Name')}\n type=\"text\"\n value={name}\n onChange={(e) => {\n setName(e.target.value);\n setNameEdited(true);\n }}\n placeholder={tr(\n 'agentsPage.namePlaceholderUpfront',\n 'My laptop, prod jump-host, …'\n )}\n className=\"w-full px-3 py-2 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n data-testid=\"agent-form-name-input\"\n />\n <p className=\"mt-1 text-xs text-text-muted\">\n {tr(\n 'agentsPage.nameHint',\n \"How this agent shows up in the UI. Defaults to the target's name (or the machine's hostname after Validate) — leave blank to accept the autogen.\"\n )}\n </p>\n </div>\n\n {/* Target Selection — with inline creation */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('agentsPage.target', 'Target')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n\n {hasTargets && !showInlineTarget && (\n <>\n <SearchableSelect\n placeholder={tr('agentsPage.selectTarget', 'Select a target...')}\n searchPlaceholder={tr('agentsPage.searchTargets', 'Search targets...')}\n options={(targets ?? []).map(\n (tgt): SearchableSelectOption => ({\n value: tgt.id,\n label: tgt.name,\n description: `${tgt.type}${tgt.status ? ` · ${tgt.status}` : ''}`,\n })\n )}\n value={selectedTargetId}\n onChange={setSelectedTargetId}\n clearable\n />\n <p className=\"mt-1 text-xs text-text-muted\">\n {tr(\n 'agentsPage.targetDescription',\n 'The target machine this agent will be installed on.'\n )}{' '}\n {onCreateTarget && (\n <button\n type=\"button\"\n onClick={() => {\n setShowInlineTarget(true);\n setInlineTargetName(name.trim() || '');\n }}\n className=\"text-action-primary-bg hover:underline inline-flex items-center gap-1\"\n >\n <Plus className=\"size-3\" />\n {tr('agentsPage.createNewTarget', 'Create a new target')}\n </button>\n )}\n </p>\n </>\n )}\n\n {/* Empty state prompt (no targets exist yet) */}\n {!hasTargets && !targetsLoading && onCreateTarget && !showInlineTarget && (\n <div className=\"p-3 bg-bg-sunken border border-border-default rounded-lg\">\n <p className=\"text-sm text-text-secondary mb-2\">\n {tr(\n 'agentsPage.noTargetsInlinePrompt',\n \"You don't have any targets yet. Create one now to install this agent on it.\"\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => {\n setShowInlineTarget(true);\n setInlineTargetName(name.trim() || '');\n }}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:opacity-90\"\n >\n <Plus className=\"size-4\" />\n {tr('agentsPage.createFirstTarget', 'Create first target')}\n </button>\n </div>\n )}\n\n {/* Inline creation form */}\n {showInlineTarget && onCreateTarget && (\n <div className=\"p-3 bg-bg-sunken border border-border-default rounded-lg space-y-2\">\n <p className=\"text-xs font-medium text-text-secondary\">\n {tr(\n 'agentsPage.newDirectTarget',\n 'New target (Direct — you can change the type later)'\n )}\n </p>\n <input\n aria-label=\"control\"\n type=\"text\"\n value={inlineTargetName}\n onChange={(e) => setInlineTargetName(e.target.value)}\n placeholder={tr('agentsPage.targetName', 'Target name')}\n className=\"w-full px-3 py-2 text-sm bg-bg-elevated border border-border-default rounded-md text-text-primary placeholder-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n void handleInlineCreateTarget();\n }\n }}\n />\n {inlineTargetError && (\n <p className=\"text-xs text-status-error-text\">{inlineTargetError}</p>\n )}\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => void handleInlineCreateTarget()}\n disabled={!inlineTargetName.trim() || creatingTarget}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:opacity-90 disabled:opacity-50\"\n >\n {creatingTarget ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <Check className=\"size-3.5\" />\n )}\n {tr('agentsPage.create', 'Create')}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setShowInlineTarget(false);\n setInlineTargetName('');\n setInlineTargetError(null);\n }}\n className=\"px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary\"\n >\n {tr('agentsPage.cancel', 'Cancel')}\n </button>\n </div>\n </div>\n )}\n\n {/* Loading placeholder */}\n {targetsLoading && !hasTargets && (\n <div className=\"p-3 bg-bg-sunken border border-border-default rounded-lg flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n {tr('agentsPage.loadingTargets', 'Loading targets...')}\n </div>\n )}\n </div>\n\n {/* API/Tunnel URL */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('agentsPage.apiTunnelUrl', 'API / Tunnel URL')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n aria-label=\"https://your-tunnel.trycloudflare.com\"\n type=\"url\"\n value={tunnelUrl}\n onChange={(e) => setTunnelUrl(e.target.value)}\n placeholder=\"https://your-tunnel.trycloudflare.com\"\n required\n className=\"w-full px-3 py-2 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n />\n <p className=\"mt-1 text-xs text-text-muted\">\n {tr(\n 'agentsPage.tunnelUrlHint',\n \"The agent's tunnel URL or local API URL (e.g. http://localhost:3005)\"\n )}\n </p>\n </div>\n\n {/* Agent API Key */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('agentsPage.agentApiKey', 'Agent API Key')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <div className=\"relative\">\n <input\n aria-label=\"vcak_...\"\n type={showApiKey ? 'text' : 'password'}\n value={agentApiKey}\n onChange={(e) => setAgentApiKey(e.target.value)}\n placeholder=\"vcak_...\"\n required\n className=\"w-full px-3 py-2 pr-10 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n />\n <button\n type=\"button\"\n onClick={() => setShowApiKey(!showApiKey)}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded hover:bg-bg-elevated transition-colors\"\n >\n {showApiKey ? (\n <EyeOff className=\"size-4 text-text-secondary\" />\n ) : (\n <Eye className=\"size-4 text-text-secondary\" />\n )}\n </button>\n </div>\n <p className=\"mt-1 text-xs text-text-muted\">\n {tr('agentsPage.apiKeyHint', 'Run')}{' '}\n <code className=\"bg-bg-sunken px-1 rounded\">vibe key</code>{' '}\n {tr(\n 'agentsPage.apiKeyHintSuffix',\n 'on the agent to get this. Changes on restart.'\n )}\n </p>\n </div>\n\n {/* Advanced options — per-daemon profile name (Phase 5B) */}\n <div className=\"border border-border-default rounded-lg overflow-hidden\">\n <button\n type=\"button\"\n onClick={() => setShowAdvanced((v) => !v)}\n className=\"w-full flex items-center justify-between px-4 py-2.5 bg-bg-sunken hover:bg-bg-sunken/80 transition-colors\"\n aria-expanded={showAdvanced}\n data-testid=\"agent-form-advanced-toggle\"\n >\n <div className=\"flex items-center gap-2\">\n <Settings2 className=\"size-4 text-text-secondary\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('agentsPage.advancedOptions', 'Advanced options')}\n </span>\n </div>\n <ChevronDown\n className={`size-4 text-text-secondary transition-transform ${showAdvanced ? 'rotate-180' : ''}`}\n />\n </button>\n {showAdvanced && (\n <div className=\"px-4 py-3 space-y-3 border-t border-border-default\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('agentsPage.profileName', 'Profile name')}\n </label>\n <input\n aria-label=\"default\"\n type=\"text\"\n value={profile}\n onChange={(e) => setProfile(e.target.value)}\n placeholder=\"default\"\n className=\"w-full px-3 py-2 text-sm bg-bg-surface border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n data-testid=\"agent-form-profile-input\"\n />\n <p className=\"mt-1 text-xs text-text-muted\">\n {tr(\n 'agentsPage.profileNameHint',\n 'Each profile runs an isolated CLI on the daemon. Leave as \"default\" unless you want this agent to host multiple parallel profiles.'\n )}\n </p>\n </div>\n </div>\n )}\n </div>\n\n {extraSection && (\n <div className=\"border-t border-border-default pt-4 mt-4\">{extraSection}</div>\n )}\n\n {/* Actions */}\n <div className=\"flex items-center justify-between pt-4\">\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"px-4 py-2 text-sm font-medium text-text-primary bg-bg-sunken border border-border-default rounded-md hover:bg-bg-elevated transition-colors\"\n >\n {tr('agentsPage.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n disabled={!canProceedToValidate}\n onClick={() => {\n setStage('validate');\n validateAgent();\n }}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n >\n {tr('agentsPage.nextValidate', 'Next: Validate')}\n <ChevronRight className=\"size-4\" />\n </button>\n </div>\n </div>\n )}\n\n {/* Stage 2: Validate */}\n {stage === 'validate' && (\n <div className=\"space-y-4\">\n {/* Validation Results */}\n <div className=\"space-y-3\">\n {/* Health Check */}\n <div\n className={`flex items-center gap-3 p-3 rounded-lg border ${\n healthResult === null\n ? 'border-border-default bg-bg-sunken'\n : healthResult.ok\n ? 'border-status-success-text/20 bg-status-success-bg/30'\n : 'border-status-error-text/20 bg-status-error-bg/30'\n }`}\n >\n {validating && !healthResult ? (\n <Loader2 className=\"size-5 text-text-secondary animate-spin\" />\n ) : healthResult?.ok ? (\n <CheckCircle2 className=\"size-5 text-status-success-text\" />\n ) : healthResult ? (\n <XCircle className=\"size-5 text-status-error-text\" />\n ) : (\n <div className=\"size-5 rounded-full border-2 border-border-default\" />\n )}\n <div className=\"flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('agentsPage.healthCheck', 'Health Check')}\n </p>\n {healthResult?.ok && healthResult.data && (\n <p className=\"text-xs text-text-secondary\">\n Status: {healthResult.data.status} | Version: {healthResult.data.version} |\n Uptime: {Math.round(healthResult.data.uptime)}s\n </p>\n )}\n {healthResult && !healthResult.ok && (\n <p className=\"text-xs text-status-error-text\">{healthResult.error}</p>\n )}\n </div>\n </div>\n\n {/* API Access Check */}\n <div\n className={`flex items-center gap-3 p-3 rounded-lg border ${\n identityResult === null\n ? 'border-border-default bg-bg-sunken'\n : identityResult.ok\n ? 'border-status-success-text/20 bg-status-success-bg/30'\n : 'border-status-error-text/20 bg-status-error-bg/30'\n }`}\n >\n {validating && !identityResult ? (\n <Loader2 className=\"size-5 text-text-secondary animate-spin\" />\n ) : identityResult?.ok ? (\n <CheckCircle2 className=\"size-5 text-status-success-text\" />\n ) : identityResult ? (\n <XCircle className=\"size-5 text-status-error-text\" />\n ) : (\n <div className=\"size-5 rounded-full border-2 border-border-default\" />\n )}\n <div className=\"flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('agentsPage.apiAccess', 'API Access')}\n </p>\n {identityResult?.ok && identityResult.data && (\n <p className=\"text-xs text-text-secondary\">\n Host: {identityResult.data.hostname} | {identityResult.data.platform} /{' '}\n {identityResult.data.arch}\n </p>\n )}\n {identityResult && !identityResult.ok && (\n <p className=\"text-xs text-status-error-text\">{identityResult.error}</p>\n )}\n </div>\n </div>\n </div>\n\n {/* Retry button */}\n {!validating && (!healthResult?.ok || !identityResult?.ok) && (\n <button\n type=\"button\"\n onClick={validateAgent}\n className=\"flex items-center gap-2 px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-md transition-colors\"\n >\n <RefreshCw className=\"size-3.5\" />\n {tr('agentsPage.retryValidation', 'Retry validation')}\n </button>\n )}\n\n {/* Detected info (editable overrides) — Name lives upfront\n now; this section shows the validate-time fields only. */}\n {isValidationPassed && (\n <div className=\"space-y-3 pt-2\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('agentsPage.detectedAgentInfo', 'Detected Agent Info')}\n </p>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label className=\"block text-xs font-medium text-text-secondary mb-1\">\n {tr('agentsPage.platform', 'Platform')}\n </label>\n <select\n value={detectedPlatform}\n onChange={(e) => setDetectedPlatform(e.target.value)}\n className=\"w-full px-3 py-1.5 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"linux\">Linux</option>\n <option value=\"darwin\">macOS</option>\n <option value=\"windows\">Windows</option>\n </select>\n </div>\n <div>\n <label className=\"block text-xs font-medium text-text-secondary mb-1\">\n {tr('agentsPage.architecture', 'Architecture')}\n </label>\n <select\n value={detectedArch}\n onChange={(e) => setDetectedArch(e.target.value)}\n className=\"w-full px-3 py-1.5 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"x86_64\">x86_64</option>\n <option value=\"arm64\">arm64</option>\n <option value=\"aarch64\">aarch64</option>\n </select>\n </div>\n </div>\n </div>\n )}\n\n {/* Creation error */}\n {createError && (\n <div className=\"p-3 bg-status-error-bg/30 border border-status-error-text/20 rounded-lg\">\n <div className=\"flex items-center gap-2\">\n <XCircle className=\"size-5 text-status-error-text flex-shrink-0\" />\n <p className=\"text-sm text-status-error-text\">{createError}</p>\n </div>\n <div className=\"flex items-center gap-2 mt-3\">\n <button\n type=\"button\"\n onClick={() => {\n setCreateError(null);\n // Reset form to allow entering different credentials\n setStage('details');\n setTunnelUrl('');\n setAgentApiKey('');\n setHealthResult(null);\n setIdentityResult(null);\n }}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-text-primary bg-bg-sunken border border-border-default rounded-md hover:bg-bg-elevated transition-colors\"\n >\n <RefreshCw className=\"size-3.5\" />\n {tr('agentsPage.tryDifferentAgent', 'Try Different Agent')}\n </button>\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary transition-colors\"\n >\n {tr('agentsPage.close', 'Close')}\n </button>\n </div>\n </div>\n )}\n\n {/* Actions */}\n <div className=\"flex items-center justify-between pt-4\">\n <button\n type=\"button\"\n onClick={() => setStage('details')}\n className=\"flex items-center gap-1 px-4 py-2 text-sm font-medium text-text-primary bg-bg-sunken border border-border-default rounded-md hover:bg-bg-elevated transition-colors\"\n >\n <ChevronLeft className=\"size-4\" />\n {tr('agentsPage.back', 'Back')}\n </button>\n <button\n type=\"button\"\n disabled={!isValidationPassed || isLoading || validating}\n onClick={handleCreateAgent}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n >\n {isLoading ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n {tr('agentsPage.adding', 'Adding...')}\n </>\n ) : (\n <>\n {tr('agentsPage.addAgent', 'Add Agent')}\n <ChevronRight className=\"size-4\" />\n </>\n )}\n </button>\n </div>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AgentForm.js","names":[],"sources":["../../../src/components/agents/AgentForm.tsx"],"sourcesContent":["import { useEffect, useRef, useState, useCallback } from 'react';\nimport type { ReactNode } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n X,\n Eye,\n EyeOff,\n CheckCircle2,\n XCircle,\n Loader2,\n ChevronRight,\n ChevronLeft,\n ChevronDown,\n Copy,\n Check,\n Terminal,\n Download,\n RefreshCw,\n Plus,\n Settings2,\n} from 'lucide-react';\nimport { useProbeAgentTunnelLazyQuery } from '@/generated/wspace-operations';\nimport { SearchableSelect } from '@/components/shared';\nimport type { SearchableSelectOption } from '@/components/shared';\nimport { isQuotaExhaustedError, wasQuotaHandledLocally } from '@/utils/quotaUtils';\n\ntype WizardStage = 'details' | 'validate';\n\ninterface TargetOption {\n id: string;\n name: string;\n type: string;\n status: string;\n}\n\ninterface AgentFormProps {\n onSubmit: (data: Record<string, unknown>) => Promise<{ id: string } | void>;\n onCancel: () => void;\n isLoading?: boolean;\n /** Navigate to a path (used to go to agent details after creation) */\n onNavigate?: (path: string) => void;\n /** Available targets for the agent to be installed on */\n targets?: TargetOption[];\n /** Whether targets are still loading */\n targetsLoading?: boolean;\n /**\n * Create a new target inline. When provided, the form shows a\n * \"+ Create new target\" affordance that lets users create a DIRECT\n * target on the fly without leaving the dialog. Must return the new\n * target so the form can auto-select it.\n */\n onCreateTarget?: (name: string) => Promise<TargetOption | null>;\n /** Optional extra content (e.g. a draft EntityTagPicker) rendered above the configure-stage actions. */\n extraSection?: ReactNode;\n}\n\nexport function AgentForm({\n onSubmit,\n onCancel,\n isLoading,\n onNavigate,\n targets,\n targetsLoading,\n onCreateTarget,\n extraSection,\n}: AgentFormProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n const [stage, setStage] = useState<WizardStage>('details');\n const [showApiKey, setShowApiKey] = useState(false);\n const [showInstallInstructions, setShowInstallInstructions] = useState(false);\n const [copiedCommand, setCopiedCommand] = useState<string | null>(null);\n\n // Stage 1: Basic details\n const [name, setName] = useState('');\n // Tracks whether the operator has typed into the name field. Once true,\n // we stop overwriting the value from auto-fill sources (target select +\n // identity probe) so we never clobber their input.\n const [nameEdited, setNameEdited] = useState(false);\n const [tunnelUrl, setTunnelUrl] = useState('');\n const [agentApiKey, setAgentApiKey] = useState('');\n const [selectedTargetId, setSelectedTargetId] = useState('');\n\n // When the operator picks a target, autofill the agent name with the\n // target's display name. Skip if they've already typed something into\n // the name field. The svc append a `-2`/`-3` suffix on collision so\n // it's safe to suggest a duplicate; the actual stored name comes back\n // on the create response.\n useEffect(() => {\n if (nameEdited) return;\n if (!selectedTargetId) return;\n const picked = (targets ?? []).find((t) => t.id === selectedTargetId);\n if (picked?.name) setName(picked.name);\n }, [selectedTargetId, targets, nameEdited]);\n\n // Inline target creation state\n const [showInlineTarget, setShowInlineTarget] = useState(false);\n const [inlineTargetName, setInlineTargetName] = useState('');\n const [creatingTarget, setCreatingTarget] = useState(false);\n const [inlineTargetError, setInlineTargetError] = useState<string | null>(null);\n\n // Stage 2: Validation (via backend-proxied GraphQL query)\n const [validating, setValidating] = useState(false);\n const [healthResult, setHealthResult] = useState<{\n ok: boolean;\n data?: { status: string; version: string; uptime: number };\n error?: string;\n } | null>(null);\n const [identityResult, setIdentityResult] = useState<{\n ok: boolean;\n data?: { hostname: string; platform: string; arch: string };\n error?: string;\n } | null>(null);\n\n // Auto-detected fields (editable overrides)\n const [detectedHostname, setDetectedHostname] = useState('');\n const [detectedPlatform, setDetectedPlatform] = useState('linux');\n const [detectedArch, setDetectedArch] = useState('x86_64');\n const [detectedVersion, setDetectedVersion] = useState('1.0.0');\n\n // Error state for creation failures\n const [createError, setCreateError] = useState<string | null>(null);\n\n // Advanced options (Phase 5B: per-daemon profile name).\n // Defaults to \"default\" (autofilled but editable). Disclosure is closed\n // by default — users only need to touch this for multi-profile setups.\n const [showAdvanced, setShowAdvanced] = useState(false);\n const [profile, setProfile] = useState('default');\n\n const copyToClipboard = useCallback((text: string, id: string) => {\n navigator.clipboard.writeText(text);\n setCopiedCommand(id);\n setTimeout(() => setCopiedCommand(null), 2000);\n }, []);\n\n const normalizeUrl = (url: string) => {\n let u = url.trim();\n if (u.endsWith('/')) u = u.slice(0, -1);\n return u;\n };\n\n const [runProbe] = useProbeAgentTunnelLazyQuery({ fetchPolicy: 'network-only' });\n\n // Validate the agent through the svc-proxied `probeAgentTunnel` query.\n // svc fans out to /health + /api/profiles/default/agent/identity over\n // its own egress and returns a combined AgentProbeResult — the\n // operator's API key never touches the browser→tunnel path.\n const validateAgent = async () => {\n const baseUrl = normalizeUrl(tunnelUrl);\n if (!baseUrl) return;\n\n setValidating(true);\n setHealthResult(null);\n setIdentityResult(null);\n\n try {\n const { data, error } = await runProbe({\n variables: { url: baseUrl, agentApiKey: agentApiKey || null },\n });\n const probe = data?.probeAgentTunnel;\n if (error || !probe) {\n const msg = error?.message ?? 'Probe failed';\n setHealthResult({ ok: false, error: msg });\n setIdentityResult({ ok: false, error: msg });\n } else {\n setHealthResult(\n probe.healthOk\n ? {\n ok: true,\n data: {\n status: probe.healthStatus ?? 'healthy',\n version: probe.agentVersion ?? 'unknown',\n uptime: probe.uptime ?? 0,\n },\n }\n : { ok: false, error: probe.healthError ?? 'Health check failed' }\n );\n if (probe.agentVersion) setDetectedVersion(probe.agentVersion);\n\n setIdentityResult(\n probe.identityOk\n ? {\n ok: true,\n data: {\n hostname: probe.hostname ?? '',\n platform: probe.platform ?? 'linux',\n arch: probe.architecture ?? 'x86_64',\n },\n }\n : { ok: false, error: probe.identityError ?? 'Identity check failed' }\n );\n if (probe.identityOk) {\n if (probe.platform) setDetectedPlatform(probe.platform);\n if (probe.architecture) setDetectedArch(probe.architecture);\n // Auto-fill name from probe hostname unless (a) the operator\n // has manually typed one, or (b) a target is already selected\n // (target wins as the canonical autogen value).\n if (probe.hostname) {\n setDetectedHostname(probe.hostname);\n if (!nameEdited && !selectedTargetId) setName(probe.hostname);\n }\n }\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Probe failed';\n setHealthResult({ ok: false, error: msg });\n setIdentityResult({ ok: false, error: msg });\n }\n\n setValidating(false);\n };\n\n // Refs let us short-circuit double-submits before React re-renders\n // the button as disabled. The parent passes `isLoading` (driven by\n // `useState`) and we wire it to the button's `disabled` prop, but\n // `setState` is asynchronous — between the user's first click and\n // React's next commit there is a microtask window in which the button\n // is still clickable. Without this ref a fast double-click fires\n // `addAgentFromTunnel` twice in parallel; the svc sees one in-flight\n // and one fresh call, the second one wins the (workspaceId, name)\n // unique-resolution race, and the workspace ends up with two agent\n // rows (the original autogen + the operator's custom name).\n const submittingRef = useRef(false);\n\n // Create the agent and close the modal\n const handleCreateAgent = async () => {\n if (submittingRef.current) return;\n submittingRef.current = true;\n setCreateError(null);\n\n // Pipe the chosen profile through `metadata.profile`. Phase 5A (svc) is\n // adding first-class AgentProfile rows; in the meantime the backend\n // also reads `metadata.profile` so the create path keeps working\n // regardless of which side of the cut-over the cluster is on.\n const trimmedProfile = profile.trim() || 'default';\n const data: Record<string, unknown> = {\n name: name || detectedHostname || 'Unnamed Agent',\n hostname: detectedHostname || 'unknown',\n platform: detectedPlatform,\n architecture: detectedArch,\n version: detectedVersion,\n tunnelUrl: normalizeUrl(tunnelUrl),\n tunnelStatus: 'ACTIVE',\n agentApiKey,\n metadata: { profile: trimmedProfile },\n ...(selectedTargetId ? { targetId: selectedTargetId } : {}),\n };\n\n try {\n const result = await onSubmit(data);\n const newAgentId = result?.id || null;\n\n if (!newAgentId) {\n // Mutation succeeded but returned no agent ID — treat as failure\n setCreateError('Agent creation returned no ID. The agent may already exist.');\n return;\n }\n\n // Close the modal — the agent list will refetch automatically\n if (onNavigate) {\n onNavigate(`/agents/${newAgentId}`);\n } else {\n onCancel();\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to create agent';\n setCreateError(message);\n } finally {\n submittingRef.current = false;\n }\n };\n\n const isValidationPassed = healthResult?.ok && identityResult?.ok;\n const hasTargets = targets && targets.length > 0;\n // The user must either pick or inline-create a target if inline creation\n // is enabled; otherwise we only require it when targets already exist.\n const targetRequiredButMissing = onCreateTarget\n ? selectedTargetId.length === 0\n : hasTargets && selectedTargetId.length === 0;\n const canProceedToValidate = tunnelUrl.trim().length > 0 && !targetRequiredButMissing;\n\n const handleInlineCreateTarget = useCallback(async () => {\n if (!onCreateTarget || !inlineTargetName.trim()) return;\n setCreatingTarget(true);\n setInlineTargetError(null);\n try {\n const created = await onCreateTarget(inlineTargetName.trim());\n if (created) {\n setSelectedTargetId(created.id);\n setShowInlineTarget(false);\n setInlineTargetName('');\n } else if (wasQuotaHandledLocally()) {\n // Parent surfaced the quota modal — collapse the inline row\n // quietly so the user isn't left with a vague \"try again\"\n // message under an explicit upgrade-your-plan dialog.\n setShowInlineTarget(false);\n setInlineTargetName('');\n } else {\n setInlineTargetError(\n tr('agentsPage.inlineTargetFailed', 'Failed to create target. Please try again.')\n );\n }\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n // Quota dialog will (or already has) opened at the parent;\n // suppress the generic inline error so the two don't compete.\n setShowInlineTarget(false);\n setInlineTargetName('');\n } else {\n setInlineTargetError(err instanceof Error ? err.message : 'Failed to create target');\n }\n } finally {\n setCreatingTarget(false);\n }\n }, [onCreateTarget, inlineTargetName, tr]);\n\n const renderCopyButton = (text: string, id: string) => (\n <button\n type=\"button\"\n onClick={() => copyToClipboard(text, id)}\n className=\"p-1 rounded hover:bg-bg-elevated transition-colors\"\n title={tr('agentsPage.copy', 'Copy')}\n >\n {copiedCommand === id ? (\n <Check className=\"size-3.5 text-status-success-text\" />\n ) : (\n <Copy className=\"size-3.5 text-text-secondary\" />\n )}\n </button>\n );\n\n // Stepper\n const steps = [\n { key: 'details', label: 'Setup' },\n { key: 'validate', label: 'Validate' },\n ];\n\n const currentStepIndex = steps.findIndex((s) => s.key === stage);\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center bg-bg-overlay\">\n <div className=\"w-full max-w-xl bg-bg-surface rounded-lg shadow-xl max-h-[90vh] overflow-y-auto\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-border-default sticky top-0 bg-bg-surface z-10\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('agentsPage.addNewAgent', 'Add New Agent')}\n </h2>\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"p-1 rounded hover:bg-bg-sunken transition-colors\"\n >\n <X className=\"size-5 text-text-secondary\" />\n </button>\n </div>\n\n {/* Stepper */}\n <div className=\"flex items-center gap-2 px-6 pt-4 pb-2\">\n {steps.map((step, i) => (\n <div key={step.key} className=\"flex items-center gap-2\">\n <div\n className={`flex items-center justify-center size-7 rounded-full text-xs font-semibold transition-colors ${\n i < currentStepIndex\n ? 'bg-status-success-bg text-status-success-text'\n : i === currentStepIndex\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary'\n }`}\n >\n {i < currentStepIndex ? <CheckCircle2 className=\"size-4\" /> : i + 1}\n </div>\n <span\n className={`text-sm ${i === currentStepIndex ? 'font-medium text-text-primary' : 'text-text-secondary'}`}\n >\n {step.label}\n </span>\n {i < steps.length - 1 && <ChevronRight className=\"size-4 text-text-muted\" />}\n </div>\n ))}\n </div>\n\n <div className=\"p-6\">\n {/* Stage 1: Setup */}\n {stage === 'details' && (\n <div className=\"space-y-4\">\n {/* Install instructions (collapsible) */}\n <div className=\"border border-border-default rounded-lg overflow-hidden\">\n <button\n type=\"button\"\n onClick={() => setShowInstallInstructions(!showInstallInstructions)}\n className=\"w-full flex items-center justify-between px-4 py-3 bg-bg-sunken hover:bg-bg-sunken/80 transition-colors\"\n >\n <div className=\"flex items-center gap-2\">\n <Download className=\"size-4 text-action-primary-bg\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('agentsPage.installInstructions', 'Agent Install Instructions')}\n </span>\n </div>\n <ChevronRight\n className={`size-4 text-text-secondary transition-transform ${showInstallInstructions ? 'rotate-90' : ''}`}\n />\n </button>\n {showInstallInstructions && (\n <div className=\"px-4 py-3 space-y-3 border-t border-border-default\">\n <div className=\"space-y-2\">\n {[\n {\n label: tr('agentsPage.installStep1', '1. Install the agent CLI'),\n // `&& hash -r` refreshes the shell so the new `vibe`\n // resolves immediately (no manual hash -r needed).\n cmd: 'curl -fsSL https://app.vibecontrols.com/install | bash && hash -r 2>/dev/null',\n },\n {\n label: tr(\n 'agentsPage.installStep2',\n '2. Start the agent (prints tunnel URL + API key)'\n ),\n cmd: 'vibe start',\n },\n ].map((step) => (\n <div key={step.cmd}>\n <p className=\"text-xs text-text-secondary mb-1\">{step.label}</p>\n <div className=\"flex items-center gap-2 bg-bg-sunken rounded-md px-3 py-1.5 font-mono text-xs text-text-primary\">\n <Terminal className=\"size-3 text-text-muted flex-shrink-0\" />\n <code className=\"flex-1\">{step.cmd}</code>\n {renderCopyButton(step.cmd, step.cmd)}\n </div>\n </div>\n ))}\n <p className=\"text-xs text-text-secondary pt-1\">\n {tr(\n 'agentsPage.installHint',\n 'After running vibe start, copy the printed tunnel URL and API key into the fields above.'\n )}\n </p>\n </div>\n </div>\n )}\n </div>\n\n {/* Agent Name — visible upfront so operators see (and can\n override) the autogen before validation. The autofill\n chain still runs in the background: picking a target\n pulls in its name; clicking Validate falls back to the\n probed hostname when the user hasn't typed anything.\n The svc dedupes inside the workspace via\n resolveUniqueAgentName(), so suffixes like `-2` may\n still get appended at submit time even when the user\n picked their own string. */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('agentsPage.name', 'Name')}\n </label>\n <input\n aria-label={tr('agentsPage.name', 'Name')}\n type=\"text\"\n value={name}\n onChange={(e) => {\n setName(e.target.value);\n setNameEdited(true);\n }}\n placeholder={tr(\n 'agentsPage.namePlaceholderUpfront',\n 'My laptop, prod jump-host, …'\n )}\n className=\"w-full px-3 py-2 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n data-testid=\"agent-form-name-input\"\n />\n <p className=\"mt-1 text-xs text-text-muted\">\n {tr(\n 'agentsPage.nameHint',\n \"How this agent shows up in the UI. Defaults to the target's name (or the machine's hostname after Validate) — leave blank to accept the autogen.\"\n )}\n </p>\n </div>\n\n {/* Target Selection — with inline creation */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('agentsPage.target', 'Target')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n\n {hasTargets && !showInlineTarget && (\n <>\n <SearchableSelect\n placeholder={tr('agentsPage.selectTarget', 'Select a target...')}\n searchPlaceholder={tr('agentsPage.searchTargets', 'Search targets...')}\n options={(targets ?? []).map(\n (tgt): SearchableSelectOption => ({\n value: tgt.id,\n label: tgt.name,\n description: `${tgt.type}${tgt.status ? ` · ${tgt.status}` : ''}`,\n })\n )}\n value={selectedTargetId}\n onChange={setSelectedTargetId}\n clearable\n />\n <p className=\"mt-1 text-xs text-text-muted\">\n {tr(\n 'agentsPage.targetDescription',\n 'The target machine this agent will be installed on.'\n )}{' '}\n {onCreateTarget && (\n <button\n type=\"button\"\n onClick={() => {\n setShowInlineTarget(true);\n setInlineTargetName(name.trim() || '');\n }}\n className=\"text-action-primary-bg hover:underline inline-flex items-center gap-1\"\n >\n <Plus className=\"size-3\" />\n {tr('agentsPage.createNewTarget', 'Create a new target')}\n </button>\n )}\n </p>\n </>\n )}\n\n {/* Empty state prompt (no targets exist yet) */}\n {!hasTargets && !targetsLoading && onCreateTarget && !showInlineTarget && (\n <div className=\"p-3 bg-bg-sunken border border-border-default rounded-lg\">\n <p className=\"text-sm text-text-secondary mb-2\">\n {tr(\n 'agentsPage.noTargetsInlinePrompt',\n \"You don't have any targets yet. Create one now to install this agent on it.\"\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => {\n setShowInlineTarget(true);\n setInlineTargetName(name.trim() || '');\n }}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:opacity-90\"\n >\n <Plus className=\"size-4\" />\n {tr('agentsPage.createFirstTarget', 'Create first target')}\n </button>\n </div>\n )}\n\n {/* Inline creation form */}\n {showInlineTarget && onCreateTarget && (\n <div className=\"p-3 bg-bg-sunken border border-border-default rounded-lg space-y-2\">\n <p className=\"text-xs font-medium text-text-secondary\">\n {tr(\n 'agentsPage.newDirectTarget',\n 'New target (Direct — you can change the type later)'\n )}\n </p>\n <input\n aria-label=\"control\"\n type=\"text\"\n value={inlineTargetName}\n onChange={(e) => setInlineTargetName(e.target.value)}\n placeholder={tr('agentsPage.targetName', 'Target name')}\n className=\"w-full px-3 py-2 text-sm bg-bg-elevated border border-border-default rounded-md text-text-primary placeholder-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n void handleInlineCreateTarget();\n }\n }}\n />\n {inlineTargetError && (\n <p className=\"text-xs text-status-error-text\">{inlineTargetError}</p>\n )}\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => void handleInlineCreateTarget()}\n disabled={!inlineTargetName.trim() || creatingTarget}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:opacity-90 disabled:opacity-50\"\n >\n {creatingTarget ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <Check className=\"size-3.5\" />\n )}\n {tr('agentsPage.create', 'Create')}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setShowInlineTarget(false);\n setInlineTargetName('');\n setInlineTargetError(null);\n }}\n className=\"px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary\"\n >\n {tr('agentsPage.cancel', 'Cancel')}\n </button>\n </div>\n </div>\n )}\n\n {/* Loading placeholder */}\n {targetsLoading && !hasTargets && (\n <div className=\"p-3 bg-bg-sunken border border-border-default rounded-lg flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n {tr('agentsPage.loadingTargets', 'Loading targets...')}\n </div>\n )}\n </div>\n\n {/* API/Tunnel URL */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('agentsPage.apiTunnelUrl', 'API / Tunnel URL')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n aria-label=\"https://your-tunnel.trycloudflare.com\"\n type=\"url\"\n value={tunnelUrl}\n onChange={(e) => setTunnelUrl(e.target.value)}\n placeholder=\"https://your-tunnel.trycloudflare.com\"\n required\n className=\"w-full px-3 py-2 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n />\n <p className=\"mt-1 text-xs text-text-muted\">\n {tr(\n 'agentsPage.tunnelUrlHint',\n \"The agent's tunnel URL or local API URL (e.g. http://localhost:3005)\"\n )}\n </p>\n </div>\n\n {/* Agent API Key */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('agentsPage.agentApiKey', 'Agent API Key')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <div className=\"relative\">\n <input\n aria-label=\"vcak_...\"\n type={showApiKey ? 'text' : 'password'}\n value={agentApiKey}\n onChange={(e) => setAgentApiKey(e.target.value)}\n placeholder=\"vcak_...\"\n required\n className=\"w-full px-3 py-2 pr-10 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n />\n <button\n type=\"button\"\n onClick={() => setShowApiKey(!showApiKey)}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded hover:bg-bg-elevated transition-colors\"\n >\n {showApiKey ? (\n <EyeOff className=\"size-4 text-text-secondary\" />\n ) : (\n <Eye className=\"size-4 text-text-secondary\" />\n )}\n </button>\n </div>\n <p className=\"mt-1 text-xs text-text-muted\">\n {tr('agentsPage.apiKeyHint', 'Run')}{' '}\n <code className=\"bg-bg-sunken px-1 rounded\">vibe key</code>{' '}\n {tr(\n 'agentsPage.apiKeyHintSuffix',\n 'on the agent to get this. Changes on restart.'\n )}\n </p>\n </div>\n\n {/* Advanced options — per-daemon profile name (Phase 5B) */}\n <div className=\"border border-border-default rounded-lg overflow-hidden\">\n <button\n type=\"button\"\n onClick={() => setShowAdvanced((v) => !v)}\n className=\"w-full flex items-center justify-between px-4 py-2.5 bg-bg-sunken hover:bg-bg-sunken/80 transition-colors\"\n aria-expanded={showAdvanced}\n data-testid=\"agent-form-advanced-toggle\"\n >\n <div className=\"flex items-center gap-2\">\n <Settings2 className=\"size-4 text-text-secondary\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('agentsPage.advancedOptions', 'Advanced options')}\n </span>\n </div>\n <ChevronDown\n className={`size-4 text-text-secondary transition-transform ${showAdvanced ? 'rotate-180' : ''}`}\n />\n </button>\n {showAdvanced && (\n <div className=\"px-4 py-3 space-y-3 border-t border-border-default\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('agentsPage.profileName', 'Profile name')}\n </label>\n <input\n aria-label=\"default\"\n type=\"text\"\n value={profile}\n onChange={(e) => setProfile(e.target.value)}\n placeholder=\"default\"\n className=\"w-full px-3 py-2 text-sm bg-bg-surface border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-action-primary-bg font-mono\"\n data-testid=\"agent-form-profile-input\"\n />\n <p className=\"mt-1 text-xs text-text-muted\">\n {tr(\n 'agentsPage.profileNameHint',\n 'Each profile runs an isolated CLI on the daemon. Leave as \"default\" unless you want this agent to host multiple parallel profiles.'\n )}\n </p>\n </div>\n </div>\n )}\n </div>\n\n {extraSection && (\n <div className=\"border-t border-border-default pt-4 mt-4\">{extraSection}</div>\n )}\n\n {/* Actions */}\n <div className=\"flex items-center justify-between pt-4\">\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"px-4 py-2 text-sm font-medium text-text-primary bg-bg-sunken border border-border-default rounded-md hover:bg-bg-elevated transition-colors\"\n >\n {tr('agentsPage.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n disabled={!canProceedToValidate}\n onClick={() => {\n setStage('validate');\n validateAgent();\n }}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n >\n {tr('agentsPage.nextValidate', 'Next: Validate')}\n <ChevronRight className=\"size-4\" />\n </button>\n </div>\n </div>\n )}\n\n {/* Stage 2: Validate */}\n {stage === 'validate' && (\n <div className=\"space-y-4\">\n {/* Validation Results */}\n <div className=\"space-y-3\">\n {/* Health Check */}\n <div\n className={`flex items-center gap-3 p-3 rounded-lg border ${\n healthResult === null\n ? 'border-border-default bg-bg-sunken'\n : healthResult.ok\n ? 'border-status-success-text/20 bg-status-success-bg/30'\n : 'border-status-error-text/20 bg-status-error-bg/30'\n }`}\n >\n {validating && !healthResult ? (\n <Loader2 className=\"size-5 text-text-secondary animate-spin\" />\n ) : healthResult?.ok ? (\n <CheckCircle2 className=\"size-5 text-status-success-text\" />\n ) : healthResult ? (\n <XCircle className=\"size-5 text-status-error-text\" />\n ) : (\n <div className=\"size-5 rounded-full border-2 border-border-default\" />\n )}\n <div className=\"flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('agentsPage.healthCheck', 'Health Check')}\n </p>\n {healthResult?.ok && healthResult.data && (\n <p className=\"text-xs text-text-secondary\">\n Status: {healthResult.data.status} | Version: {healthResult.data.version} |\n Uptime: {Math.round(healthResult.data.uptime)}s\n </p>\n )}\n {healthResult && !healthResult.ok && (\n <p className=\"text-xs text-status-error-text\">{healthResult.error}</p>\n )}\n </div>\n </div>\n\n {/* API Access Check */}\n <div\n className={`flex items-center gap-3 p-3 rounded-lg border ${\n identityResult === null\n ? 'border-border-default bg-bg-sunken'\n : identityResult.ok\n ? 'border-status-success-text/20 bg-status-success-bg/30'\n : 'border-status-error-text/20 bg-status-error-bg/30'\n }`}\n >\n {validating && !identityResult ? (\n <Loader2 className=\"size-5 text-text-secondary animate-spin\" />\n ) : identityResult?.ok ? (\n <CheckCircle2 className=\"size-5 text-status-success-text\" />\n ) : identityResult ? (\n <XCircle className=\"size-5 text-status-error-text\" />\n ) : (\n <div className=\"size-5 rounded-full border-2 border-border-default\" />\n )}\n <div className=\"flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('agentsPage.apiAccess', 'API Access')}\n </p>\n {identityResult?.ok && identityResult.data && (\n <p className=\"text-xs text-text-secondary\">\n Host: {identityResult.data.hostname} | {identityResult.data.platform} /{' '}\n {identityResult.data.arch}\n </p>\n )}\n {identityResult && !identityResult.ok && (\n <p className=\"text-xs text-status-error-text\">{identityResult.error}</p>\n )}\n </div>\n </div>\n </div>\n\n {/* Retry button */}\n {!validating && (!healthResult?.ok || !identityResult?.ok) && (\n <button\n type=\"button\"\n onClick={validateAgent}\n className=\"flex items-center gap-2 px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary hover:bg-bg-sunken rounded-md transition-colors\"\n >\n <RefreshCw className=\"size-3.5\" />\n {tr('agentsPage.retryValidation', 'Retry validation')}\n </button>\n )}\n\n {/* Detected info (editable overrides) — Name lives upfront\n now; this section shows the validate-time fields only. */}\n {isValidationPassed && (\n <div className=\"space-y-3 pt-2\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('agentsPage.detectedAgentInfo', 'Detected Agent Info')}\n </p>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label className=\"block text-xs font-medium text-text-secondary mb-1\">\n {tr('agentsPage.platform', 'Platform')}\n </label>\n <select\n value={detectedPlatform}\n onChange={(e) => setDetectedPlatform(e.target.value)}\n className=\"w-full px-3 py-1.5 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"linux\">Linux</option>\n <option value=\"darwin\">macOS</option>\n <option value=\"windows\">Windows</option>\n </select>\n </div>\n <div>\n <label className=\"block text-xs font-medium text-text-secondary mb-1\">\n {tr('agentsPage.architecture', 'Architecture')}\n </label>\n <select\n value={detectedArch}\n onChange={(e) => setDetectedArch(e.target.value)}\n className=\"w-full px-3 py-1.5 text-sm bg-bg-sunken border border-border-default rounded-md text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg\"\n >\n <option value=\"x86_64\">x86_64</option>\n <option value=\"arm64\">arm64</option>\n <option value=\"aarch64\">aarch64</option>\n </select>\n </div>\n </div>\n </div>\n )}\n\n {/* Creation error */}\n {createError && (\n <div className=\"p-3 bg-status-error-bg/30 border border-status-error-text/20 rounded-lg\">\n <div className=\"flex items-center gap-2\">\n <XCircle className=\"size-5 text-status-error-text flex-shrink-0\" />\n <p className=\"text-sm text-status-error-text\">{createError}</p>\n </div>\n <div className=\"flex items-center gap-2 mt-3\">\n <button\n type=\"button\"\n onClick={() => {\n setCreateError(null);\n // Reset form to allow entering different credentials\n setStage('details');\n setTunnelUrl('');\n setAgentApiKey('');\n setHealthResult(null);\n setIdentityResult(null);\n }}\n className=\"flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-text-primary bg-bg-sunken border border-border-default rounded-md hover:bg-bg-elevated transition-colors\"\n >\n <RefreshCw className=\"size-3.5\" />\n {tr('agentsPage.tryDifferentAgent', 'Try Different Agent')}\n </button>\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary transition-colors\"\n >\n {tr('agentsPage.close', 'Close')}\n </button>\n </div>\n </div>\n )}\n\n {/* Actions */}\n <div className=\"flex items-center justify-between pt-4\">\n <button\n type=\"button\"\n onClick={() => setStage('details')}\n className=\"flex items-center gap-1 px-4 py-2 text-sm font-medium text-text-primary bg-bg-sunken border border-border-default rounded-md hover:bg-bg-elevated transition-colors\"\n >\n <ChevronLeft className=\"size-4\" />\n {tr('agentsPage.back', 'Back')}\n </button>\n <button\n type=\"button\"\n disabled={!isValidationPassed || isLoading || validating}\n onClick={handleCreateAgent}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n >\n {isLoading ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n {tr('agentsPage.adding', 'Adding...')}\n </>\n ) : (\n <>\n {tr('agentsPage.addAgent', 'Add Agent')}\n <ChevronRight className=\"size-4\" />\n </>\n )}\n </button>\n </div>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
+
import { useVibeNavigate as t } from "../../providers/VibeControlsProvider.js";
|
|
3
|
+
import { getAgentStatus as n } from "../agents/agentStatus.js";
|
|
4
|
+
import { Clock as r, Cpu as i, ExternalLink as a, MonitorSmartphone as o, Network as s, Server as c, Wifi as l } from "lucide-react";
|
|
5
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
6
|
+
import { Popover as f, PopoverContent as p, PopoverTrigger as m } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
//#region src/components/sessions/EntityInfoPopover.tsx
|
|
8
|
+
function h(e) {
|
|
9
|
+
if (!e) return "—";
|
|
10
|
+
let t = Date.now() - new Date(e).getTime();
|
|
11
|
+
return Number.isFinite(t) ? t < 6e4 ? "just now" : t < 36e5 ? `${Math.floor(t / 6e4)}m ago` : t < 864e5 ? `${Math.floor(t / 36e5)}h ago` : new Date(e).toLocaleDateString() : "—";
|
|
12
|
+
}
|
|
13
|
+
var g = {
|
|
14
|
+
ACTIVE: "bg-status-success-text",
|
|
15
|
+
DEGRADED: "bg-status-warning-text",
|
|
16
|
+
OFFLINE: "bg-status-error-text"
|
|
17
|
+
}, _ = {
|
|
18
|
+
DIRECT: "bg-bg-sunken text-text-secondary",
|
|
19
|
+
SSH: "bg-status-success-bg text-status-success-text",
|
|
20
|
+
RDP: "bg-status-warning-bg text-status-warning-text",
|
|
21
|
+
VNC: "bg-action-primary-bg/15 text-action-primary-text"
|
|
22
|
+
};
|
|
23
|
+
function v({ icon: e, label: t, value: n }) {
|
|
24
|
+
return /* @__PURE__ */ d("div", {
|
|
25
|
+
className: "flex items-center justify-between gap-3 py-1 text-xs",
|
|
26
|
+
children: [/* @__PURE__ */ d("span", {
|
|
27
|
+
className: "flex items-center gap-1.5 text-text-tertiary",
|
|
28
|
+
children: [e, t]
|
|
29
|
+
}), /* @__PURE__ */ u("span", {
|
|
30
|
+
className: "font-medium text-text-primary truncate max-w-[160px]",
|
|
31
|
+
title: n,
|
|
32
|
+
children: n
|
|
33
|
+
})]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function y({ agent: o, children: s, className: _ = "" }) {
|
|
37
|
+
let y = e(), b = t(), x = n(o);
|
|
38
|
+
return /* @__PURE__ */ d(f, { children: [/* @__PURE__ */ u(m, {
|
|
39
|
+
asChild: !0,
|
|
40
|
+
children: s ?? /* @__PURE__ */ d("button", {
|
|
41
|
+
type: "button",
|
|
42
|
+
onClick: (e) => e.stopPropagation(),
|
|
43
|
+
"data-testid": "session-agent-chip",
|
|
44
|
+
className: `inline-flex items-center gap-1.5 px-1.5 py-0.5 rounded-md text-xs text-text-secondary hover:bg-bg-sunken hover:text-text-primary transition-colors max-w-[140px] ${_}`,
|
|
45
|
+
title: o.name,
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ u("span", {
|
|
48
|
+
className: `size-1.5 rounded-full shrink-0 ${g[x] ?? "bg-text-muted"}`,
|
|
49
|
+
"aria-hidden": !0
|
|
50
|
+
}),
|
|
51
|
+
/* @__PURE__ */ u(c, { className: "size-3 shrink-0" }),
|
|
52
|
+
/* @__PURE__ */ u("span", {
|
|
53
|
+
className: "truncate",
|
|
54
|
+
children: o.name
|
|
55
|
+
})
|
|
56
|
+
]
|
|
57
|
+
})
|
|
58
|
+
}), /* @__PURE__ */ d(p, {
|
|
59
|
+
align: "start",
|
|
60
|
+
className: "w-64 p-3",
|
|
61
|
+
onClick: (e) => e.stopPropagation(),
|
|
62
|
+
"data-testid": "session-agent-popover",
|
|
63
|
+
children: [
|
|
64
|
+
/* @__PURE__ */ d("div", {
|
|
65
|
+
className: "flex items-center gap-2 mb-2",
|
|
66
|
+
children: [
|
|
67
|
+
/* @__PURE__ */ u("span", {
|
|
68
|
+
className: `size-2 rounded-full shrink-0 ${g[x] ?? "bg-text-muted"}`,
|
|
69
|
+
"aria-hidden": !0
|
|
70
|
+
}),
|
|
71
|
+
/* @__PURE__ */ u("span", {
|
|
72
|
+
className: "text-sm font-semibold text-text-primary truncate",
|
|
73
|
+
children: o.name
|
|
74
|
+
}),
|
|
75
|
+
/* @__PURE__ */ u("span", {
|
|
76
|
+
className: "ml-auto text-[10px] uppercase tracking-wide text-text-tertiary",
|
|
77
|
+
children: x
|
|
78
|
+
})
|
|
79
|
+
]
|
|
80
|
+
}),
|
|
81
|
+
/* @__PURE__ */ d("div", {
|
|
82
|
+
className: "border-t border-border-subtle pt-1",
|
|
83
|
+
children: [
|
|
84
|
+
/* @__PURE__ */ u(v, {
|
|
85
|
+
icon: /* @__PURE__ */ u(c, { className: "size-3" }),
|
|
86
|
+
label: y("sessions.info.host", "Host"),
|
|
87
|
+
value: o.hostname || "—"
|
|
88
|
+
}),
|
|
89
|
+
/* @__PURE__ */ u(v, {
|
|
90
|
+
icon: /* @__PURE__ */ u(i, { className: "size-3" }),
|
|
91
|
+
label: y("sessions.info.platform", "Platform"),
|
|
92
|
+
value: o.platform || "—"
|
|
93
|
+
}),
|
|
94
|
+
/* @__PURE__ */ u(v, {
|
|
95
|
+
icon: /* @__PURE__ */ u(l, { className: "size-3" }),
|
|
96
|
+
label: y("sessions.info.tunnel", "Tunnel"),
|
|
97
|
+
value: o.tunnelStatus || (o.tunnelUrl ? "set" : "—")
|
|
98
|
+
}),
|
|
99
|
+
/* @__PURE__ */ u(v, {
|
|
100
|
+
icon: /* @__PURE__ */ u(r, { className: "size-3" }),
|
|
101
|
+
label: y("sessions.info.heartbeat", "Heartbeat"),
|
|
102
|
+
value: h(o.lastHeartbeat)
|
|
103
|
+
})
|
|
104
|
+
]
|
|
105
|
+
}),
|
|
106
|
+
/* @__PURE__ */ d("button", {
|
|
107
|
+
type: "button",
|
|
108
|
+
onClick: () => b(`/agents/${o.id}`),
|
|
109
|
+
"data-testid": "session-agent-view",
|
|
110
|
+
className: "mt-2 w-full inline-flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs font-medium text-action-primary-text bg-action-primary-bg rounded-md hover:bg-action-primary-bg-hover transition-colors",
|
|
111
|
+
children: [/* @__PURE__ */ u(a, { className: "size-3" }), y("sessions.info.viewAgent", "View agent")]
|
|
112
|
+
})
|
|
113
|
+
]
|
|
114
|
+
})] });
|
|
115
|
+
}
|
|
116
|
+
function b({ target: n, children: r, className: i = "" }) {
|
|
117
|
+
let c = e(), h = t();
|
|
118
|
+
return /* @__PURE__ */ d(f, { children: [/* @__PURE__ */ u(m, {
|
|
119
|
+
asChild: !0,
|
|
120
|
+
children: r ?? /* @__PURE__ */ d("button", {
|
|
121
|
+
type: "button",
|
|
122
|
+
onClick: (e) => e.stopPropagation(),
|
|
123
|
+
"data-testid": "session-target-chip",
|
|
124
|
+
className: `inline-flex items-center gap-1.5 px-1.5 py-0.5 rounded-md text-xs text-text-secondary hover:bg-bg-sunken hover:text-text-primary transition-colors max-w-[150px] ${i}`,
|
|
125
|
+
title: `${n.name} · ${n.type}`,
|
|
126
|
+
children: [
|
|
127
|
+
/* @__PURE__ */ u(o, { className: "size-3 shrink-0" }),
|
|
128
|
+
/* @__PURE__ */ u("span", {
|
|
129
|
+
className: "truncate",
|
|
130
|
+
children: n.name
|
|
131
|
+
}),
|
|
132
|
+
/* @__PURE__ */ u("span", {
|
|
133
|
+
className: `shrink-0 px-1 py-0 rounded text-[9px] font-semibold ${_[n.type] ?? "bg-bg-sunken text-text-secondary"}`,
|
|
134
|
+
children: n.type
|
|
135
|
+
})
|
|
136
|
+
]
|
|
137
|
+
})
|
|
138
|
+
}), /* @__PURE__ */ d(p, {
|
|
139
|
+
align: "start",
|
|
140
|
+
className: "w-60 p-3",
|
|
141
|
+
onClick: (e) => e.stopPropagation(),
|
|
142
|
+
"data-testid": "session-target-popover",
|
|
143
|
+
children: [
|
|
144
|
+
/* @__PURE__ */ d("div", {
|
|
145
|
+
className: "flex items-center gap-2 mb-2",
|
|
146
|
+
children: [
|
|
147
|
+
/* @__PURE__ */ u(o, { className: "size-3.5 text-text-secondary shrink-0" }),
|
|
148
|
+
/* @__PURE__ */ u("span", {
|
|
149
|
+
className: "text-sm font-semibold text-text-primary truncate",
|
|
150
|
+
children: n.name
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ u("span", {
|
|
153
|
+
className: `ml-auto shrink-0 px-1.5 py-0.5 rounded text-[10px] font-semibold ${_[n.type] ?? "bg-bg-sunken text-text-secondary"}`,
|
|
154
|
+
children: n.type
|
|
155
|
+
})
|
|
156
|
+
]
|
|
157
|
+
}),
|
|
158
|
+
/* @__PURE__ */ d("div", {
|
|
159
|
+
className: "border-t border-border-subtle pt-1",
|
|
160
|
+
children: [/* @__PURE__ */ u(v, {
|
|
161
|
+
icon: /* @__PURE__ */ u(l, { className: "size-3" }),
|
|
162
|
+
label: c("sessions.info.status", "Status"),
|
|
163
|
+
value: n.status || "—"
|
|
164
|
+
}), /* @__PURE__ */ u(v, {
|
|
165
|
+
icon: /* @__PURE__ */ u(s, { className: "size-3" }),
|
|
166
|
+
label: c("sessions.info.address", "Address"),
|
|
167
|
+
value: n.host ? `${n.host}${n.port ? `:${n.port}` : ""}` : "—"
|
|
168
|
+
})]
|
|
169
|
+
}),
|
|
170
|
+
/* @__PURE__ */ d("button", {
|
|
171
|
+
type: "button",
|
|
172
|
+
onClick: () => h(`/targets/${n.id}`),
|
|
173
|
+
"data-testid": "session-target-view",
|
|
174
|
+
className: "mt-2 w-full inline-flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs font-medium text-action-primary-text bg-action-primary-bg rounded-md hover:bg-action-primary-bg-hover transition-colors",
|
|
175
|
+
children: [/* @__PURE__ */ u(a, { className: "size-3" }), c("sessions.info.viewTarget", "View target")]
|
|
176
|
+
})
|
|
177
|
+
]
|
|
178
|
+
})] });
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
export { y as AgentInfoPopover, b as TargetInfoPopover };
|
|
182
|
+
|
|
183
|
+
//# sourceMappingURL=EntityInfoPopover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EntityInfoPopover.js","names":[],"sources":["../../../src/components/sessions/EntityInfoPopover.tsx"],"sourcesContent":["/**\n * EntityInfoPopover — quick inline detail for the agent / target a\n * session runs on. Anchored on the agent/target chips in the session\n * list, cards and the terminal status bar so operators can confirm\n * \"which machine / is it healthy\" without leaving the page, with a\n * one-click jump to the full AgentDetails / TargetDetails page.\n */\nimport { Popover, PopoverTrigger, PopoverContent } from '@burdenoff/fe-libs/ui';\nimport { Server, MonitorSmartphone, ExternalLink, Cpu, Clock, Wifi, Network } from 'lucide-react';\nimport { getAgentStatus } from '@/components/agents/agentStatus';\nimport { useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport { useTr } from '../../shared/hooks/useTr';\nimport type { Session, SessionTarget, TargetType } from './types';\n\ntype SessionAgent = NonNullable<Session['agent']>;\n\n/** Relative \"x ago\" for a heartbeat / timestamp. */\nfunction relativeTime(ts: string | null | undefined): string {\n if (!ts) return '—';\n const diff = Date.now() - new Date(ts).getTime();\n if (!Number.isFinite(diff)) return '—';\n if (diff < 60_000) return 'just now';\n if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;\n if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;\n return new Date(ts).toLocaleDateString();\n}\n\nconst statusDotClass: Record<string, string> = {\n ACTIVE: 'bg-status-success-text',\n DEGRADED: 'bg-status-warning-text',\n OFFLINE: 'bg-status-error-text',\n};\n\nconst targetTypeBadgeClass: Record<TargetType, string> = {\n DIRECT: 'bg-bg-sunken text-text-secondary',\n SSH: 'bg-status-success-bg text-status-success-text',\n RDP: 'bg-status-warning-bg text-status-warning-text',\n VNC: 'bg-action-primary-bg/15 text-action-primary-text',\n};\n\nfunction InfoRow({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {\n return (\n <div className=\"flex items-center justify-between gap-3 py-1 text-xs\">\n <span className=\"flex items-center gap-1.5 text-text-tertiary\">\n {icon}\n {label}\n </span>\n <span className=\"font-medium text-text-primary truncate max-w-[160px]\" title={value}>\n {value}\n </span>\n </div>\n );\n}\n\nexport interface AgentInfoPopoverProps {\n agent: SessionAgent;\n /** Optional custom trigger; defaults to a chip with the agent name + status dot. */\n children?: React.ReactNode;\n className?: string;\n}\n\n/** Popover summarising an agent's health + a link to its detail page. */\nexport function AgentInfoPopover({ agent, children, className = '' }: AgentInfoPopoverProps) {\n const tr = useTr();\n const navigate = useVibeNavigate();\n const status = getAgentStatus(agent);\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n {children ?? (\n <button\n type=\"button\"\n onClick={(e) => e.stopPropagation()}\n data-testid=\"session-agent-chip\"\n className={`inline-flex items-center gap-1.5 px-1.5 py-0.5 rounded-md text-xs text-text-secondary hover:bg-bg-sunken hover:text-text-primary transition-colors max-w-[140px] ${className}`}\n title={agent.name}\n >\n <span\n className={`size-1.5 rounded-full shrink-0 ${statusDotClass[status] ?? 'bg-text-muted'}`}\n aria-hidden\n />\n <Server className=\"size-3 shrink-0\" />\n <span className=\"truncate\">{agent.name}</span>\n </button>\n )}\n </PopoverTrigger>\n <PopoverContent\n align=\"start\"\n className=\"w-64 p-3\"\n onClick={(e) => e.stopPropagation()}\n data-testid=\"session-agent-popover\"\n >\n <div className=\"flex items-center gap-2 mb-2\">\n <span\n className={`size-2 rounded-full shrink-0 ${statusDotClass[status] ?? 'bg-text-muted'}`}\n aria-hidden\n />\n <span className=\"text-sm font-semibold text-text-primary truncate\">{agent.name}</span>\n <span className=\"ml-auto text-[10px] uppercase tracking-wide text-text-tertiary\">\n {status}\n </span>\n </div>\n <div className=\"border-t border-border-subtle pt-1\">\n <InfoRow\n icon={<Server className=\"size-3\" />}\n label={tr('sessions.info.host', 'Host')}\n value={agent.hostname || '—'}\n />\n <InfoRow\n icon={<Cpu className=\"size-3\" />}\n label={tr('sessions.info.platform', 'Platform')}\n value={agent.platform || '—'}\n />\n <InfoRow\n icon={<Wifi className=\"size-3\" />}\n label={tr('sessions.info.tunnel', 'Tunnel')}\n value={agent.tunnelStatus || (agent.tunnelUrl ? 'set' : '—')}\n />\n <InfoRow\n icon={<Clock className=\"size-3\" />}\n label={tr('sessions.info.heartbeat', 'Heartbeat')}\n value={relativeTime(agent.lastHeartbeat)}\n />\n </div>\n <button\n type=\"button\"\n onClick={() => navigate(`/agents/${agent.id}`)}\n data-testid=\"session-agent-view\"\n className=\"mt-2 w-full inline-flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs font-medium text-action-primary-text bg-action-primary-bg rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n <ExternalLink className=\"size-3\" />\n {tr('sessions.info.viewAgent', 'View agent')}\n </button>\n </PopoverContent>\n </Popover>\n );\n}\n\nexport interface TargetInfoPopoverProps {\n target: SessionTarget;\n children?: React.ReactNode;\n className?: string;\n}\n\n/** Popover summarising the target machine + a link to its detail page. */\nexport function TargetInfoPopover({ target, children, className = '' }: TargetInfoPopoverProps) {\n const tr = useTr();\n const navigate = useVibeNavigate();\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n {children ?? (\n <button\n type=\"button\"\n onClick={(e) => e.stopPropagation()}\n data-testid=\"session-target-chip\"\n className={`inline-flex items-center gap-1.5 px-1.5 py-0.5 rounded-md text-xs text-text-secondary hover:bg-bg-sunken hover:text-text-primary transition-colors max-w-[150px] ${className}`}\n title={`${target.name} · ${target.type}`}\n >\n <MonitorSmartphone className=\"size-3 shrink-0\" />\n <span className=\"truncate\">{target.name}</span>\n <span\n className={`shrink-0 px-1 py-0 rounded text-[9px] font-semibold ${targetTypeBadgeClass[target.type] ?? 'bg-bg-sunken text-text-secondary'}`}\n >\n {target.type}\n </span>\n </button>\n )}\n </PopoverTrigger>\n <PopoverContent\n align=\"start\"\n className=\"w-60 p-3\"\n onClick={(e) => e.stopPropagation()}\n data-testid=\"session-target-popover\"\n >\n <div className=\"flex items-center gap-2 mb-2\">\n <MonitorSmartphone className=\"size-3.5 text-text-secondary shrink-0\" />\n <span className=\"text-sm font-semibold text-text-primary truncate\">{target.name}</span>\n <span\n className={`ml-auto shrink-0 px-1.5 py-0.5 rounded text-[10px] font-semibold ${targetTypeBadgeClass[target.type] ?? 'bg-bg-sunken text-text-secondary'}`}\n >\n {target.type}\n </span>\n </div>\n <div className=\"border-t border-border-subtle pt-1\">\n <InfoRow\n icon={<Wifi className=\"size-3\" />}\n label={tr('sessions.info.status', 'Status')}\n value={target.status || '—'}\n />\n <InfoRow\n icon={<Network className=\"size-3\" />}\n label={tr('sessions.info.address', 'Address')}\n value={target.host ? `${target.host}${target.port ? `:${target.port}` : ''}` : '—'}\n />\n </div>\n <button\n type=\"button\"\n onClick={() => navigate(`/targets/${target.id}`)}\n data-testid=\"session-target-view\"\n className=\"mt-2 w-full inline-flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs font-medium text-action-primary-text bg-action-primary-bg rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n <ExternalLink className=\"size-3\" />\n {tr('sessions.info.viewTarget', 'View target')}\n </button>\n </PopoverContent>\n </Popover>\n );\n}\n"],"mappings":";;;;;;;AAiBA,SAAS,EAAa,GAAuC;AAC3D,KAAI,CAAC,EAAI,QAAO;CAChB,IAAM,IAAO,KAAK,KAAK,GAAG,IAAI,KAAK,EAAG,CAAC,SAAS;AAKhD,QAJK,OAAO,SAAS,EAAK,GACtB,IAAO,MAAe,aACtB,IAAO,OAAkB,GAAG,KAAK,MAAM,IAAO,IAAO,CAAC,SACtD,IAAO,QAAmB,GAAG,KAAK,MAAM,IAAO,KAAU,CAAC,SACvD,IAAI,KAAK,EAAG,CAAC,oBAAoB,GAJL;;AAOrC,IAAM,IAAyC;CAC7C,QAAQ;CACR,UAAU;CACV,SAAS;CACV,EAEK,IAAmD;CACvD,QAAQ;CACR,KAAK;CACL,KAAK;CACL,KAAK;CACN;AAED,SAAS,EAAQ,EAAE,SAAM,UAAO,YAAkE;AAChG,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,QAAD;GAAM,WAAU;aAAhB,CACG,GACA,EACI;MACP,kBAAC,QAAD;GAAM,WAAU;GAAuD,OAAO;aAC3E;GACI,CAAA,CACH;;;AAYV,SAAgB,EAAiB,EAAE,UAAO,aAAU,eAAY,MAA6B;CAC3F,IAAM,IAAK,GAAO,EACZ,IAAW,GAAiB,EAC5B,IAAS,EAAe,EAAM;AAEpC,QACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;EAAgB,SAAA;YACb,KACC,kBAAC,UAAD;GACE,MAAK;GACL,UAAU,MAAM,EAAE,iBAAiB;GACnC,eAAY;GACZ,WAAW,oKAAoK;GAC/K,OAAO,EAAM;aALf;IAOE,kBAAC,QAAD;KACE,WAAW,kCAAkC,EAAe,MAAW;KACvE,eAAA;KACA,CAAA;IACF,kBAAC,GAAD,EAAQ,WAAU,mBAAoB,CAAA;IACtC,kBAAC,QAAD;KAAM,WAAU;eAAY,EAAM;KAAY,CAAA;IACvC;;EAEI,CAAA,EACjB,kBAAC,GAAD;EACE,OAAM;EACN,WAAU;EACV,UAAU,MAAM,EAAE,iBAAiB;EACnC,eAAY;YAJd;GAME,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD;MACE,WAAW,gCAAgC,EAAe,MAAW;MACrE,eAAA;MACA,CAAA;KACF,kBAAC,QAAD;MAAM,WAAU;gBAAoD,EAAM;MAAY,CAAA;KACtF,kBAAC,QAAD;MAAM,WAAU;gBACb;MACI,CAAA;KACH;;GACN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;MACnC,OAAO,EAAG,sBAAsB,OAAO;MACvC,OAAO,EAAM,YAAY;MACzB,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAK,WAAU,UAAW,CAAA;MAChC,OAAO,EAAG,0BAA0B,WAAW;MAC/C,OAAO,EAAM,YAAY;MACzB,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA;MACjC,OAAO,EAAG,wBAAwB,SAAS;MAC3C,OAAO,EAAM,iBAAiB,EAAM,YAAY,QAAQ;MACxD,CAAA;KACF,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;MAClC,OAAO,EAAG,2BAA2B,YAAY;MACjD,OAAO,EAAa,EAAM,cAAc;MACxC,CAAA;KACE;;GACN,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAS,WAAW,EAAM,KAAK;IAC9C,eAAY;IACZ,WAAU;cAJZ,CAME,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA,EAClC,EAAG,2BAA2B,aAAa,CACrC;;GACM;IACT,EAAA,CAAA;;AAWd,SAAgB,EAAkB,EAAE,WAAQ,aAAU,eAAY,MAA8B;CAC9F,IAAM,IAAK,GAAO,EACZ,IAAW,GAAiB;AAElC,QACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;EAAgB,SAAA;YACb,KACC,kBAAC,UAAD;GACE,MAAK;GACL,UAAU,MAAM,EAAE,iBAAiB;GACnC,eAAY;GACZ,WAAW,oKAAoK;GAC/K,OAAO,GAAG,EAAO,KAAK,KAAK,EAAO;aALpC;IAOE,kBAAC,GAAD,EAAmB,WAAU,mBAAoB,CAAA;IACjD,kBAAC,QAAD;KAAM,WAAU;eAAY,EAAO;KAAY,CAAA;IAC/C,kBAAC,QAAD;KACE,WAAW,uDAAuD,EAAqB,EAAO,SAAS;eAEtG,EAAO;KACH,CAAA;IACA;;EAEI,CAAA,EACjB,kBAAC,GAAD;EACE,OAAM;EACN,WAAU;EACV,UAAU,MAAM,EAAE,iBAAiB;EACnC,eAAY;YAJd;GAME,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAmB,WAAU,yCAA0C,CAAA;KACvE,kBAAC,QAAD;MAAM,WAAU;gBAAoD,EAAO;MAAY,CAAA;KACvF,kBAAC,QAAD;MACE,WAAW,oEAAoE,EAAqB,EAAO,SAAS;gBAEnH,EAAO;MACH,CAAA;KACH;;GACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KACE,MAAM,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA;KACjC,OAAO,EAAG,wBAAwB,SAAS;KAC3C,OAAO,EAAO,UAAU;KACxB,CAAA,EACF,kBAAC,GAAD;KACE,MAAM,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;KACpC,OAAO,EAAG,yBAAyB,UAAU;KAC7C,OAAO,EAAO,OAAO,GAAG,EAAO,OAAO,EAAO,OAAO,IAAI,EAAO,SAAS,OAAO;KAC/E,CAAA,CACE;;GACN,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAS,YAAY,EAAO,KAAK;IAChD,eAAY;IACZ,WAAU;cAJZ,CAME,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA,EAClC,EAAG,4BAA4B,cAAc,CACvC;;GACM;IACT,EAAA,CAAA"}
|