@burdenoff/microfe-vibecontrols 2026.608.1 → 2026.608.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/agents/OpenInBrowserEditorButton.js +22 -17
- package/dist/components/agents/OpenInBrowserEditorButton.js.map +1 -1
- package/dist/components/sessions/TerminalPanel.js +536 -413
- package/dist/components/sessions/TerminalPanel.js.map +1 -1
- package/dist/components/sessions/sessionShortcuts.js +30 -3
- package/dist/components/sessions/sessionShortcuts.js.map +1 -1
- package/dist/components/shared/SessionNotificationBanner.js +28 -21
- package/dist/components/shared/SessionNotificationBanner.js.map +1 -1
- package/dist/pages/SessionsPage.js +1126 -908
- package/dist/pages/SessionsPage.js.map +1 -1
- package/dist/store/vibeControlsStore.js +18 -0
- package/dist/store/vibeControlsStore.js.map +1 -1
- package/dist/translations/en.js +5 -0
- package/dist/translations/en.js.map +1 -1
- package/dist/utils/randomSessionName.js +55 -0
- package/dist/utils/randomSessionName.js.map +1 -0
- package/package.json +2 -2
|
@@ -35,22 +35,24 @@ function h({ agentId: h, tunnelUrl: g, profile: _, workspacePath: v, mode: y = "
|
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
E(null);
|
|
38
|
+
let e = null;
|
|
39
|
+
y !== "iframe" && (e = window.open("about:blank", "_blank"));
|
|
38
40
|
try {
|
|
39
41
|
w("checking");
|
|
40
|
-
let
|
|
41
|
-
for (let
|
|
42
|
-
let { effective:
|
|
43
|
-
if (!a &&
|
|
44
|
-
|
|
42
|
+
let t = _ === "default" ? ["default"] : [_, "default"], n = null, r = null;
|
|
43
|
+
for (let e of t) {
|
|
44
|
+
let { effective: t, status: i, missing: a } = await j(e);
|
|
45
|
+
if (!a && t) {
|
|
46
|
+
n = t, r = i;
|
|
45
47
|
break;
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
|
-
if (!
|
|
49
|
-
if (!
|
|
50
|
+
if (!n || !r || !r.installed) throw Error(S("vibecontrols.agents.editor.notInstalled", "Code-server isn't installed on this agent. Install it from the Plugins tab to enable Open Editor."));
|
|
51
|
+
if (!r.running) {
|
|
50
52
|
w("starting");
|
|
51
53
|
let e = (await O({ variables: {
|
|
52
54
|
agentId: h,
|
|
53
|
-
profile:
|
|
55
|
+
profile: n,
|
|
54
56
|
name: m,
|
|
55
57
|
...v ? { path: v } : {}
|
|
56
58
|
} })).data?.agentCodeServerStart;
|
|
@@ -59,12 +61,12 @@ function h({ agentId: h, tunnelUrl: g, profile: _, workspacePath: v, mode: y = "
|
|
|
59
61
|
} else if (v) {
|
|
60
62
|
w("starting"), await k({ variables: {
|
|
61
63
|
agentId: h,
|
|
62
|
-
profile:
|
|
64
|
+
profile: n,
|
|
63
65
|
name: m
|
|
64
66
|
} });
|
|
65
67
|
let e = (await O({ variables: {
|
|
66
68
|
agentId: h,
|
|
67
|
-
profile:
|
|
69
|
+
profile: n,
|
|
68
70
|
name: m,
|
|
69
71
|
path: v
|
|
70
72
|
} })).data?.agentCodeServerStart;
|
|
@@ -72,18 +74,21 @@ function h({ agentId: h, tunnelUrl: g, profile: _, workspacePath: v, mode: y = "
|
|
|
72
74
|
await new Promise((e) => setTimeout(e, 1e3));
|
|
73
75
|
}
|
|
74
76
|
w("ready");
|
|
75
|
-
let
|
|
77
|
+
let i = (await A({ variables: {
|
|
76
78
|
agentId: h,
|
|
77
|
-
profile:
|
|
79
|
+
profile: n,
|
|
78
80
|
scope: a.IframePlugin,
|
|
79
81
|
resourceId: `code-server:${m}`,
|
|
80
82
|
ttlSeconds: 60
|
|
81
83
|
} })).data?.mintAgentCapability?.token;
|
|
82
|
-
if (!
|
|
83
|
-
let
|
|
84
|
-
y === "iframe" && b
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
if (!i) throw Error("Failed to mint editor capability");
|
|
85
|
+
let o = `${g.replace(/\/$/, "")}/code-server/?vt=${encodeURIComponent(i)}`;
|
|
86
|
+
if (y === "iframe" && b) b(o);
|
|
87
|
+
else if (e && !e.closed) e.location.replace(o);
|
|
88
|
+
else if (!window.open(o, "_blank")) throw Error(S("vibecontrols.agents.editor.popupBlocked", "Your browser blocked the editor tab. Allow pop-ups for this site, then click Open Editor again."));
|
|
89
|
+
setTimeout(() => w("idle"), 2e3);
|
|
90
|
+
} catch (t) {
|
|
91
|
+
e && !e.closed && e.close(), E(t instanceof Error ? t.message : "Failed to open editor"), w("error");
|
|
87
92
|
}
|
|
88
93
|
}, [
|
|
89
94
|
h,
|
|
@@ -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 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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"}
|
|
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 // Pre-open the editor tab *synchronously*, inside the click gesture, so the\n // browser's popup blocker lets it through. The control-plane calls below\n // (probe → start → mint) are async, and a `window.open` issued after those\n // awaits has lost the user-activation and gets blocked — which is the\n // \"popup blocker blocks the editor\" report. We open `about:blank` now and\n // navigate it once the real URL is ready; on failure we close it again.\n let editorWindow: Window | null = null;\n if (mode !== 'iframe') {\n editorWindow = window.open('about:blank', '_blank');\n }\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 if (editorWindow && !editorWindow.closed) {\n // Navigate the tab we pre-opened inside the click gesture.\n editorWindow.location.replace(editorUrl);\n } else {\n // The pre-opened tab was blocked (or closed by the user). Try once\n // more — this still happens within the original gesture's task — and\n // surface a hint if the blocker rejects it again.\n const opened = window.open(editorUrl, '_blank');\n if (!opened) {\n throw new Error(\n tr(\n 'vibecontrols.agents.editor.popupBlocked',\n 'Your browser blocked the editor tab. Allow pop-ups for this site, then click Open Editor again.'\n )\n );\n }\n }\n\n // Reset after a moment\n setTimeout(() => setState('idle'), 2000);\n } catch (err) {\n // Close the placeholder tab so a blank \"about:blank\" window isn't left\n // orphaned when the editor fails to come up.\n if (editorWindow && !editorWindow.closed) {\n editorWindow.close();\n }\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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"}
|