@burdenoff/microfe-vibecontrols 2026.602.4 → 2026.602.5
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.
|
@@ -80,7 +80,7 @@ function h({ agentId: h, tunnelUrl: g, profile: _, workspacePath: v, mode: y = "
|
|
|
80
80
|
ttlSeconds: 60
|
|
81
81
|
} })).data?.mintAgentCapability?.token;
|
|
82
82
|
if (!r) throw Error("Failed to mint editor capability");
|
|
83
|
-
let i = `${g.replace(/\/$/, "")}/code-server
|
|
83
|
+
let i = `${g.replace(/\/$/, "")}/code-server/?vt=${encodeURIComponent(r)}`;
|
|
84
84
|
y === "iframe" && b ? b(i) : window.open(i, "_blank"), setTimeout(() => w("idle"), 2e3);
|
|
85
85
|
} catch (e) {
|
|
86
86
|
E(e instanceof Error ? e.message : "Failed to open editor"), w("error");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OpenInBrowserEditorButton.js","names":[],"sources":["../../../src/components/agents/OpenInBrowserEditorButton.tsx"],"sourcesContent":["import { useState, useCallback } from 'react';\nimport { Code, Loader2, AlertCircle, Check } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n useAgentCodeServerStatusLazyQuery,\n useAgentCodeServerStartMutation,\n useAgentCodeServerStopMutation,\n useMintAgentCapabilityMutation,\n} from '@/generated/wspace-operations';\nimport { AgentCapabilityScope } from '@/generated/wspace-types';\n\n// ── Types ──────────────────────────────────────────────────────────────\n\ntype EditorState = 'idle' | 'checking' | 'installing' | 'starting' | 'ready' | 'error';\n\nexport interface OpenInBrowserEditorButtonProps {\n /**\n * Agent ID — F4 Wave B replaced the direct browser fetch (which relied\n * on `agentApiKey` headers) with svc-mediated `agentCodeServer*`\n * mutations. The agent's tunnel URL is still used for the iframe `src`,\n * but every control-plane call now routes through the gateway.\n */\n agentId: string | null;\n /** Agent tunnel URL (e.g. \"https://xxx.trycloudflare.com\") */\n tunnelUrl: string | null;\n /**\n * Canonical CLI profile name on the remote agent. Maps to the\n * `/api/profiles/<profile>/code-server/...` URL prefix.\n */\n profile: string;\n /** Directory to open as workspace root. Defaults to agent home if not set. */\n workspacePath?: string;\n /** How to open — new browser tab or call back with URL for iframe embedding */\n mode?: 'newtab' | 'iframe';\n /** Called with the ready URL when mode is 'iframe' */\n onIframeUrl?: (url: string) => void;\n /** Additional CSS class names */\n className?: string;\n}\n\ninterface CodeServerStatusPayload {\n installed?: boolean;\n installing?: boolean;\n running?: boolean;\n port?: number;\n error?: string;\n}\n\n// Code-server is a single named instance per profile; the proxy resolver\n// requires a `name`. Keep the legacy default — operators who installed\n// the plugin pre-rename still have an instance keyed under \"default\".\nconst CODE_SERVER_INSTANCE = 'default';\n\n// ── Component ──────────────────────────────────────────────────────────\n\nexport function OpenInBrowserEditorButton({\n agentId,\n tunnelUrl,\n profile,\n workspacePath,\n mode = 'newtab',\n onIframeUrl,\n className,\n}: OpenInBrowserEditorButtonProps) {\n const tr = useTr();\n const [state, setState] = useState<EditorState>('idle');\n const [errorMsg, setErrorMsg] = useState<string | null>(null);\n\n const [fetchStatus] = useAgentCodeServerStatusLazyQuery({ fetchPolicy: 'network-only' });\n const [startCodeServer] = useAgentCodeServerStartMutation();\n const [stopCodeServer] = useAgentCodeServerStopMutation();\n const [mintAgentCapability] = useMintAgentCapabilityMutation();\n\n const probe = useCallback(\n async (\n candidate: string\n ): Promise<{\n effective: string | null;\n status: CodeServerStatusPayload | null;\n missing: boolean;\n }> => {\n const res = await fetchStatus({\n variables: { agentId: agentId!, profile: candidate, name: CODE_SERVER_INSTANCE },\n });\n const proxy = res.data?.agentCodeServerStatus;\n if (!proxy) {\n return { effective: null, status: null, missing: false };\n }\n if (proxy.status === 404) {\n return { effective: null, status: null, missing: true };\n }\n if (!proxy.ok) {\n throw new Error(proxy.error || `Status check failed: HTTP ${proxy.status}`);\n }\n return {\n effective: candidate,\n status: (proxy.data as CodeServerStatusPayload | null) ?? {},\n missing: false,\n };\n },\n [fetchStatus, agentId]\n );\n\n const handleClick = useCallback(async () => {\n if (!agentId || !tunnelUrl) {\n setErrorMsg('Agent not selected');\n setState('error');\n return;\n }\n\n setErrorMsg(null);\n\n try {\n // Step 1: Probe the active profile, falling back to \"default\" if the\n // current profile reports 404 (code-server isn't a per-profile\n // service — its management API is profile-scoped though).\n setState('checking');\n const candidates = profile === 'default' ? ['default'] : [profile, 'default'];\n let effectiveProfile: string | null = null;\n let status: CodeServerStatusPayload | null = null;\n for (const candidate of candidates) {\n const { effective, status: probed, missing } = await probe(candidate);\n if (missing) continue;\n if (effective) {\n effectiveProfile = effective;\n status = probed;\n break;\n }\n }\n if (!effectiveProfile || !status) {\n throw new Error(\n tr(\n 'vibecontrols.agents.editor.notInstalled',\n \"Code-server isn't installed on this agent. Install it from the Plugins tab to enable Open Editor.\"\n )\n );\n }\n\n // Step 2: code-server needs to be installed up-front via the\n // Plugins tab. The deprecated direct-browser install fetch isn't\n // proxied; surface a clear message instead of silently hanging.\n if (!status.installed) {\n throw new Error(\n tr(\n 'vibecontrols.agents.editor.notInstalled',\n \"Code-server isn't installed on this agent. Install it from the Plugins tab to enable Open Editor.\"\n )\n );\n }\n\n // Step 3: Start if not running, or restart with a fresh workspace path.\n if (!status.running) {\n setState('starting');\n const startRes = await startCodeServer({\n variables: {\n agentId,\n profile: effectiveProfile,\n name: CODE_SERVER_INSTANCE,\n ...(workspacePath ? { path: workspacePath } : {}),\n },\n });\n const startProxy = startRes.data?.agentCodeServerStart;\n if (!startProxy?.ok) {\n throw new Error(startProxy?.error || `Start failed: HTTP ${startProxy?.status ?? 0}`);\n }\n await new Promise((r) => setTimeout(r, 1000));\n } else if (workspacePath) {\n setState('starting');\n // No dedicated restart proxy yet — stop, then start with the new path.\n await stopCodeServer({\n variables: { agentId, profile: effectiveProfile, name: CODE_SERVER_INSTANCE },\n });\n const restartRes = await startCodeServer({\n variables: {\n agentId,\n profile: effectiveProfile,\n name: CODE_SERVER_INSTANCE,\n path: workspacePath,\n },\n });\n const restartProxy = restartRes.data?.agentCodeServerStart;\n if (!restartProxy?.ok) {\n throw new Error(\n restartProxy?.error || `Restart failed: HTTP ${restartProxy?.status ?? 0}`\n );\n }\n await new Promise((r) => setTimeout(r, 1000));\n }\n\n // Step 4: Mint a short-lived IFRAME_PLUGIN capability so the\n // editor URL doesn't embed the long-lived `agentApiKey`. The\n // agent's `/code-server/` bootstrap exchanges the `#vt=` fragment\n // for a scoped HttpOnly cookie before proxying.\n setState('ready');\n const mintRes = await mintAgentCapability({\n variables: {\n agentId,\n profile: effectiveProfile,\n scope: AgentCapabilityScope.IframePlugin,\n resourceId: `code-server:${CODE_SERVER_INSTANCE}`,\n ttlSeconds: 60,\n },\n });\n const token = mintRes.data?.mintAgentCapability?.token;\n if (!token) {\n throw new Error('Failed to mint editor capability');\n }\n const base = tunnelUrl.replace(/\\/$/, '');\n const editorUrl = `${base}/code-server/#vt=${encodeURIComponent(token)}`;\n\n if (mode === 'iframe' && onIframeUrl) {\n onIframeUrl(editorUrl);\n } else {\n window.open(editorUrl, '_blank');\n }\n\n // Reset after a moment\n setTimeout(() => setState('idle'), 2000);\n } catch (err) {\n setErrorMsg(err instanceof Error ? err.message : 'Failed to open editor');\n setState('error');\n }\n }, [\n agentId,\n tunnelUrl,\n profile,\n workspacePath,\n mode,\n onIframeUrl,\n tr,\n probe,\n startCodeServer,\n stopCodeServer,\n mintAgentCapability,\n ]);\n\n const isDisabled =\n !agentId || !tunnelUrl || (state !== 'idle' && state !== 'error' && state !== 'ready');\n\n const label = (() => {\n switch (state) {\n case 'checking':\n return tr('vibecontrols.agents.editor.checking', 'Checking...');\n case 'installing':\n return tr('vibecontrols.agents.editor.installing', 'Installing...');\n case 'starting':\n return tr('vibecontrols.agents.editor.starting', 'Starting...');\n case 'ready':\n return tr('vibecontrols.agents.editor.opened', 'Opened');\n case 'error':\n return tr('vibecontrols.agents.editor.retry', 'Retry');\n default:\n return tr('vibecontrols.agents.editor.openEditor', 'Open Editor');\n }\n })();\n\n const Icon = (() => {\n switch (state) {\n case 'checking':\n case 'installing':\n case 'starting':\n return Loader2;\n case 'ready':\n return Check;\n case 'error':\n return AlertCircle;\n default:\n return Code;\n }\n })();\n\n const isSpinning = state === 'checking' || state === 'installing' || state === 'starting';\n\n return (\n <div className=\"relative inline-flex\">\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={isDisabled}\n className={`flex items-center gap-1.5 px-2.5 py-1.5 sm:px-3 sm:py-2 text-xs sm:text-sm rounded-md transition-colors disabled:opacity-50 ${\n state === 'error'\n ? 'bg-status-error-bg text-status-error-text hover:opacity-80'\n : state === 'ready'\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-bg-sunken text-text-primary hover:bg-bg-sunken/80'\n } ${className || ''}`}\n title={errorMsg || 'Open VS Code in browser via code-server'}\n >\n <Icon className={`size-3.5 sm:w-4 sm:h-4 ${isSpinning ? 'animate-spin' : ''}`} />\n <span className=\"hidden sm:inline\">{label}</span>\n </button>\n\n {/* Error popover. Anchored to the right edge (the button sits at the\n header's right side) with a fixed width so the message wraps as\n normal prose instead of being squeezed into a narrow column by the\n inline-flex wrapper. */}\n {state === 'error' && errorMsg && (\n <div\n role=\"alert\"\n className=\"absolute top-full right-0 mt-1.5 z-50 w-72 rounded-md border border-status-error-text/30 bg-bg-surface p-3 text-xs leading-relaxed text-status-error-text whitespace-normal break-words shadow-lg\"\n >\n <div className=\"flex items-start gap-2\">\n <AlertCircle className=\"mt-0.5 size-4 shrink-0\" />\n <span>{errorMsg}</span>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OpenInBrowserEditorButton.js","names":[],"sources":["../../../src/components/agents/OpenInBrowserEditorButton.tsx"],"sourcesContent":["import { useState, useCallback } from 'react';\nimport { Code, Loader2, AlertCircle, Check } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n useAgentCodeServerStatusLazyQuery,\n useAgentCodeServerStartMutation,\n useAgentCodeServerStopMutation,\n useMintAgentCapabilityMutation,\n} from '@/generated/wspace-operations';\nimport { AgentCapabilityScope } from '@/generated/wspace-types';\n\n// ── Types ──────────────────────────────────────────────────────────────\n\ntype EditorState = 'idle' | 'checking' | 'installing' | 'starting' | 'ready' | 'error';\n\nexport interface OpenInBrowserEditorButtonProps {\n /**\n * Agent ID — F4 Wave B replaced the direct browser fetch (which relied\n * on `agentApiKey` headers) with svc-mediated `agentCodeServer*`\n * mutations. The agent's tunnel URL is still used for the iframe `src`,\n * but every control-plane call now routes through the gateway.\n */\n agentId: string | null;\n /** Agent tunnel URL (e.g. \"https://xxx.trycloudflare.com\") */\n tunnelUrl: string | null;\n /**\n * Canonical CLI profile name on the remote agent. Maps to the\n * `/api/profiles/<profile>/code-server/...` URL prefix.\n */\n profile: string;\n /** Directory to open as workspace root. Defaults to agent home if not set. */\n workspacePath?: string;\n /** How to open — new browser tab or call back with URL for iframe embedding */\n mode?: 'newtab' | 'iframe';\n /** Called with the ready URL when mode is 'iframe' */\n onIframeUrl?: (url: string) => void;\n /** Additional CSS class names */\n className?: string;\n}\n\ninterface CodeServerStatusPayload {\n installed?: boolean;\n installing?: boolean;\n running?: boolean;\n port?: number;\n error?: string;\n}\n\n// Code-server is a single named instance per profile; the proxy resolver\n// requires a `name`. Keep the legacy default — operators who installed\n// the plugin pre-rename still have an instance keyed under \"default\".\nconst CODE_SERVER_INSTANCE = 'default';\n\n// ── Component ──────────────────────────────────────────────────────────\n\nexport function OpenInBrowserEditorButton({\n agentId,\n tunnelUrl,\n profile,\n workspacePath,\n mode = 'newtab',\n onIframeUrl,\n className,\n}: OpenInBrowserEditorButtonProps) {\n const tr = useTr();\n const [state, setState] = useState<EditorState>('idle');\n const [errorMsg, setErrorMsg] = useState<string | null>(null);\n\n const [fetchStatus] = useAgentCodeServerStatusLazyQuery({ fetchPolicy: 'network-only' });\n const [startCodeServer] = useAgentCodeServerStartMutation();\n const [stopCodeServer] = useAgentCodeServerStopMutation();\n const [mintAgentCapability] = useMintAgentCapabilityMutation();\n\n const probe = useCallback(\n async (\n candidate: string\n ): Promise<{\n effective: string | null;\n status: CodeServerStatusPayload | null;\n missing: boolean;\n }> => {\n const res = await fetchStatus({\n variables: { agentId: agentId!, profile: candidate, name: CODE_SERVER_INSTANCE },\n });\n const proxy = res.data?.agentCodeServerStatus;\n if (!proxy) {\n return { effective: null, status: null, missing: false };\n }\n if (proxy.status === 404) {\n return { effective: null, status: null, missing: true };\n }\n if (!proxy.ok) {\n throw new Error(proxy.error || `Status check failed: HTTP ${proxy.status}`);\n }\n return {\n effective: candidate,\n status: (proxy.data as CodeServerStatusPayload | null) ?? {},\n missing: false,\n };\n },\n [fetchStatus, agentId]\n );\n\n const handleClick = useCallback(async () => {\n if (!agentId || !tunnelUrl) {\n setErrorMsg('Agent not selected');\n setState('error');\n return;\n }\n\n setErrorMsg(null);\n\n try {\n // Step 1: Probe the active profile, falling back to \"default\" if the\n // current profile reports 404 (code-server isn't a per-profile\n // service — its management API is profile-scoped though).\n setState('checking');\n const candidates = profile === 'default' ? ['default'] : [profile, 'default'];\n let effectiveProfile: string | null = null;\n let status: CodeServerStatusPayload | null = null;\n for (const candidate of candidates) {\n const { effective, status: probed, missing } = await probe(candidate);\n if (missing) continue;\n if (effective) {\n effectiveProfile = effective;\n status = probed;\n break;\n }\n }\n if (!effectiveProfile || !status) {\n throw new Error(\n tr(\n 'vibecontrols.agents.editor.notInstalled',\n \"Code-server isn't installed on this agent. Install it from the Plugins tab to enable Open Editor.\"\n )\n );\n }\n\n // Step 2: code-server needs to be installed up-front via the\n // Plugins tab. The deprecated direct-browser install fetch isn't\n // proxied; surface a clear message instead of silently hanging.\n if (!status.installed) {\n throw new Error(\n tr(\n 'vibecontrols.agents.editor.notInstalled',\n \"Code-server isn't installed on this agent. Install it from the Plugins tab to enable Open Editor.\"\n )\n );\n }\n\n // Step 3: Start if not running, or restart with a fresh workspace path.\n if (!status.running) {\n setState('starting');\n const startRes = await startCodeServer({\n variables: {\n agentId,\n profile: effectiveProfile,\n name: CODE_SERVER_INSTANCE,\n ...(workspacePath ? { path: workspacePath } : {}),\n },\n });\n const startProxy = startRes.data?.agentCodeServerStart;\n if (!startProxy?.ok) {\n throw new Error(startProxy?.error || `Start failed: HTTP ${startProxy?.status ?? 0}`);\n }\n await new Promise((r) => setTimeout(r, 1000));\n } else if (workspacePath) {\n setState('starting');\n // No dedicated restart proxy yet — stop, then start with the new path.\n await stopCodeServer({\n variables: { agentId, profile: effectiveProfile, name: CODE_SERVER_INSTANCE },\n });\n const restartRes = await startCodeServer({\n variables: {\n agentId,\n profile: effectiveProfile,\n name: CODE_SERVER_INSTANCE,\n path: workspacePath,\n },\n });\n const restartProxy = restartRes.data?.agentCodeServerStart;\n if (!restartProxy?.ok) {\n throw new Error(\n restartProxy?.error || `Restart failed: HTTP ${restartProxy?.status ?? 0}`\n );\n }\n await new Promise((r) => setTimeout(r, 1000));\n }\n\n // Step 4: Mint a short-lived IFRAME_PLUGIN capability so the\n // editor URL doesn't embed the long-lived `agentApiKey`. The agent\n // consumes the `?vt=` query on the first `/code-server/` request and\n // mints a scoped proxy session cookie server-side before proxying.\n setState('ready');\n const mintRes = await mintAgentCapability({\n variables: {\n agentId,\n profile: effectiveProfile,\n scope: AgentCapabilityScope.IframePlugin,\n resourceId: `code-server:${CODE_SERVER_INSTANCE}`,\n ttlSeconds: 60,\n },\n });\n const token = mintRes.data?.mintAgentCapability?.token;\n if (!token) {\n throw new Error('Failed to mint editor capability');\n }\n const base = tunnelUrl.replace(/\\/$/, '');\n // code-server reads the capability from the `?vt=` QUERY (GET-only): the\n // agent consumes it server-side on the first `/code-server/` request to\n // mint the proxy session cookie. A `#vt=` fragment is never sent to the\n // server, so the editor would 401 before it could bootstrap (code-server\n // has no client-side fragment-exchange shim, unlike `/ui/*` / `/terminal`).\n const editorUrl = `${base}/code-server/?vt=${encodeURIComponent(token)}`;\n\n if (mode === 'iframe' && onIframeUrl) {\n onIframeUrl(editorUrl);\n } else {\n window.open(editorUrl, '_blank');\n }\n\n // Reset after a moment\n setTimeout(() => setState('idle'), 2000);\n } catch (err) {\n setErrorMsg(err instanceof Error ? err.message : 'Failed to open editor');\n setState('error');\n }\n }, [\n agentId,\n tunnelUrl,\n profile,\n workspacePath,\n mode,\n onIframeUrl,\n tr,\n probe,\n startCodeServer,\n stopCodeServer,\n mintAgentCapability,\n ]);\n\n const isDisabled =\n !agentId || !tunnelUrl || (state !== 'idle' && state !== 'error' && state !== 'ready');\n\n const label = (() => {\n switch (state) {\n case 'checking':\n return tr('vibecontrols.agents.editor.checking', 'Checking...');\n case 'installing':\n return tr('vibecontrols.agents.editor.installing', 'Installing...');\n case 'starting':\n return tr('vibecontrols.agents.editor.starting', 'Starting...');\n case 'ready':\n return tr('vibecontrols.agents.editor.opened', 'Opened');\n case 'error':\n return tr('vibecontrols.agents.editor.retry', 'Retry');\n default:\n return tr('vibecontrols.agents.editor.openEditor', 'Open Editor');\n }\n })();\n\n const Icon = (() => {\n switch (state) {\n case 'checking':\n case 'installing':\n case 'starting':\n return Loader2;\n case 'ready':\n return Check;\n case 'error':\n return AlertCircle;\n default:\n return Code;\n }\n })();\n\n const isSpinning = state === 'checking' || state === 'installing' || state === 'starting';\n\n return (\n <div className=\"relative inline-flex\">\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={isDisabled}\n className={`flex items-center gap-1.5 px-2.5 py-1.5 sm:px-3 sm:py-2 text-xs sm:text-sm rounded-md transition-colors disabled:opacity-50 ${\n state === 'error'\n ? 'bg-status-error-bg text-status-error-text hover:opacity-80'\n : state === 'ready'\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-bg-sunken text-text-primary hover:bg-bg-sunken/80'\n } ${className || ''}`}\n title={errorMsg || 'Open VS Code in browser via code-server'}\n >\n <Icon className={`size-3.5 sm:w-4 sm:h-4 ${isSpinning ? 'animate-spin' : ''}`} />\n <span className=\"hidden sm:inline\">{label}</span>\n </button>\n\n {/* Error popover. Anchored to the right edge (the button sits at the\n header's right side) with a fixed width so the message wraps as\n normal prose instead of being squeezed into a narrow column by the\n inline-flex wrapper. */}\n {state === 'error' && errorMsg && (\n <div\n role=\"alert\"\n className=\"absolute top-full right-0 mt-1.5 z-50 w-72 rounded-md border border-status-error-text/30 bg-bg-surface p-3 text-xs leading-relaxed text-status-error-text whitespace-normal break-words shadow-lg\"\n >\n <div className=\"flex items-start gap-2\">\n <AlertCircle className=\"mt-0.5 size-4 shrink-0\" />\n <span>{errorMsg}</span>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -26,7 +26,9 @@ function w({ availablePlugins: S, tunnelUrl: w, agentId: T, profile: E }) {
|
|
|
26
26
|
resourceId: e,
|
|
27
27
|
ttlSeconds: 60
|
|
28
28
|
} })).data?.mintAgentCapability?.token;
|
|
29
|
-
|
|
29
|
+
if (!n) return a;
|
|
30
|
+
let r = new URL(a);
|
|
31
|
+
return e === "code-server" || r.pathname.startsWith("/code-server") ? (r.searchParams.set("vt", n), r.toString()) : `${a}#vt=${encodeURIComponent(n)}`;
|
|
30
32
|
} catch {
|
|
31
33
|
return a;
|
|
32
34
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluginUIPanel.js","names":[],"sources":["../../../src/components/agents/PluginUIPanel.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useState, useCallback, useRef, useEffect } from 'react';\nimport { useMintAgentCapabilityMutation } from '@/generated/wspace-operations';\nimport { AgentCapabilityScope } from '@/generated/wspace-types';\nimport {\n X,\n Maximize2,\n Minimize2,\n ExternalLink,\n Loader2,\n AlertCircle,\n RotateCcw,\n Wifi,\n WifiOff,\n GripVertical,\n Plus,\n Monitor,\n ChevronDown,\n} from 'lucide-react';\n\n// ── Types ──────────────────────────────────────────────────────────────\n\nexport interface PluginUITab {\n /** Unique tab ID (allows duplicate plugins in multiple tabs) */\n id: string;\n /** The plugin name (e.g. \"ui-ai\") — may be null if nothing selected yet */\n pluginName: string | null;\n /** Display name shown on the tab */\n name: string;\n /** The full URL to load in the iframe */\n uiUrl: string | null;\n}\n\nexport interface UIPluginOption {\n /** The plugin name key (e.g. \"ui-ai\") */\n pluginName: string;\n /** Display name */\n displayName: string;\n /** Description shown in dropdown */\n description?: string;\n /** Version */\n version?: string;\n /** Custom URL to use instead of the default /ui/{pluginName} path (e.g. for code-server proxy) */\n customUrl?: string;\n}\n\ninterface PluginUIPanelProps {\n /** Available UI plugins the user can pick from */\n availablePlugins: UIPluginOption[];\n /** Base URL of the agent tunnel (e.g. \"https://xxx.trycloudflare.com\") */\n tunnelUrl: string | null;\n /**\n * Agent ID — F4 Wave B replaced the legacy `?apiKey=` URL embed +\n * postMessage handoff with `mintAgentCapability(IFRAME_PLUGIN)`. Each\n * plugin tab loads with `#vt=<token>` so the long-lived agent secret\n * never reaches the browser.\n */\n agentId: string | null;\n /** Canonical CLI profile name; required for capability mint scoping. */\n profile: string;\n}\n\nlet tabIdCounter = 0;\nfunction nextTabId(): string {\n return `ui-tab-${++tabIdCounter}`;\n}\n\n/**\n * Embedded browser-like panel for plugin UIs.\n * Each tab has a dropdown to select which plugin to load. The same plugin can\n * be loaded in multiple tabs, each with its own navigation state.\n */\nexport function PluginUIPanel({\n availablePlugins,\n tunnelUrl,\n agentId,\n profile,\n}: PluginUIPanelProps) {\n const [mintAgentCapability] = useMintAgentCapabilityMutation();\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 [tabs, setTabs] = useState<PluginUITab[]>(() => {\n // Start with one blank tab\n return [{ id: nextTabId(), pluginName: null, name: 'New Tab', uiUrl: null }];\n });\n const [activeTabId, setActiveTabId] = useState<string>(tabs[0].id);\n const [maximized, setMaximized] = useState(false);\n\n // Connection status per tab\n const [connectionStatus, setConnectionStatus] = useState<\n Record<string, 'connecting' | 'connected' | 'error'>\n >({});\n\n // Dropdown open state per tab\n const [dropdownOpen, setDropdownOpen] = useState<string | null>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n // Drag-to-resize state\n const [resizeHeight, setResizeHeight] = useState<number | null>(null);\n const resizingRef = useRef(false);\n const startYRef = useRef(0);\n const startHeightRef = useRef(0);\n const panelRef = useRef<HTMLDivElement>(null);\n\n // Iframe refs for auth postMessage\n const iframeRefs = useRef<Record<string, HTMLIFrameElement | null>>({});\n\n const activeTab = tabs.find((t) => t.id === activeTabId) || tabs[0] || null;\n\n // Build UI URL for a plugin — F4 Wave B: append a short-lived\n // IFRAME_PLUGIN capability as `#vt=<token>` so the agent's `/ui/<name>`\n // bootstrap can exchange it for a scoped HttpOnly cookie. No\n // long-lived secret in the URL or postMessage.\n const buildUiUrl = useCallback(\n async (pluginName: string, customUrl?: string): Promise<string | null> => {\n if (!tunnelUrl || !agentId) return null;\n const base = tunnelUrl.replace(/\\/$/, '');\n\n // Use customUrl if provided (e.g. for code-server proxy path)\n const urlStr = customUrl\n ? customUrl.startsWith('http')\n ? customUrl\n : `${base}${customUrl}`\n : `${base}/ui/${pluginName}`;\n\n const finalUrl = new URL(urlStr).toString();\n\n try {\n const res = await mintAgentCapability({\n variables: {\n agentId,\n profile: profile ?? 'default',\n scope: AgentCapabilityScope.IframePlugin,\n // Pass the bare plugin name — the svc maps to `/ui/<plugin>`\n // which matches the agent's iframe-token allow-list. Previous\n // `ui:<plugin>` form produced an invalid `/ui/ui:<plugin>`\n // prefix that the agent now rejects (audit-B P0-SEC-03).\n resourceId: pluginName,\n ttlSeconds: 60,\n },\n });\n const token = res.data?.mintAgentCapability?.token;\n if (!token) return finalUrl;\n // Fragment-only — never sent to proxies/logs/Referer.\n return `${finalUrl}#vt=${encodeURIComponent(token)}`;\n } catch {\n return finalUrl;\n }\n },\n [tunnelUrl, agentId, profile, mintAgentCapability]\n );\n\n // Display name for a plugin name\n const getDisplayName = useCallback(\n (pluginName: string): string => {\n const opt = availablePlugins.find((p) => p.pluginName === pluginName);\n return (\n opt?.displayName ||\n pluginName\n .replace(/^ui-/, '')\n .replace(/-/g, ' ')\n .replace(/\\b\\w/g, (c) => c.toUpperCase())\n );\n },\n [availablePlugins]\n );\n\n // ── Tab Actions ───────────────────────────────────────────────────\n\n const handleAddTab = useCallback(() => {\n const newTab: PluginUITab = {\n id: nextTabId(),\n pluginName: null,\n name: 'New Tab',\n uiUrl: null,\n };\n setTabs((prev) => [...prev, newTab]);\n setActiveTabId(newTab.id);\n setDropdownOpen(newTab.id);\n }, []);\n\n const handleCloseTab = useCallback(\n (tabId: string) => {\n setTabs((prev) => {\n const next = prev.filter((t) => t.id !== tabId);\n // If we closed the last tab, add a blank one\n if (next.length === 0) {\n const blank: PluginUITab = {\n id: nextTabId(),\n pluginName: null,\n name: 'New Tab',\n uiUrl: null,\n };\n next.push(blank);\n setActiveTabId(blank.id);\n } else if (activeTabId === tabId) {\n // Select the previous tab or the first one\n const closedIdx = prev.findIndex((t) => t.id === tabId);\n const newIdx = Math.max(0, closedIdx - 1);\n setActiveTabId(next[Math.min(newIdx, next.length - 1)].id);\n }\n return next;\n });\n // Clean up refs\n delete iframeRefs.current[tabId];\n setConnectionStatus((prev) => {\n const next = { ...prev };\n delete next[tabId];\n return next;\n });\n },\n [activeTabId]\n );\n\n const handleSelectPlugin = useCallback(\n async (tabId: string, pluginName: string) => {\n const opt = availablePlugins.find((p) => p.pluginName === pluginName);\n const displayName = getDisplayName(pluginName);\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'connecting' }));\n setDropdownOpen(null);\n const uiUrl = await buildUiUrl(pluginName, opt?.customUrl);\n setTabs((prev) =>\n prev.map((t) => (t.id === tabId ? { ...t, pluginName, name: displayName, uiUrl } : t))\n );\n },\n [availablePlugins, buildUiUrl, getDisplayName]\n );\n\n // ── Auth ──────────────────────────────────────────────────────────\n // F4 Wave B: auth is now handled by the `#vt=` fragment in the iframe\n // src. The agent exchanges it for a scoped cookie on first load. No\n // postMessage handoff needed.\n\n const handleIframeLoad = useCallback((tabId: string) => {\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'connected' }));\n }, []);\n\n const handleIframeError = useCallback((tabId: string) => {\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'error' }));\n }, []);\n\n const handleReload = useCallback((tabId: string) => {\n const iframe = iframeRefs.current[tabId];\n if (iframe) {\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'connecting' }));\n const src = iframe.src;\n iframe.src = 'about:blank';\n requestAnimationFrame(() => {\n iframe.src = src;\n });\n }\n }, []);\n\n const handleOpenExternal = useCallback(\n (tabId: string) => {\n const tab = tabs.find((t) => t.id === tabId);\n if (!tab?.uiUrl) return;\n window.open(tab.uiUrl, '_blank');\n },\n [tabs]\n );\n\n // ── Close dropdown on outside click ───────────────────────────────\n\n useEffect(() => {\n if (!dropdownOpen) return;\n const handler = (e: MouseEvent) => {\n if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {\n setDropdownOpen(null);\n }\n };\n document.addEventListener('mousedown', handler);\n return () => document.removeEventListener('mousedown', handler);\n }, [dropdownOpen]);\n\n // ── Drag-to-resize ────────────────────────────────────────────────\n\n const handleResizeStart = useCallback(\n (e: React.MouseEvent) => {\n if (maximized) return;\n e.preventDefault();\n resizingRef.current = true;\n startYRef.current = e.clientY;\n startHeightRef.current = panelRef.current?.offsetHeight || 500;\n document.body.style.cursor = 'ns-resize';\n document.body.style.userSelect = 'none';\n },\n [maximized]\n );\n\n useEffect(() => {\n const handleMouseMove = (e: MouseEvent) => {\n if (!resizingRef.current) return;\n const delta = startYRef.current - e.clientY;\n const newHeight = Math.max(\n 250,\n Math.min(startHeightRef.current + delta, window.innerHeight - 150)\n );\n setResizeHeight(newHeight);\n };\n const handleMouseUp = () => {\n if (resizingRef.current) {\n resizingRef.current = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n }\n };\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n }, []);\n\n // ── Render helpers ────────────────────────────────────────────────\n\n const renderStatusDot = (tabId: string) => {\n const status = connectionStatus[tabId];\n if (status === 'connected')\n return <Wifi className=\"size-3 text-status-success-text shrink-0\" />;\n if (status === 'error') return <WifiOff className=\"size-3 text-status-error-text shrink-0\" />;\n if (status === 'connecting')\n return <Loader2 className=\"size-3 animate-spin text-status-warning-text shrink-0\" />;\n return <Monitor className=\"size-3 text-text-muted shrink-0\" />;\n };\n\n const panelHeight = maximized ? 'h-[calc(100vh-200px)]' : resizeHeight ? undefined : 'h-[500px]';\n const panelStyle = !maximized && resizeHeight ? { height: `${resizeHeight}px` } : undefined;\n\n return (\n <div\n ref={panelRef}\n className=\"border border-border-default rounded-lg overflow-hidden bg-bg-sunken\"\n >\n {/* Drag handle */}\n <div\n onMouseDown={handleResizeStart}\n className=\"h-1 cursor-ns-resize hover:bg-action-primary-bg/30 transition-colors flex items-center justify-center group\"\n title=\"Drag to resize\"\n >\n <GripVertical className=\"w-4 h-3 text-text-muted opacity-0 group-hover:opacity-100 transition-opacity rotate-90\" />\n </div>\n\n {/* ── Tab Bar ────────────────────────────────────────────────── */}\n <div className=\"flex items-center bg-bg-secondary border-b border-border-default overflow-x-auto\">\n {tabs.map((tab) => {\n const isActive = tab.id === activeTab?.id;\n return (\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => setActiveTabId(tab.id))();\n }\n }}\n key={tab.id}\n className={`group flex items-center gap-1.5 pl-3 pr-1.5 py-1.5 text-xs border-r border-border-subtle cursor-pointer transition-colors min-w-0 max-w-[200px] shrink-0 ${\n isActive\n ? 'bg-bg-surface text-text-primary border-b-2 border-b-action-primary-bg'\n : 'text-text-secondary hover:text-text-primary hover:bg-bg-surface/50'\n }`}\n onClick={() => setActiveTabId(tab.id)}\n >\n {renderStatusDot(tab.id)}\n <span className=\"truncate\">{tab.name}</span>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n handleCloseTab(tab.id);\n }}\n className=\"ml-auto opacity-0 group-hover:opacity-100 hover:text-status-error-text transition-opacity shrink-0 p-0.5\"\n title=\"Close tab\"\n >\n <X className=\"size-3\" />\n </button>\n </div>\n );\n })}\n\n {/* Add tab button */}\n <button\n type=\"button\"\n onClick={handleAddTab}\n className=\"flex items-center justify-center size-8 shrink-0 text-text-secondary hover:text-text-primary hover:bg-bg-surface/50 transition-colors\"\n title=\"New tab\"\n >\n <Plus className=\"size-3.5\" />\n </button>\n\n {/* Spacer */}\n <div className=\"flex-1 min-w-0\" />\n\n {/* Global panel actions */}\n <div className=\"flex items-center gap-0.5 px-2 shrink-0\">\n {activeTab?.uiUrl && (\n <>\n <button\n type=\"button\"\n onClick={() => activeTab && handleReload(activeTab.id)}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title=\"Reload\"\n >\n <RotateCcw className=\"size-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => activeTab && handleOpenExternal(activeTab.id)}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title=\"Open in new browser tab\"\n >\n <ExternalLink className=\"size-3.5\" />\n </button>\n </>\n )}\n <button\n type=\"button\"\n onClick={() => {\n setMaximized((prev) => !prev);\n setResizeHeight(null);\n }}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title={maximized ? 'Restore size' : 'Maximize'}\n >\n {maximized ? <Minimize2 className=\"size-3.5\" /> : <Maximize2 className=\"size-3.5\" />}\n </button>\n </div>\n </div>\n\n {/* ── Address Bar (Plugin Selector Dropdown) ─────────────────── */}\n {activeTab && (\n <div className=\"flex items-center gap-2 px-3 py-1.5 bg-bg-surface border-b border-border-default\">\n <Monitor className=\"size-4 text-text-muted shrink-0\" />\n <div className=\"relative flex-1 min-w-0\" ref={dropdownRef}>\n <button\n type=\"button\"\n onClick={() => setDropdownOpen(dropdownOpen === activeTab.id ? null : activeTab.id)}\n className=\"w-full flex items-center justify-between gap-2 px-3 py-1.5 text-sm bg-bg-sunken border border-border-default rounded-md text-left hover:border-action-primary-bg/50 focus:outline-none focus:ring-1 focus:ring-action-primary-bg transition-colors\"\n >\n <span\n className={`truncate ${activeTab.pluginName ? 'text-text-primary' : 'text-text-muted'}`}\n >\n {activeTab.pluginName\n ? `${activeTab.name} — ${activeTab.pluginName}`\n : 'Select a plugin to load...'}\n </span>\n <ChevronDown\n className={`size-3.5 text-text-muted shrink-0 transition-transform ${dropdownOpen === activeTab.id ? 'rotate-180' : ''}`}\n />\n </button>\n\n {/* Dropdown list */}\n {dropdownOpen === activeTab.id && (\n <div className=\"absolute z-50 top-full left-0 right-0 mt-1 bg-bg-surface border border-border-default rounded-md shadow-lg overflow-hidden max-h-64 overflow-y-auto\">\n {availablePlugins.length > 0 ? (\n availablePlugins.map((opt) => {\n const isSelected = activeTab.pluginName === opt.pluginName;\n return (\n <button\n type=\"button\"\n key={opt.pluginName}\n onClick={() => handleSelectPlugin(activeTab.id, opt.pluginName)}\n className={`w-full text-left px-3 py-2.5 hover:bg-bg-sunken transition-colors border-b border-border-subtle last:border-b-0 ${\n isSelected ? 'bg-action-primary-bg/10' : ''\n }`}\n >\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">\n {opt.displayName}\n </span>\n {opt.version && (\n <span className=\"text-xs text-text-muted\">\n {opt.version === 'latest' ? opt.version : `v${opt.version}`}\n </span>\n )}\n </div>\n {opt.description && (\n <p className=\"text-xs text-text-secondary mt-0.5\">{opt.description}</p>\n )}\n <p className=\"text-xs text-text-muted font-mono mt-0.5\">{opt.pluginName}</p>\n </button>\n );\n })\n ) : (\n <div className=\"px-3 py-4 text-center text-sm text-text-muted\">\n No UI plugins available\n </div>\n )}\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* ── Content Area ───────────────────────────────────────────── */}\n <div\n className={`${panelHeight || ''} bg-bg-inverse text-text-inverse relative`}\n style={panelStyle}\n >\n {tabs.map((tab) => (\n <div\n key={tab.id}\n className={`absolute inset-0 ${tab.id === activeTab?.id ? 'block' : 'hidden'}`}\n >\n {!tab.pluginName ? (\n // Empty state — no plugin selected\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center max-w-sm\">\n <Monitor className=\"size-10 text-text-secondary mx-auto mb-3\" />\n <p className=\"text-text-tertiary text-sm font-medium\">No plugin selected</p>\n <p className=\"text-text-secondary text-xs mt-1\">\n Use the dropdown above to pick a plugin UI to load in this tab.\n </p>\n <button\n type=\"button\"\n onClick={() => setDropdownOpen(tab.id)}\n className=\"mt-4 px-4 py-2 bg-bg-sunken text-text-primary text-sm rounded-md hover:bg-bg-muted transition-colors inline-flex items-center gap-2\"\n >\n <ChevronDown className=\"size-3.5\" />\n Select Plugin\n </button>\n </div>\n </div>\n ) : !tab.uiUrl ? (\n // No tunnel\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center\">\n <AlertCircle className=\"size-8 text-text-secondary mx-auto mb-3\" />\n <p className=\"text-text-tertiary text-sm\">Agent tunnel not active</p>\n <p className=\"text-text-secondary text-xs mt-1\">\n Cannot load plugin UI without an active tunnel connection.\n </p>\n </div>\n </div>\n ) : (\n // Iframe\n <iframe\n ref={(el) => {\n iframeRefs.current[tab.id] = el;\n }}\n src={tab.uiUrl}\n title={`Plugin UI — ${tab.name}`}\n className=\"w-full h-full border-0\"\n allow=\"clipboard-read; clipboard-write\"\n onLoad={() => handleIframeLoad(tab.id)}\n onError={() => handleIframeError(tab.id)}\n />\n )}\n </div>\n ))}\n\n {/* Connection status overlay */}\n {activeTab && connectionStatus[activeTab.id] === 'connecting' && activeTab.uiUrl && (\n <div className=\"absolute bottom-3 right-3 flex items-center gap-2 px-3 py-1.5 bg-bg-overlay backdrop-blur-sm rounded-md text-xs text-status-warning-text pointer-events-none\">\n <Loader2 className=\"size-3 animate-spin\" />\n Connecting...\n </div>\n )}\n {activeTab && connectionStatus[activeTab.id] === 'error' && activeTab.uiUrl && (\n <div className=\"absolute bottom-3 right-3 flex items-center gap-2 px-3 py-1.5 bg-bg-overlay backdrop-blur-sm rounded-md text-xs text-status-error-text\">\n <WifiOff className=\"size-3\" />\n Disconnected\n <button\n type=\"button\"\n onClick={() => handleReload(activeTab.id)}\n className=\"underline hover:text-status-error-text/80 transition-colors\"\n >\n Reconnect\n </button>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PluginUIPanel.js","names":[],"sources":["../../../src/components/agents/PluginUIPanel.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useState, useCallback, useRef, useEffect } from 'react';\nimport { useMintAgentCapabilityMutation } from '@/generated/wspace-operations';\nimport { AgentCapabilityScope } from '@/generated/wspace-types';\nimport {\n X,\n Maximize2,\n Minimize2,\n ExternalLink,\n Loader2,\n AlertCircle,\n RotateCcw,\n Wifi,\n WifiOff,\n GripVertical,\n Plus,\n Monitor,\n ChevronDown,\n} from 'lucide-react';\n\n// ── Types ──────────────────────────────────────────────────────────────\n\nexport interface PluginUITab {\n /** Unique tab ID (allows duplicate plugins in multiple tabs) */\n id: string;\n /** The plugin name (e.g. \"ui-ai\") — may be null if nothing selected yet */\n pluginName: string | null;\n /** Display name shown on the tab */\n name: string;\n /** The full URL to load in the iframe */\n uiUrl: string | null;\n}\n\nexport interface UIPluginOption {\n /** The plugin name key (e.g. \"ui-ai\") */\n pluginName: string;\n /** Display name */\n displayName: string;\n /** Description shown in dropdown */\n description?: string;\n /** Version */\n version?: string;\n /** Custom URL to use instead of the default /ui/{pluginName} path (e.g. for code-server proxy) */\n customUrl?: string;\n}\n\ninterface PluginUIPanelProps {\n /** Available UI plugins the user can pick from */\n availablePlugins: UIPluginOption[];\n /** Base URL of the agent tunnel (e.g. \"https://xxx.trycloudflare.com\") */\n tunnelUrl: string | null;\n /**\n * Agent ID — F4 Wave B replaced the legacy `?apiKey=` URL embed +\n * postMessage handoff with `mintAgentCapability(IFRAME_PLUGIN)`. Each\n * plugin tab loads with `#vt=<token>` so the long-lived agent secret\n * never reaches the browser.\n */\n agentId: string | null;\n /** Canonical CLI profile name; required for capability mint scoping. */\n profile: string;\n}\n\nlet tabIdCounter = 0;\nfunction nextTabId(): string {\n return `ui-tab-${++tabIdCounter}`;\n}\n\n/**\n * Embedded browser-like panel for plugin UIs.\n * Each tab has a dropdown to select which plugin to load. The same plugin can\n * be loaded in multiple tabs, each with its own navigation state.\n */\nexport function PluginUIPanel({\n availablePlugins,\n tunnelUrl,\n agentId,\n profile,\n}: PluginUIPanelProps) {\n const [mintAgentCapability] = useMintAgentCapabilityMutation();\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 [tabs, setTabs] = useState<PluginUITab[]>(() => {\n // Start with one blank tab\n return [{ id: nextTabId(), pluginName: null, name: 'New Tab', uiUrl: null }];\n });\n const [activeTabId, setActiveTabId] = useState<string>(tabs[0].id);\n const [maximized, setMaximized] = useState(false);\n\n // Connection status per tab\n const [connectionStatus, setConnectionStatus] = useState<\n Record<string, 'connecting' | 'connected' | 'error'>\n >({});\n\n // Dropdown open state per tab\n const [dropdownOpen, setDropdownOpen] = useState<string | null>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n // Drag-to-resize state\n const [resizeHeight, setResizeHeight] = useState<number | null>(null);\n const resizingRef = useRef(false);\n const startYRef = useRef(0);\n const startHeightRef = useRef(0);\n const panelRef = useRef<HTMLDivElement>(null);\n\n // Iframe refs for auth postMessage\n const iframeRefs = useRef<Record<string, HTMLIFrameElement | null>>({});\n\n const activeTab = tabs.find((t) => t.id === activeTabId) || tabs[0] || null;\n\n // Build UI URL for a plugin — F4 Wave B: append a short-lived\n // IFRAME_PLUGIN capability as `#vt=<token>` so the agent's `/ui/<name>`\n // bootstrap can exchange it for a scoped HttpOnly cookie. No\n // long-lived secret in the URL or postMessage.\n const buildUiUrl = useCallback(\n async (pluginName: string, customUrl?: string): Promise<string | null> => {\n if (!tunnelUrl || !agentId) return null;\n const base = tunnelUrl.replace(/\\/$/, '');\n\n // Use customUrl if provided (e.g. for code-server proxy path)\n const urlStr = customUrl\n ? customUrl.startsWith('http')\n ? customUrl\n : `${base}${customUrl}`\n : `${base}/ui/${pluginName}`;\n\n const finalUrl = new URL(urlStr).toString();\n\n try {\n const res = await mintAgentCapability({\n variables: {\n agentId,\n profile: profile ?? 'default',\n scope: AgentCapabilityScope.IframePlugin,\n // Pass the bare plugin name — the svc maps to `/ui/<plugin>`\n // which matches the agent's iframe-token allow-list. Previous\n // `ui:<plugin>` form produced an invalid `/ui/ui:<plugin>`\n // prefix that the agent now rejects (audit-B P0-SEC-03).\n resourceId: pluginName,\n ttlSeconds: 60,\n },\n });\n const token = res.data?.mintAgentCapability?.token;\n if (!token) return finalUrl;\n // code-server's reverse proxy reads the capability from the `?vt=`\n // QUERY (GET-only) and consumes it server-side to mint its session\n // cookie — it has no client-side fragment-exchange shim. `/ui/<plugin>`\n // (and `/terminal`) DO have that shim, so they keep the `#vt=`\n // fragment, which is never sent to proxies/logs/Referer.\n const builtUrl = new URL(finalUrl);\n const isCodeServer =\n pluginName === 'code-server' || builtUrl.pathname.startsWith('/code-server');\n if (isCodeServer) {\n builtUrl.searchParams.set('vt', token);\n return builtUrl.toString();\n }\n return `${finalUrl}#vt=${encodeURIComponent(token)}`;\n } catch {\n return finalUrl;\n }\n },\n [tunnelUrl, agentId, profile, mintAgentCapability]\n );\n\n // Display name for a plugin name\n const getDisplayName = useCallback(\n (pluginName: string): string => {\n const opt = availablePlugins.find((p) => p.pluginName === pluginName);\n return (\n opt?.displayName ||\n pluginName\n .replace(/^ui-/, '')\n .replace(/-/g, ' ')\n .replace(/\\b\\w/g, (c) => c.toUpperCase())\n );\n },\n [availablePlugins]\n );\n\n // ── Tab Actions ───────────────────────────────────────────────────\n\n const handleAddTab = useCallback(() => {\n const newTab: PluginUITab = {\n id: nextTabId(),\n pluginName: null,\n name: 'New Tab',\n uiUrl: null,\n };\n setTabs((prev) => [...prev, newTab]);\n setActiveTabId(newTab.id);\n setDropdownOpen(newTab.id);\n }, []);\n\n const handleCloseTab = useCallback(\n (tabId: string) => {\n setTabs((prev) => {\n const next = prev.filter((t) => t.id !== tabId);\n // If we closed the last tab, add a blank one\n if (next.length === 0) {\n const blank: PluginUITab = {\n id: nextTabId(),\n pluginName: null,\n name: 'New Tab',\n uiUrl: null,\n };\n next.push(blank);\n setActiveTabId(blank.id);\n } else if (activeTabId === tabId) {\n // Select the previous tab or the first one\n const closedIdx = prev.findIndex((t) => t.id === tabId);\n const newIdx = Math.max(0, closedIdx - 1);\n setActiveTabId(next[Math.min(newIdx, next.length - 1)].id);\n }\n return next;\n });\n // Clean up refs\n delete iframeRefs.current[tabId];\n setConnectionStatus((prev) => {\n const next = { ...prev };\n delete next[tabId];\n return next;\n });\n },\n [activeTabId]\n );\n\n const handleSelectPlugin = useCallback(\n async (tabId: string, pluginName: string) => {\n const opt = availablePlugins.find((p) => p.pluginName === pluginName);\n const displayName = getDisplayName(pluginName);\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'connecting' }));\n setDropdownOpen(null);\n const uiUrl = await buildUiUrl(pluginName, opt?.customUrl);\n setTabs((prev) =>\n prev.map((t) => (t.id === tabId ? { ...t, pluginName, name: displayName, uiUrl } : t))\n );\n },\n [availablePlugins, buildUiUrl, getDisplayName]\n );\n\n // ── Auth ──────────────────────────────────────────────────────────\n // F4 Wave B: auth is now handled by the `#vt=` fragment in the iframe\n // src. The agent exchanges it for a scoped cookie on first load. No\n // postMessage handoff needed.\n\n const handleIframeLoad = useCallback((tabId: string) => {\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'connected' }));\n }, []);\n\n const handleIframeError = useCallback((tabId: string) => {\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'error' }));\n }, []);\n\n const handleReload = useCallback((tabId: string) => {\n const iframe = iframeRefs.current[tabId];\n if (iframe) {\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'connecting' }));\n const src = iframe.src;\n iframe.src = 'about:blank';\n requestAnimationFrame(() => {\n iframe.src = src;\n });\n }\n }, []);\n\n const handleOpenExternal = useCallback(\n (tabId: string) => {\n const tab = tabs.find((t) => t.id === tabId);\n if (!tab?.uiUrl) return;\n window.open(tab.uiUrl, '_blank');\n },\n [tabs]\n );\n\n // ── Close dropdown on outside click ───────────────────────────────\n\n useEffect(() => {\n if (!dropdownOpen) return;\n const handler = (e: MouseEvent) => {\n if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {\n setDropdownOpen(null);\n }\n };\n document.addEventListener('mousedown', handler);\n return () => document.removeEventListener('mousedown', handler);\n }, [dropdownOpen]);\n\n // ── Drag-to-resize ────────────────────────────────────────────────\n\n const handleResizeStart = useCallback(\n (e: React.MouseEvent) => {\n if (maximized) return;\n e.preventDefault();\n resizingRef.current = true;\n startYRef.current = e.clientY;\n startHeightRef.current = panelRef.current?.offsetHeight || 500;\n document.body.style.cursor = 'ns-resize';\n document.body.style.userSelect = 'none';\n },\n [maximized]\n );\n\n useEffect(() => {\n const handleMouseMove = (e: MouseEvent) => {\n if (!resizingRef.current) return;\n const delta = startYRef.current - e.clientY;\n const newHeight = Math.max(\n 250,\n Math.min(startHeightRef.current + delta, window.innerHeight - 150)\n );\n setResizeHeight(newHeight);\n };\n const handleMouseUp = () => {\n if (resizingRef.current) {\n resizingRef.current = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n }\n };\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n }, []);\n\n // ── Render helpers ────────────────────────────────────────────────\n\n const renderStatusDot = (tabId: string) => {\n const status = connectionStatus[tabId];\n if (status === 'connected')\n return <Wifi className=\"size-3 text-status-success-text shrink-0\" />;\n if (status === 'error') return <WifiOff className=\"size-3 text-status-error-text shrink-0\" />;\n if (status === 'connecting')\n return <Loader2 className=\"size-3 animate-spin text-status-warning-text shrink-0\" />;\n return <Monitor className=\"size-3 text-text-muted shrink-0\" />;\n };\n\n const panelHeight = maximized ? 'h-[calc(100vh-200px)]' : resizeHeight ? undefined : 'h-[500px]';\n const panelStyle = !maximized && resizeHeight ? { height: `${resizeHeight}px` } : undefined;\n\n return (\n <div\n ref={panelRef}\n className=\"border border-border-default rounded-lg overflow-hidden bg-bg-sunken\"\n >\n {/* Drag handle */}\n <div\n onMouseDown={handleResizeStart}\n className=\"h-1 cursor-ns-resize hover:bg-action-primary-bg/30 transition-colors flex items-center justify-center group\"\n title=\"Drag to resize\"\n >\n <GripVertical className=\"w-4 h-3 text-text-muted opacity-0 group-hover:opacity-100 transition-opacity rotate-90\" />\n </div>\n\n {/* ── Tab Bar ────────────────────────────────────────────────── */}\n <div className=\"flex items-center bg-bg-secondary border-b border-border-default overflow-x-auto\">\n {tabs.map((tab) => {\n const isActive = tab.id === activeTab?.id;\n return (\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => setActiveTabId(tab.id))();\n }\n }}\n key={tab.id}\n className={`group flex items-center gap-1.5 pl-3 pr-1.5 py-1.5 text-xs border-r border-border-subtle cursor-pointer transition-colors min-w-0 max-w-[200px] shrink-0 ${\n isActive\n ? 'bg-bg-surface text-text-primary border-b-2 border-b-action-primary-bg'\n : 'text-text-secondary hover:text-text-primary hover:bg-bg-surface/50'\n }`}\n onClick={() => setActiveTabId(tab.id)}\n >\n {renderStatusDot(tab.id)}\n <span className=\"truncate\">{tab.name}</span>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n handleCloseTab(tab.id);\n }}\n className=\"ml-auto opacity-0 group-hover:opacity-100 hover:text-status-error-text transition-opacity shrink-0 p-0.5\"\n title=\"Close tab\"\n >\n <X className=\"size-3\" />\n </button>\n </div>\n );\n })}\n\n {/* Add tab button */}\n <button\n type=\"button\"\n onClick={handleAddTab}\n className=\"flex items-center justify-center size-8 shrink-0 text-text-secondary hover:text-text-primary hover:bg-bg-surface/50 transition-colors\"\n title=\"New tab\"\n >\n <Plus className=\"size-3.5\" />\n </button>\n\n {/* Spacer */}\n <div className=\"flex-1 min-w-0\" />\n\n {/* Global panel actions */}\n <div className=\"flex items-center gap-0.5 px-2 shrink-0\">\n {activeTab?.uiUrl && (\n <>\n <button\n type=\"button\"\n onClick={() => activeTab && handleReload(activeTab.id)}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title=\"Reload\"\n >\n <RotateCcw className=\"size-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => activeTab && handleOpenExternal(activeTab.id)}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title=\"Open in new browser tab\"\n >\n <ExternalLink className=\"size-3.5\" />\n </button>\n </>\n )}\n <button\n type=\"button\"\n onClick={() => {\n setMaximized((prev) => !prev);\n setResizeHeight(null);\n }}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title={maximized ? 'Restore size' : 'Maximize'}\n >\n {maximized ? <Minimize2 className=\"size-3.5\" /> : <Maximize2 className=\"size-3.5\" />}\n </button>\n </div>\n </div>\n\n {/* ── Address Bar (Plugin Selector Dropdown) ─────────────────── */}\n {activeTab && (\n <div className=\"flex items-center gap-2 px-3 py-1.5 bg-bg-surface border-b border-border-default\">\n <Monitor className=\"size-4 text-text-muted shrink-0\" />\n <div className=\"relative flex-1 min-w-0\" ref={dropdownRef}>\n <button\n type=\"button\"\n onClick={() => setDropdownOpen(dropdownOpen === activeTab.id ? null : activeTab.id)}\n className=\"w-full flex items-center justify-between gap-2 px-3 py-1.5 text-sm bg-bg-sunken border border-border-default rounded-md text-left hover:border-action-primary-bg/50 focus:outline-none focus:ring-1 focus:ring-action-primary-bg transition-colors\"\n >\n <span\n className={`truncate ${activeTab.pluginName ? 'text-text-primary' : 'text-text-muted'}`}\n >\n {activeTab.pluginName\n ? `${activeTab.name} — ${activeTab.pluginName}`\n : 'Select a plugin to load...'}\n </span>\n <ChevronDown\n className={`size-3.5 text-text-muted shrink-0 transition-transform ${dropdownOpen === activeTab.id ? 'rotate-180' : ''}`}\n />\n </button>\n\n {/* Dropdown list */}\n {dropdownOpen === activeTab.id && (\n <div className=\"absolute z-50 top-full left-0 right-0 mt-1 bg-bg-surface border border-border-default rounded-md shadow-lg overflow-hidden max-h-64 overflow-y-auto\">\n {availablePlugins.length > 0 ? (\n availablePlugins.map((opt) => {\n const isSelected = activeTab.pluginName === opt.pluginName;\n return (\n <button\n type=\"button\"\n key={opt.pluginName}\n onClick={() => handleSelectPlugin(activeTab.id, opt.pluginName)}\n className={`w-full text-left px-3 py-2.5 hover:bg-bg-sunken transition-colors border-b border-border-subtle last:border-b-0 ${\n isSelected ? 'bg-action-primary-bg/10' : ''\n }`}\n >\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">\n {opt.displayName}\n </span>\n {opt.version && (\n <span className=\"text-xs text-text-muted\">\n {opt.version === 'latest' ? opt.version : `v${opt.version}`}\n </span>\n )}\n </div>\n {opt.description && (\n <p className=\"text-xs text-text-secondary mt-0.5\">{opt.description}</p>\n )}\n <p className=\"text-xs text-text-muted font-mono mt-0.5\">{opt.pluginName}</p>\n </button>\n );\n })\n ) : (\n <div className=\"px-3 py-4 text-center text-sm text-text-muted\">\n No UI plugins available\n </div>\n )}\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* ── Content Area ───────────────────────────────────────────── */}\n <div\n className={`${panelHeight || ''} bg-bg-inverse text-text-inverse relative`}\n style={panelStyle}\n >\n {tabs.map((tab) => (\n <div\n key={tab.id}\n className={`absolute inset-0 ${tab.id === activeTab?.id ? 'block' : 'hidden'}`}\n >\n {!tab.pluginName ? (\n // Empty state — no plugin selected\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center max-w-sm\">\n <Monitor className=\"size-10 text-text-secondary mx-auto mb-3\" />\n <p className=\"text-text-tertiary text-sm font-medium\">No plugin selected</p>\n <p className=\"text-text-secondary text-xs mt-1\">\n Use the dropdown above to pick a plugin UI to load in this tab.\n </p>\n <button\n type=\"button\"\n onClick={() => setDropdownOpen(tab.id)}\n className=\"mt-4 px-4 py-2 bg-bg-sunken text-text-primary text-sm rounded-md hover:bg-bg-muted transition-colors inline-flex items-center gap-2\"\n >\n <ChevronDown className=\"size-3.5\" />\n Select Plugin\n </button>\n </div>\n </div>\n ) : !tab.uiUrl ? (\n // No tunnel\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center\">\n <AlertCircle className=\"size-8 text-text-secondary mx-auto mb-3\" />\n <p className=\"text-text-tertiary text-sm\">Agent tunnel not active</p>\n <p className=\"text-text-secondary text-xs mt-1\">\n Cannot load plugin UI without an active tunnel connection.\n </p>\n </div>\n </div>\n ) : (\n // Iframe\n <iframe\n ref={(el) => {\n iframeRefs.current[tab.id] = el;\n }}\n src={tab.uiUrl}\n title={`Plugin UI — ${tab.name}`}\n className=\"w-full h-full border-0\"\n allow=\"clipboard-read; clipboard-write\"\n onLoad={() => handleIframeLoad(tab.id)}\n onError={() => handleIframeError(tab.id)}\n />\n )}\n </div>\n ))}\n\n {/* Connection status overlay */}\n {activeTab && connectionStatus[activeTab.id] === 'connecting' && activeTab.uiUrl && (\n <div className=\"absolute bottom-3 right-3 flex items-center gap-2 px-3 py-1.5 bg-bg-overlay backdrop-blur-sm rounded-md text-xs text-status-warning-text pointer-events-none\">\n <Loader2 className=\"size-3 animate-spin\" />\n Connecting...\n </div>\n )}\n {activeTab && connectionStatus[activeTab.id] === 'error' && activeTab.uiUrl && (\n <div className=\"absolute bottom-3 right-3 flex items-center gap-2 px-3 py-1.5 bg-bg-overlay backdrop-blur-sm rounded-md text-xs text-status-error-text\">\n <WifiOff className=\"size-3\" />\n Disconnected\n <button\n type=\"button\"\n onClick={() => handleReload(activeTab.id)}\n className=\"underline hover:text-status-error-text/80 transition-colors\"\n >\n Reconnect\n </button>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;AA8DA,IAAI,IAAe;AACnB,SAAS,IAAoB;AAC3B,QAAO,UAAU,EAAE;;AAQrB,SAAgB,EAAc,EAC5B,qBACA,cACA,YACA,cACqB;CACrB,IAAM,CAAC,KAAuB,GAAgC,EACxD,EAAE,UAAM,GAAS,EAKjB,CAAC,GAAM,KAAW,QAEf,CAAC;EAAE,IAAI,GAAW;EAAE,YAAY;EAAM,MAAM;EAAW,OAAO;EAAM,CAAC,CAC5E,EACI,CAAC,GAAa,KAAkB,EAAiB,EAAK,GAAG,GAAG,EAC5D,CAAC,GAAW,KAAgB,EAAS,GAAM,EAG3C,CAAC,GAAkB,KAAuB,EAE9C,EAAE,CAAC,EAGC,CAAC,GAAc,KAAmB,EAAwB,KAAK,EAC/D,IAAc,EAAuB,KAAK,EAG1C,CAAC,GAAc,KAAmB,EAAwB,KAAK,EAC/D,IAAc,EAAO,GAAM,EAC3B,IAAY,EAAO,EAAE,EACrB,IAAiB,EAAO,EAAE,EAC1B,IAAW,EAAuB,KAAK,EAGvC,IAAa,EAAiD,EAAE,CAAC,EAEjE,IAAY,EAAK,MAAM,MAAM,EAAE,OAAO,EAAY,IAAI,EAAK,MAAM,MAMjE,IAAa,EACjB,OAAO,GAAoB,MAA+C;AACxE,MAAI,CAAC,KAAa,CAAC,EAAS,QAAO;EACnC,IAAM,IAAO,EAAU,QAAQ,OAAO,GAAG,EAGnC,IAAS,IACX,EAAU,WAAW,OAAO,GAC1B,IACA,GAAG,IAAO,MACZ,GAAG,EAAK,MAAM,KAEZ,IAAW,IAAI,IAAI,EAAO,CAAC,UAAU;AAE3C,MAAI;GAcF,IAAM,KAbM,MAAM,EAAoB,EACpC,WAAW;IACT;IACA,SAAS,KAAW;IACpB,OAAO,EAAqB;IAK5B,YAAY;IACZ,YAAY;IACb,EACF,CAAC,EACgB,MAAM,qBAAqB;AAC7C,OAAI,CAAC,EAAO,QAAO;GAMnB,IAAM,IAAW,IAAI,IAAI,EAAS;AAOlC,UALE,MAAe,iBAAiB,EAAS,SAAS,WAAW,eAAe,IAE5E,EAAS,aAAa,IAAI,MAAM,EAAM,EAC/B,EAAS,UAAU,IAErB,GAAG,EAAS,MAAM,mBAAmB,EAAM;UAC5C;AACN,UAAO;;IAGX;EAAC;EAAW;EAAS;EAAS;EAAoB,CACnD,EAGK,IAAiB,GACpB,MACa,EAAiB,MAAM,MAAM,EAAE,eAAe,EAAW,EAE9D,eACL,EACG,QAAQ,QAAQ,GAAG,CACnB,QAAQ,MAAM,IAAI,CAClB,QAAQ,UAAU,MAAM,EAAE,aAAa,CAAC,EAG/C,CAAC,EAAiB,CACnB,EAIK,IAAe,QAAkB;EACrC,IAAM,IAAsB;GAC1B,IAAI,GAAW;GACf,YAAY;GACZ,MAAM;GACN,OAAO;GACR;AAGD,EAFA,GAAS,MAAS,CAAC,GAAG,GAAM,EAAO,CAAC,EACpC,EAAe,EAAO,GAAG,EACzB,EAAgB,EAAO,GAAG;IACzB,EAAE,CAAC,EAEA,IAAiB,GACpB,MAAkB;AAuBjB,EAtBA,GAAS,MAAS;GAChB,IAAM,IAAO,EAAK,QAAQ,MAAM,EAAE,OAAO,EAAM;AAE/C,OAAI,EAAK,WAAW,GAAG;IACrB,IAAM,IAAqB;KACzB,IAAI,GAAW;KACf,YAAY;KACZ,MAAM;KACN,OAAO;KACR;AAED,IADA,EAAK,KAAK,EAAM,EAChB,EAAe,EAAM,GAAG;cACf,MAAgB,GAAO;IAEhC,IAAM,IAAY,EAAK,WAAW,MAAM,EAAE,OAAO,EAAM,EACjD,IAAS,KAAK,IAAI,GAAG,IAAY,EAAE;AACzC,MAAe,EAAK,KAAK,IAAI,GAAQ,EAAK,SAAS,EAAE,EAAE,GAAG;;AAE5D,UAAO;IACP,EAEF,OAAO,EAAW,QAAQ,IAC1B,GAAqB,MAAS;GAC5B,IAAM,IAAO,EAAE,GAAG,GAAM;AAExB,UADA,OAAO,EAAK,IACL;IACP;IAEJ,CAAC,EAAY,CACd,EAEK,IAAqB,EACzB,OAAO,GAAe,MAAuB;EAC3C,IAAM,IAAM,EAAiB,MAAM,MAAM,EAAE,eAAe,EAAW,EAC/D,IAAc,EAAe,EAAW;AAE9C,EADA,GAAqB,OAAU;GAAE,GAAG;IAAO,IAAQ;GAAc,EAAE,EACnE,EAAgB,KAAK;EACrB,IAAM,IAAQ,MAAM,EAAW,GAAY,GAAK,UAAU;AAC1D,KAAS,MACP,EAAK,KAAK,MAAO,EAAE,OAAO,IAAQ;GAAE,GAAG;GAAG;GAAY,MAAM;GAAa;GAAO,GAAG,EAAG,CACvF;IAEH;EAAC;EAAkB;EAAY;EAAe,CAC/C,EAOK,IAAmB,GAAa,MAAkB;AACtD,KAAqB,OAAU;GAAE,GAAG;IAAO,IAAQ;GAAa,EAAE;IACjE,EAAE,CAAC,EAEA,KAAoB,GAAa,MAAkB;AACvD,KAAqB,OAAU;GAAE,GAAG;IAAO,IAAQ;GAAS,EAAE;IAC7D,EAAE,CAAC,EAEA,IAAe,GAAa,MAAkB;EAClD,IAAM,IAAS,EAAW,QAAQ;AAClC,MAAI,GAAQ;AACV,MAAqB,OAAU;IAAE,GAAG;KAAO,IAAQ;IAAc,EAAE;GACnE,IAAM,IAAM,EAAO;AAEnB,GADA,EAAO,MAAM,eACb,4BAA4B;AAC1B,MAAO,MAAM;KACb;;IAEH,EAAE,CAAC,EAEA,KAAqB,GACxB,MAAkB;EACjB,IAAM,IAAM,EAAK,MAAM,MAAM,EAAE,OAAO,EAAM;AACvC,KAAK,SACV,OAAO,KAAK,EAAI,OAAO,SAAS;IAElC,CAAC,EAAK,CACP;AAID,SAAgB;AACd,MAAI,CAAC,EAAc;EACnB,IAAM,KAAW,MAAkB;AACjC,GAAI,EAAY,WAAW,CAAC,EAAY,QAAQ,SAAS,EAAE,OAAe,IACxE,EAAgB,KAAK;;AAIzB,SADA,SAAS,iBAAiB,aAAa,EAAQ,QAClC,SAAS,oBAAoB,aAAa,EAAQ;IAC9D,CAAC,EAAa,CAAC;CAIlB,IAAM,KAAoB,GACvB,MAAwB;AACnB,QACJ,EAAE,gBAAgB,EAClB,EAAY,UAAU,IACtB,EAAU,UAAU,EAAE,SACtB,EAAe,UAAU,EAAS,SAAS,gBAAgB,KAC3D,SAAS,KAAK,MAAM,SAAS,aAC7B,SAAS,KAAK,MAAM,aAAa;IAEnC,CAAC,EAAU,CACZ;AAED,SAAgB;EACd,IAAM,KAAmB,MAAkB;AACzC,OAAI,CAAC,EAAY,QAAS;GAC1B,IAAM,IAAQ,EAAU,UAAU,EAAE;AAKpC,KAJkB,KAAK,IACrB,KACA,KAAK,IAAI,EAAe,UAAU,GAAO,OAAO,cAAc,IAAI,CACnE,CACyB;KAEtB,UAAsB;AAC1B,GAAI,EAAY,YACd,EAAY,UAAU,IACtB,SAAS,KAAK,MAAM,SAAS,IAC7B,SAAS,KAAK,MAAM,aAAa;;AAKrC,SAFA,SAAS,iBAAiB,aAAa,EAAgB,EACvD,SAAS,iBAAiB,WAAW,EAAc,QACtC;AAEX,GADA,SAAS,oBAAoB,aAAa,EAAgB,EAC1D,SAAS,oBAAoB,WAAW,EAAc;;IAEvD,EAAE,CAAC;CAIN,IAAM,MAAmB,MAAkB;EACzC,IAAM,IAAS,EAAiB;AAMhC,SALI,MAAW,cACN,kBAAC,GAAD,EAAM,WAAU,4CAA6C,CAAA,GAClE,MAAW,UAAgB,kBAAC,GAAD,EAAS,WAAU,0CAA2C,CAAA,GACzF,MAAW,eACN,kBAAC,GAAD,EAAS,WAAU,yDAA0D,CAAA,GAC/E,kBAAC,GAAD,EAAS,WAAU,mCAAoC,CAAA;IAG1D,KAAc,IAAY,0BAA0B,IAAe,KAAA,IAAY,aAC/E,KAAa,CAAC,KAAa,IAAe,EAAE,QAAQ,GAAG,EAAa,KAAK,GAAG,KAAA;AAElF,QACE,kBAAC,OAAD;EACE,KAAK;EACL,WAAU;YAFZ;GAKE,kBAAC,OAAD;IACE,aAAa;IACb,WAAU;IACV,OAAM;cAEN,kBAAC,GAAD,EAAc,WAAU,0FAA2F,CAAA;IAC/G,CAAA;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,EAAK,KAAK,MAGP,kBAAC,OAAD;MACE,MAAK;MACL,UAAU;MACV,YAAY,MAAM;AAChB,QAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,gBAAgB,EACX,EAAe,EAAI,GAAG;;MAIjC,WAAW,4JAZE,EAAI,OAAO,GAAW,KAc7B,0EACA;MAEN,eAAe,EAAe,EAAI,GAAG;gBAfvC;OAiBG,GAAgB,EAAI,GAAG;OACxB,kBAAC,QAAD;QAAM,WAAU;kBAAY,EAAI;QAAY,CAAA;OAC5C,kBAAC,UAAD;QACE,MAAK;QACL,UAAU,MAAM;AAEd,SADA,EAAE,iBAAiB,EACnB,EAAe,EAAI,GAAG;;QAExB,WAAU;QACV,OAAM;kBAEN,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;QACjB,CAAA;OACL;QArBC,EAAI,GAqBL,CAER;KAGF,kBAAC,UAAD;MACE,MAAK;MACL,SAAS;MACT,WAAU;MACV,OAAM;gBAEN,kBAAC,GAAD,EAAM,WAAU,YAAa,CAAA;MACtB,CAAA;KAGT,kBAAC,OAAD,EAAK,WAAU,kBAAmB,CAAA;KAGlC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACG,GAAW,SACV,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,KAAa,EAAa,EAAU,GAAG;OACtD,WAAU;OACV,OAAM;iBAEN,kBAAC,GAAD,EAAW,WAAU,YAAa,CAAA;OAC3B,CAAA,EACT,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,KAAa,GAAmB,EAAU,GAAG;OAC5D,WAAU;OACV,OAAM;iBAEN,kBAAC,GAAD,EAAc,WAAU,YAAa,CAAA;OAC9B,CAAA,CACR,EAAA,CAAA,EAEL,kBAAC,UAAD;OACE,MAAK;OACL,eAAe;AAEb,QADA,GAAc,MAAS,CAAC,EAAK,EAC7B,EAAgB,KAAK;;OAEvB,WAAU;OACV,OAAO,IAAY,iBAAiB;iBAEvB,EAAZ,IAAa,IAAqC,GAAtC,EAAW,WAAU,YAAa,CAAqC;OAC7E,CAAA,CACL;;KACF;;GAGL,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAS,WAAU,mCAAoC,CAAA,EACvD,kBAAC,OAAD;KAAK,WAAU;KAA0B,KAAK;eAA9C,CACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAgB,MAAiB,EAAU,KAAK,OAAO,EAAU,GAAG;MACnF,WAAU;gBAHZ,CAKE,kBAAC,QAAD;OACE,WAAW,YAAY,EAAU,aAAa,sBAAsB;iBAEnE,EAAU,aACP,GAAG,EAAU,KAAK,KAAK,EAAU,eACjC;OACC,CAAA,EACP,kBAAC,GAAD,EACE,WAAW,0DAA0D,MAAiB,EAAU,KAAK,eAAe,MACpH,CAAA,CACK;SAGR,MAAiB,EAAU,MAC1B,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAiB,SAAS,IACzB,EAAiB,KAAK,MAGlB,kBAAC,UAAD;OACE,MAAK;OAEL,eAAe,EAAmB,EAAU,IAAI,EAAI,WAAW;OAC/D,WAAW,mHANI,EAAU,eAAe,EAAI,aAO7B,4BAA4B;iBAL7C;QAQE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAI;UACA,CAAA,EACN,EAAI,WACH,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAI,YAAY,WAAW,EAAI,UAAU,IAAI,EAAI;UAC7C,CAAA,CAEL;;QACL,EAAI,eACH,kBAAC,KAAD;SAAG,WAAU;mBAAsC,EAAI;SAAgB,CAAA;QAEzE,kBAAC,KAAD;SAAG,WAAU;mBAA4C,EAAI;SAAe,CAAA;QACrE;SApBF,EAAI,WAoBF,CAEX,GAEF,kBAAC,OAAD;OAAK,WAAU;iBAAgD;OAEzD,CAAA;MAEJ,CAAA,CAEJ;OACF;;GAIR,kBAAC,OAAD;IACE,WAAW,GAAG,MAAe,GAAG;IAChC,OAAO;cAFT;KAIG,EAAK,KAAK,MACT,kBAAC,OAAD;MAEE,WAAW,oBAAoB,EAAI,OAAO,GAAW,KAAK,UAAU;gBAElE,EAAI,aAmBD,EAAI,QAaP,kBAAC,UAAD;OACE,MAAM,MAAO;AACX,UAAW,QAAQ,EAAI,MAAM;;OAE/B,KAAK,EAAI;OACT,OAAO,eAAe,EAAI;OAC1B,WAAU;OACV,OAAM;OACN,cAAc,EAAiB,EAAI,GAAG;OACtC,eAAe,GAAkB,EAAI,GAAG;OACxC,CAAA,GArBF,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,IAAD,EAAa,WAAU,2CAA4C,CAAA;SACnE,kBAAC,KAAD;UAAG,WAAU;oBAA6B;UAA2B,CAAA;SACrE,kBAAC,KAAD;UAAG,WAAU;oBAAmC;UAE5C,CAAA;SACA;;OACF,CAAA,GA3BN,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,GAAD,EAAS,WAAU,4CAA6C,CAAA;SAChE,kBAAC,KAAD;UAAG,WAAU;oBAAyC;UAAsB,CAAA;SAC5E,kBAAC,KAAD;UAAG,WAAU;oBAAmC;UAE5C,CAAA;SACJ,kBAAC,UAAD;UACE,MAAK;UACL,eAAe,EAAgB,EAAI,GAAG;UACtC,WAAU;oBAHZ,CAKE,kBAAC,GAAD,EAAa,WAAU,YAAa,CAAA,EAAA,gBAE7B;;SACL;;OACF,CAAA;MA0BJ,EA/CC,EAAI,GA+CL,CACN;KAGD,KAAa,EAAiB,EAAU,QAAQ,gBAAgB,EAAU,SACzE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,EAAA,gBAEvC;;KAEP,KAAa,EAAiB,EAAU,QAAQ,WAAW,EAAU,SACpE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;;OAE9B,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAa,EAAU,GAAG;QACzC,WAAU;kBACX;QAEQ,CAAA;OACL;;KAEJ;;GACF"}
|