@burdenoff/microfe-vibecontrols 2026.522.7 → 2026.522.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/agents/OpenInBrowserEditorButton.js +65 -66
- package/dist/components/agents/OpenInBrowserEditorButton.js.map +1 -1
- package/dist/components/ai/AIProviderCredentialsCard.js.map +1 -1
- package/dist/components/security/SecurityProviderDrawer.js +106 -31
- package/dist/components/security/SecurityProviderDrawer.js.map +1 -1
- package/dist/components/vibe/VibeAgentSwitcher.js +237 -0
- package/dist/components/vibe/VibeAgentSwitcher.js.map +1 -0
- package/dist/components/vibes/VibeCard.js +2 -2
- package/dist/components/vibes/VibeCard.js.map +1 -1
- package/dist/components/vibes/VibeList.js +12 -30
- package/dist/components/vibes/VibeList.js.map +1 -1
- package/dist/pages/AgentDetailsPage.js +172 -123
- package/dist/pages/AgentDetailsPage.js.map +1 -1
- package/dist/pages/VibeDetailsPage.js +383 -397
- package/dist/pages/VibeDetailsPage.js.map +1 -1
- package/dist/pages/VibesPage.js +155 -190
- package/dist/pages/VibesPage.js.map +1 -1
- package/dist/services/aiApi.js +1 -1
- package/dist/services/aiApi.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback as t, useState as n } from "react";
|
|
3
|
+
import { AlertCircle as r, Check as i, Code as a, Loader2 as o } from "lucide-react";
|
|
4
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
6
5
|
//#region src/components/agents/OpenInBrowserEditorButton.tsx
|
|
7
|
-
async function
|
|
6
|
+
async function l(e, t, n, r) {
|
|
8
7
|
let i = e.replace(/\/$/, "");
|
|
9
8
|
return fetch(`${i}${t}`, {
|
|
10
9
|
...r,
|
|
@@ -15,46 +14,46 @@ async function u(e, t, n, r) {
|
|
|
15
14
|
}
|
|
16
15
|
});
|
|
17
16
|
}
|
|
18
|
-
var
|
|
19
|
-
async function
|
|
20
|
-
let
|
|
21
|
-
for (; Date.now() <
|
|
22
|
-
let
|
|
23
|
-
if (!
|
|
24
|
-
let
|
|
25
|
-
if (
|
|
26
|
-
if (
|
|
27
|
-
await new Promise((e) => setTimeout(e,
|
|
17
|
+
var u = 2e3, d = 12e4;
|
|
18
|
+
async function f(e, t, n, r, i) {
|
|
19
|
+
let a = Date.now() + d;
|
|
20
|
+
for (; Date.now() < a;) {
|
|
21
|
+
let n = await l(e, "/api/code-server/status", t);
|
|
22
|
+
if (!n.ok) throw Error(`Status check failed: ${n.status}`);
|
|
23
|
+
let a = await n.json(), o = i(a);
|
|
24
|
+
if (o) throw Error(o);
|
|
25
|
+
if (r(a)) return a;
|
|
26
|
+
await new Promise((e) => setTimeout(e, u));
|
|
28
27
|
}
|
|
29
28
|
throw Error("Operation timed out");
|
|
30
29
|
}
|
|
31
|
-
function
|
|
32
|
-
let
|
|
33
|
-
if (!
|
|
34
|
-
|
|
30
|
+
function p({ tunnelUrl: u, agentApiKey: d, profile: p, workspacePath: m, mode: h = "newtab", onIframeUrl: g, className: _ }) {
|
|
31
|
+
let v = e(), [y, b] = n("idle"), [x, S] = n(null), C = t(async () => {
|
|
32
|
+
if (!u || !d) {
|
|
33
|
+
S("Agent tunnel or API key not available"), b("error");
|
|
35
34
|
return;
|
|
36
35
|
}
|
|
37
|
-
|
|
36
|
+
S(null);
|
|
38
37
|
try {
|
|
39
|
-
|
|
40
|
-
let e = await u
|
|
38
|
+
b("checking");
|
|
39
|
+
let e = await l(u, "/api/code-server/status", d);
|
|
41
40
|
if (!e.ok) throw Error(`Status check failed: ${e.status}`);
|
|
42
|
-
let
|
|
43
|
-
if (!
|
|
44
|
-
|
|
45
|
-
let e = await u
|
|
41
|
+
let t = await e.json();
|
|
42
|
+
if (!t.installed && !t.installing) {
|
|
43
|
+
b("installing");
|
|
44
|
+
let e = await l(u, "/api/code-server/install", d, { method: "POST" });
|
|
46
45
|
if (!e.ok) throw Error(`Install failed: ${e.status}`);
|
|
47
|
-
await
|
|
48
|
-
} else
|
|
49
|
-
if (
|
|
46
|
+
await f(u, d, p, (e) => e.installed, (e) => e.error || null);
|
|
47
|
+
} else t.installing && (b("installing"), await f(u, d, p, (e) => e.installed, (e) => e.error || null));
|
|
48
|
+
if (t.running) m && t.running && (b("starting"), await l(u, "/api/code-server/restart", d, {
|
|
50
49
|
method: "POST",
|
|
51
|
-
body: JSON.stringify({ workspacePath:
|
|
50
|
+
body: JSON.stringify({ workspacePath: m })
|
|
52
51
|
}), await new Promise((e) => setTimeout(e, 1e3)));
|
|
53
52
|
else {
|
|
54
|
-
|
|
55
|
-
let e = await u
|
|
53
|
+
b("starting");
|
|
54
|
+
let e = await l(u, "/api/code-server/start", d, {
|
|
56
55
|
method: "POST",
|
|
57
|
-
body: JSON.stringify({ workspacePath:
|
|
56
|
+
body: JSON.stringify({ workspacePath: m })
|
|
58
57
|
});
|
|
59
58
|
if (!e.ok) {
|
|
60
59
|
let t = await e.json().catch(() => ({}));
|
|
@@ -62,56 +61,56 @@ function m({ tunnelUrl: d, agentApiKey: f, profile: m, workspacePath: h, mode: g
|
|
|
62
61
|
}
|
|
63
62
|
await new Promise((e) => setTimeout(e, 1e3));
|
|
64
63
|
}
|
|
65
|
-
|
|
66
|
-
let
|
|
67
|
-
|
|
64
|
+
b("ready");
|
|
65
|
+
let n = `${u.replace(/\/$/, "")}/code-server/?apiKey=${encodeURIComponent(d)}`;
|
|
66
|
+
h === "iframe" && g ? g(n) : window.open(n, "_blank"), setTimeout(() => b("idle"), 2e3);
|
|
68
67
|
} catch (e) {
|
|
69
|
-
|
|
68
|
+
S(e instanceof Error ? e.message : "Failed to open editor"), b("error");
|
|
70
69
|
}
|
|
71
70
|
}, [
|
|
71
|
+
u,
|
|
72
72
|
d,
|
|
73
|
-
|
|
73
|
+
p,
|
|
74
74
|
m,
|
|
75
75
|
h,
|
|
76
|
-
g
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
case "
|
|
81
|
-
case "
|
|
82
|
-
case "
|
|
83
|
-
case "
|
|
84
|
-
|
|
85
|
-
default: return y("vibecontrols.agents.editor.openInBrowser", "Open in Browser");
|
|
76
|
+
g
|
|
77
|
+
]), w = !u || !d || y !== "idle" && y !== "error" && y !== "ready", T = (() => {
|
|
78
|
+
switch (y) {
|
|
79
|
+
case "checking": return v("vibecontrols.agents.editor.checking", "Checking...");
|
|
80
|
+
case "installing": return v("vibecontrols.agents.editor.installing", "Installing...");
|
|
81
|
+
case "starting": return v("vibecontrols.agents.editor.starting", "Starting...");
|
|
82
|
+
case "ready": return v("vibecontrols.agents.editor.opened", "Opened");
|
|
83
|
+
case "error": return v("vibecontrols.agents.editor.retry", "Retry");
|
|
84
|
+
default: return v("vibecontrols.agents.editor.openInBrowser", "Open in Browser");
|
|
86
85
|
}
|
|
87
|
-
})(),
|
|
88
|
-
switch (
|
|
86
|
+
})(), E = (() => {
|
|
87
|
+
switch (y) {
|
|
89
88
|
case "checking":
|
|
90
89
|
case "installing":
|
|
91
|
-
case "starting": return
|
|
92
|
-
case "ready": return
|
|
93
|
-
case "error": return
|
|
94
|
-
default: return
|
|
90
|
+
case "starting": return o;
|
|
91
|
+
case "ready": return i;
|
|
92
|
+
case "error": return r;
|
|
93
|
+
default: return a;
|
|
95
94
|
}
|
|
96
|
-
})(),
|
|
97
|
-
return /* @__PURE__ */
|
|
95
|
+
})(), D = y === "checking" || y === "installing" || y === "starting";
|
|
96
|
+
return /* @__PURE__ */ c("div", {
|
|
98
97
|
className: "relative inline-flex",
|
|
99
|
-
children: [/* @__PURE__ */
|
|
100
|
-
onClick:
|
|
101
|
-
disabled:
|
|
102
|
-
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 ${
|
|
103
|
-
title:
|
|
104
|
-
children: [/* @__PURE__ */
|
|
98
|
+
children: [/* @__PURE__ */ c("button", {
|
|
99
|
+
onClick: C,
|
|
100
|
+
disabled: w,
|
|
101
|
+
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 ${y === "error" ? "bg-status-error-bg text-status-error-text hover:opacity-80" : y === "ready" ? "bg-status-success-bg text-status-success-text" : "bg-bg-sunken text-text-primary hover:bg-bg-sunken/80"} ${_ || ""}`,
|
|
102
|
+
title: x || "Open VS Code in browser via code-server",
|
|
103
|
+
children: [/* @__PURE__ */ s(E, { className: `w-3.5 h-3.5 sm:w-4 sm:h-4 ${D ? "animate-spin" : ""}` }), /* @__PURE__ */ s("span", {
|
|
105
104
|
className: "hidden sm:inline",
|
|
106
|
-
children:
|
|
105
|
+
children: T
|
|
107
106
|
})]
|
|
108
|
-
}),
|
|
107
|
+
}), y === "error" && x && /* @__PURE__ */ s("div", {
|
|
109
108
|
className: "absolute top-full left-0 mt-1 z-50 bg-bg-surface border border-border-default rounded-md shadow-lg p-2 text-xs text-status-error-text max-w-[250px]",
|
|
110
|
-
children:
|
|
109
|
+
children: x
|
|
111
110
|
})]
|
|
112
111
|
});
|
|
113
112
|
}
|
|
114
113
|
//#endregion
|
|
115
|
-
export {
|
|
114
|
+
export { p as OpenInBrowserEditorButton };
|
|
116
115
|
|
|
117
116
|
//# sourceMappingURL=OpenInBrowserEditorButton.js.map
|
|
@@ -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 { agentApiPath } from '@/utils/agentApiPath';\n\n// ── Types ──────────────────────────────────────────────────────────────\n\ntype EditorState = 'idle' | 'checking' | 'installing' | 'starting' | 'ready' | 'error';\n\nexport interface OpenInBrowserEditorButtonProps {\n /** Agent tunnel URL (e.g. \"https://xxx.trycloudflare.com\") */\n tunnelUrl: string | null;\n /** Agent API key for authentication */\n agentApiKey: 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\n// ── Helpers ────────────────────────────────────────────────────────────\n\nasync function agentFetch(\n tunnelUrl: string,\n path: string,\n apiKey: string,\n options?: RequestInit\n): Promise<Response> {\n const base = tunnelUrl.replace(/\\/$/, '');\n return fetch(`${base}${path}`, {\n ...options,\n headers: {\n 'Content-Type': 'application/json',\n 'x-agent-api-key': apiKey,\n ...options?.headers,\n },\n });\n}\n\ninterface CodeServerStatus {\n installed: boolean;\n installing: boolean;\n running: boolean;\n port?: number;\n error?: string;\n}\n\nconst POLL_INTERVAL_MS = 2000;\nconst POLL_TIMEOUT_MS = 120_000; // 2 minutes max\n\n/**\n * Poll until a condition is met on the status response.\n */\nasync function pollUntil(\n tunnelUrl: string,\n apiKey: string,\n profile: string,\n predicate: (status: CodeServerStatus) => boolean,\n errorCheck: (status: CodeServerStatus) => string | null\n): Promise<CodeServerStatus> {\n const deadline = Date.now() + POLL_TIMEOUT_MS;\n\n while (Date.now() < deadline) {\n const res = await agentFetch(tunnelUrl, agentApiPath(profile, '/code-server/status'), apiKey);\n if (!res.ok) throw new Error(`Status check failed: ${res.status}`);\n const status: CodeServerStatus = await res.json();\n\n const err = errorCheck(status);\n if (err) throw new Error(err);\n\n if (predicate(status)) return status;\n\n await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));\n }\n\n throw new Error('Operation timed out');\n}\n\n// ── Component ──────────────────────────────────────────────────────────\n\nexport function OpenInBrowserEditorButton({\n tunnelUrl,\n agentApiKey,\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 handleClick = useCallback(async () => {\n if (!tunnelUrl || !agentApiKey) {\n setErrorMsg('Agent tunnel or API key not available');\n setState('error');\n return;\n }\n\n setErrorMsg(null);\n\n try {\n // Step 1: Check status\n setState('checking');\n const res = await agentFetch(\n tunnelUrl,\n agentApiPath(profile, '/code-server/status'),\n agentApiKey\n );\n if (!res.ok) throw new Error(`Status check failed: ${res.status}`);\n const status: CodeServerStatus = await res.json();\n\n // Step 2: Install if needed\n if (!status.installed && !status.installing) {\n setState('installing');\n const installRes = await agentFetch(\n tunnelUrl,\n agentApiPath(profile, '/code-server/install'),\n agentApiKey,\n { method: 'POST' }\n );\n if (!installRes.ok) throw new Error(`Install failed: ${installRes.status}`);\n\n // Poll until installed\n await pollUntil(\n tunnelUrl,\n agentApiKey,\n profile,\n (s) => s.installed,\n (s) => s.error || null\n );\n } else if (status.installing) {\n setState('installing');\n await pollUntil(\n tunnelUrl,\n agentApiKey,\n profile,\n (s) => s.installed,\n (s) => s.error || null\n );\n }\n\n // Step 3: Start if not running\n if (!status.running) {\n setState('starting');\n const startRes = await agentFetch(\n tunnelUrl,\n agentApiPath(profile, '/code-server/start'),\n agentApiKey,\n {\n method: 'POST',\n body: JSON.stringify({ workspacePath }),\n }\n );\n if (!startRes.ok) {\n const err = await startRes.json().catch(() => ({}));\n throw new Error((err as { error?: string }).error || `Start failed: ${startRes.status}`);\n }\n\n // Brief wait for code-server to be ready\n await new Promise((r) => setTimeout(r, 1000));\n } else if (workspacePath && status.running) {\n // Restart with new workspace path if different\n setState('starting');\n await agentFetch(tunnelUrl, agentApiPath(profile, '/code-server/restart'), agentApiKey, {\n method: 'POST',\n body: JSON.stringify({ workspacePath }),\n });\n await new Promise((r) => setTimeout(r, 1000));\n }\n\n // Step 4: Open the editor\n setState('ready');\n const base = tunnelUrl.replace(/\\/$/, '');\n const editorUrl = `${base}/code-server/?apiKey=${encodeURIComponent(agentApiKey)}`;\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 }, [tunnelUrl, agentApiKey, profile, workspacePath, mode, onIframeUrl]);\n\n const isDisabled =\n !tunnelUrl || !agentApiKey || (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.openInBrowser', 'Open in Browser');\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 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={`w-3.5 h-3.5 sm:w-4 sm:h-4 ${isSpinning ? 'animate-spin' : ''}`} />\n <span className=\"hidden sm:inline\">{label}</span>\n </button>\n\n {/* Error tooltip */}\n {state === 'error' && errorMsg && (\n <div className=\"absolute top-full left-0 mt-1 z-50 bg-bg-surface border border-border-default rounded-md shadow-lg p-2 text-xs text-status-error-text max-w-[250px]\">\n {errorMsg}\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';\n\n// ── Types ──────────────────────────────────────────────────────────────\n\ntype EditorState = 'idle' | 'checking' | 'installing' | 'starting' | 'ready' | 'error';\n\nexport interface OpenInBrowserEditorButtonProps {\n /** Agent tunnel URL (e.g. \"https://xxx.trycloudflare.com\") */\n tunnelUrl: string | null;\n /** Agent API key for authentication */\n agentApiKey: 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\n// ── Helpers ────────────────────────────────────────────────────────────\n\nasync function agentFetch(\n tunnelUrl: string,\n path: string,\n apiKey: string,\n options?: RequestInit\n): Promise<Response> {\n const base = tunnelUrl.replace(/\\/$/, '');\n return fetch(`${base}${path}`, {\n ...options,\n headers: {\n 'Content-Type': 'application/json',\n 'x-agent-api-key': apiKey,\n ...options?.headers,\n },\n });\n}\n\ninterface CodeServerStatus {\n installed: boolean;\n installing: boolean;\n running: boolean;\n port?: number;\n error?: string;\n}\n\nconst POLL_INTERVAL_MS = 2000;\nconst POLL_TIMEOUT_MS = 120_000; // 2 minutes max\n\n/**\n * Poll until a condition is met on the status response.\n */\nasync function pollUntil(\n tunnelUrl: string,\n apiKey: string,\n _profile: string,\n predicate: (status: CodeServerStatus) => boolean,\n errorCheck: (status: CodeServerStatus) => string | null\n): Promise<CodeServerStatus> {\n const deadline = Date.now() + POLL_TIMEOUT_MS;\n\n while (Date.now() < deadline) {\n const res = await agentFetch(tunnelUrl, '/api/code-server/status', apiKey);\n if (!res.ok) throw new Error(`Status check failed: ${res.status}`);\n const status: CodeServerStatus = await res.json();\n\n const err = errorCheck(status);\n if (err) throw new Error(err);\n\n if (predicate(status)) return status;\n\n await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));\n }\n\n throw new Error('Operation timed out');\n}\n\n// ── Component ──────────────────────────────────────────────────────────\n\nexport function OpenInBrowserEditorButton({\n tunnelUrl,\n agentApiKey,\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 handleClick = useCallback(async () => {\n if (!tunnelUrl || !agentApiKey) {\n setErrorMsg('Agent tunnel or API key not available');\n setState('error');\n return;\n }\n\n setErrorMsg(null);\n\n try {\n // Step 1: Check status\n setState('checking');\n const res = await agentFetch(tunnelUrl, '/api/code-server/status', agentApiKey);\n if (!res.ok) throw new Error(`Status check failed: ${res.status}`);\n const status: CodeServerStatus = await res.json();\n\n // Step 2: Install if needed\n if (!status.installed && !status.installing) {\n setState('installing');\n const installRes = await agentFetch(tunnelUrl, '/api/code-server/install', agentApiKey, {\n method: 'POST',\n });\n if (!installRes.ok) throw new Error(`Install failed: ${installRes.status}`);\n\n // Poll until installed\n await pollUntil(\n tunnelUrl,\n agentApiKey,\n profile,\n (s) => s.installed,\n (s) => s.error || null\n );\n } else if (status.installing) {\n setState('installing');\n await pollUntil(\n tunnelUrl,\n agentApiKey,\n profile,\n (s) => s.installed,\n (s) => s.error || null\n );\n }\n\n // Step 3: Start if not running\n if (!status.running) {\n setState('starting');\n const startRes = await agentFetch(tunnelUrl, '/api/code-server/start', agentApiKey, {\n method: 'POST',\n body: JSON.stringify({ workspacePath }),\n });\n if (!startRes.ok) {\n const err = await startRes.json().catch(() => ({}));\n throw new Error((err as { error?: string }).error || `Start failed: ${startRes.status}`);\n }\n\n // Brief wait for code-server to be ready\n await new Promise((r) => setTimeout(r, 1000));\n } else if (workspacePath && status.running) {\n // Restart with new workspace path if different\n setState('starting');\n await agentFetch(tunnelUrl, '/api/code-server/restart', agentApiKey, {\n method: 'POST',\n body: JSON.stringify({ workspacePath }),\n });\n await new Promise((r) => setTimeout(r, 1000));\n }\n\n // Step 4: Open the editor\n setState('ready');\n const base = tunnelUrl.replace(/\\/$/, '');\n const editorUrl = `${base}/code-server/?apiKey=${encodeURIComponent(agentApiKey)}`;\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 }, [tunnelUrl, agentApiKey, profile, workspacePath, mode, onIframeUrl]);\n\n const isDisabled =\n !tunnelUrl || !agentApiKey || (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.openInBrowser', 'Open in Browser');\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 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={`w-3.5 h-3.5 sm:w-4 sm:h-4 ${isSpinning ? 'animate-spin' : ''}`} />\n <span className=\"hidden sm:inline\">{label}</span>\n </button>\n\n {/* Error tooltip */}\n {state === 'error' && errorMsg && (\n <div className=\"absolute top-full left-0 mt-1 z-50 bg-bg-surface border border-border-default rounded-md shadow-lg p-2 text-xs text-status-error-text max-w-[250px]\">\n {errorMsg}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIProviderCredentialsCard.js","names":[],"sources":["../../../src/components/ai/AIProviderCredentialsCard.tsx"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\nimport {\n ArrowLeft,\n ChevronRight,\n Eye,\n EyeOff,\n KeyRound,\n Loader2,\n Save,\n Trash2,\n} from 'lucide-react';\n\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n deleteAgentConfigValue,\n getAgentConfigValue,\n setAgentConfigValue,\n type AgentRef,\n} from '@/services/aiApi';\n\n/**\n * One provider definition — what env-var-style keys the agent stores under\n * its config bag and what to call the provider in the UI. Kept as a flat\n * static list rather than discovered from the plugin manifest because the\n * config keys are part of the upstream SDK contract (ANTHROPIC_API_KEY,\n * OPENAI_API_KEY, etc.) and don't change with each plugin release.\n */\nexport interface AIProviderCredentialDef {\n /** Stable provider id — usually matches the plugin's pluginName. */\n providerId: string;\n /** Display name shown in the card heading. */\n label: string;\n /** Short helper text under the heading. */\n description: string;\n /**\n * One row per key. The `kind` distinguishes a primary credential (for\n * which the SDK refuses to start without ONE of the listed keys) from\n * an optional URL/host override.\n */\n fields: Array<{\n /** Agent-config key name, also rendered as the input label. */\n key: string;\n /**\n * Whether the field is a secret (rendered as password with eye toggle)\n * or a plain string (URL / host overrides — visible by default).\n */\n secret?: boolean;\n /**\n * Whether saving this field alone (or any sibling field with\n * primary=true) is enough to authenticate the provider. The card\n * surfaces an \"at least one set\" indicator using this — used by the\n * SDK-auth-missing modal to enable the Continue button as soon as\n * either ANTHROPIC_API_KEY *or* ANTHROPIC_AUTH_TOKEN is saved.\n *\n * Defaults to `true` for secret fields and `false` for plain-text\n * overrides (URLs/hosts). Set explicitly when a secret field is\n * supplementary rather than authentication-conferring.\n */\n primary?: boolean;\n }>;\n}\n\n/**\n * Catalog of every AI provider whose plugin reads credentials from the\n * agent's config bag (POST /api/config). CLI-only providers (Aider, Amp,\n * Copilot, Crush, Goose, Plandex, Pi) authenticate via their own harness\n * binary (~/.claude/, ~/.config/opencode/ etc.) — Bridle handles those\n * and they don't appear here.\n */\nexport const AI_PROVIDER_CREDENTIALS: AIProviderCredentialDef[] = [\n {\n providerId: 'claude',\n label: 'Anthropic Claude',\n description:\n 'Claude SDK sessions require either ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN on the agent. Save one of them to make SDK sessions work.',\n fields: [\n { key: 'ANTHROPIC_API_KEY', secret: true },\n { key: 'ANTHROPIC_AUTH_TOKEN', secret: true },\n ],\n },\n {\n providerId: 'codex',\n label: 'OpenAI Codex',\n description:\n 'Codex SDK / CLI sessions read OPENAI_API_KEY from the agent. Set it here to enable Codex-backed sessions.',\n fields: [{ key: 'OPENAI_API_KEY', secret: true }],\n },\n {\n providerId: 'gemini',\n label: 'Google Gemini',\n description:\n 'Gemini SDK reads GEMINI_API_KEY (preferred) or GOOGLE_API_KEY. Either one works — both shown so an existing GOOGLE_API_KEY in your environment keeps working.',\n fields: [\n { key: 'GEMINI_API_KEY', secret: true },\n { key: 'GOOGLE_API_KEY', secret: true },\n ],\n },\n {\n providerId: 'openai-compat',\n label: 'OpenAI-Compatible',\n description:\n 'Generic OpenAI-compatible provider (Together, Groq, Mistral, etc.). Set the API key and the base URL the SDK should hit.',\n fields: [{ key: 'OPENAI_COMPAT_API_KEY', secret: true }, { key: 'OPENAI_COMPAT_BASE_URL' }],\n },\n {\n providerId: 'cursor',\n label: 'Cursor',\n description:\n 'Cursor agent (CLI + SDK) reads CURSOR_API_KEY from the agent. Set it here to enable both modes.',\n fields: [{ key: 'CURSOR_API_KEY', secret: true }],\n },\n {\n providerId: 'openrouter',\n label: 'OpenRouter',\n description: 'OpenRouter aggregator (300+ models, SDK only). Set OPENROUTER_API_KEY to enable.',\n fields: [{ key: 'OPENROUTER_API_KEY', secret: true }],\n },\n {\n providerId: 'minimax',\n label: 'Minimax',\n description:\n 'Minimax provider (CLI + Anthropic-compatible SDK). Set MINIMAX_API_KEY to enable.',\n fields: [{ key: 'MINIMAX_API_KEY', secret: true }],\n },\n {\n providerId: 'ollama',\n label: 'Ollama',\n description:\n 'Ollama Cloud + self-hosted (CLI + SDK). OLLAMA_API_KEY for Cloud; OLLAMA_HOST optional override (defaults: https://ollama.com when key present, http://localhost:11434 otherwise).',\n fields: [{ key: 'OLLAMA_API_KEY', secret: true }, { key: 'OLLAMA_HOST' }],\n },\n];\n\ninterface AIProviderCredentialsCardProps {\n agent: AgentRef;\n /**\n * Optional filter — when present, only render providers whose providerId\n * appears in the list. Lets a future caller scope the card to \"only the\n * providers I have plugins installed for\" without changing the catalog.\n */\n providerIds?: string[];\n /**\n * When provided, the staged picker preselects this provider so the\n * caller (e.g. the SDK-auth-missing modal) can drop the user straight\n * onto the right form without an extra click.\n */\n initialProviderId?: string;\n /**\n * Fires whenever the currently-selected provider's saved-state changes\n * on disk. `satisfied` is true when at least one primary (auth-conferring)\n * field has a saved non-empty value. Used by the SDK-auth-missing modal\n * to enable a Continue affordance the moment the user persists any\n * acceptable credential, without forcing them to also fill the others.\n */\n onProviderAuthSatisfiedChange?: (satisfied: boolean) => void;\n}\n\n/**\n * Staged credentials manager: shows a provider picker first, then on\n * select renders one card with that provider's config keys plus a back\n * button. Keeps the rendered DOM small (and the surrounding modal\n * viewport-friendly) regardless of how many providers are in the catalog.\n *\n * Replaces the older Claude-only ClaudeCredentialsCard.\n */\nexport function AIProviderCredentialsCard({\n agent,\n providerIds,\n initialProviderId,\n onProviderAuthSatisfiedChange,\n}: AIProviderCredentialsCardProps) {\n const tr = useTr();\n const providers = providerIds\n ? AI_PROVIDER_CREDENTIALS.filter((p) => providerIds.includes(p.providerId))\n : AI_PROVIDER_CREDENTIALS;\n\n // Preselect when the catalog narrows to exactly one provider so the\n // single-provider case (e.g. claude-only filter) skips the picker.\n const [selectedId, setSelectedId] = useState<string | null>(() => {\n if (initialProviderId && providers.some((p) => p.providerId === initialProviderId)) {\n return initialProviderId;\n }\n return providers.length === 1 ? (providers[0]?.providerId ?? null) : null;\n });\n\n const selected = providers.find((p) => p.providerId === selectedId) ?? null;\n\n if (!selected) {\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"rounded-lg bg-bg-sunken p-2 text-text-secondary\">\n <KeyRound className=\"h-4 w-4\" />\n </div>\n <div className=\"space-y-1\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {tr('ai.providerCredentials.pickTitle', 'Choose a provider')}\n </h3>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'ai.providerCredentials.pickDescription',\n 'Pick the provider whose credentials you want to manage. Keys are stored on the agent and read at session start.'\n )}\n </p>\n </div>\n </div>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-2\">\n {providers.map((p) => (\n <button\n key={p.providerId}\n type=\"button\"\n onClick={() => setSelectedId(p.providerId)}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-default bg-bg-surface px-3 py-2 text-left hover:border-border-strong hover:bg-bg-sunken transition-colors\"\n >\n <div className=\"min-w-0\">\n <div className=\"text-sm font-medium text-text-primary truncate\">{p.label}</div>\n <div className=\"text-xs text-text-secondary truncate\">\n {p.fields.map((f) => f.key).join(', ')}\n </div>\n </div>\n <ChevronRight className=\"h-4 w-4 flex-shrink-0 text-text-secondary\" />\n </button>\n ))}\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n {providers.length > 1 ? (\n <button\n type=\"button\"\n onClick={() => setSelectedId(null)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-default px-2 py-1 text-xs text-text-secondary hover:text-text-primary hover:border-border-strong\"\n aria-label={tr('ai.providerCredentials.back', 'Back to providers')}\n >\n <ArrowLeft className=\"h-3.5 w-3.5\" />\n {tr('ai.providerCredentials.back', 'Back')}\n </button>\n ) : null}\n <div className=\"rounded-lg bg-bg-sunken p-2 text-text-secondary\">\n <KeyRound className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary truncate\">{selected.label}</h3>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'ai.providerCredentials.scopedDescription',\n 'Keys are stored on the agent and read at session start.'\n )}\n </p>\n </div>\n </div>\n <ProviderCard\n agent={agent}\n provider={selected}\n onAuthSatisfiedChange={onProviderAuthSatisfiedChange}\n />\n </div>\n );\n}\n\nfunction isPrimaryField(f: AIProviderCredentialDef['fields'][number]): boolean {\n // Defaults: secret fields gate auth; URL/host overrides don't.\n return f.primary ?? (f.secret ?? false);\n}\n\nfunction ProviderCard({\n agent,\n provider,\n onAuthSatisfiedChange,\n}: {\n agent: AgentRef;\n provider: AIProviderCredentialDef;\n onAuthSatisfiedChange?: (satisfied: boolean) => void;\n}) {\n const tr = useTr();\n const [values, setValues] = useState<Record<string, string>>({});\n // savedValues tracks what's actually on the agent (last successful\n // load/persist), not the unsaved input buffer. This is what drives the\n // \"auth satisfied\" indicator — typing an API key in the field doesn't\n // count until the user hits Save.\n const [savedValues, setSavedValues] = useState<Record<string, string>>({});\n const [visible, setVisible] = useState<Record<string, boolean>>({});\n const [loading, setLoading] = useState(true);\n const [saving, setSaving] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState<string | null>(null);\n\n const primaryKeys = provider.fields.filter(isPrimaryField).map((f) => f.key);\n const satisfiedPrimaryKeys = primaryKeys.filter((k) => (savedValues[k] ?? '').trim().length > 0);\n const authSatisfied = satisfiedPrimaryKeys.length > 0;\n\n // Notify parent whenever satisfied flips. Effect (not inline call) so\n // children don't render-during-render of the parent.\n useEffect(() => {\n onAuthSatisfiedChange?.(authSatisfied);\n }, [authSatisfied, onAuthSatisfiedChange]);\n\n const loadValues = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const entries = await Promise.all(\n provider.fields.map(async (f) => [f.key, (await getAgentConfigValue(agent, f.key)) ?? ''])\n );\n const next = Object.fromEntries(entries);\n setValues(next);\n setSavedValues(next);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr('ai.providerCredentials.loadFailed', 'Failed to load provider credentials')\n );\n } finally {\n setLoading(false);\n }\n }, [agent, provider.fields, tr]);\n\n useEffect(() => {\n void loadValues();\n }, [loadValues]);\n\n const persistValue = useCallback(\n async (key: string, value: string) => {\n setSaving(key);\n setError(null);\n setSuccess(null);\n const trimmed = value.trim();\n try {\n if (trimmed) {\n await setAgentConfigValue(agent, key, trimmed);\n } else {\n await deleteAgentConfigValue(agent, key);\n }\n // Mirror the persisted result into savedValues so the auth-\n // satisfied banner / Continue button update immediately.\n setSavedValues((cur) => ({ ...cur, [key]: trimmed }));\n setSuccess(\n trimmed\n ? tr('ai.providerCredentials.saved', `${key} saved`)\n : tr('ai.providerCredentials.cleared', `${key} cleared`)\n );\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr('ai.providerCredentials.saveFailed', 'Failed to save credential')\n );\n } finally {\n setSaving(null);\n }\n },\n [agent, tr]\n );\n\n return (\n <div className=\"rounded-xl border border-border-default bg-bg-surface p-4 space-y-3\">\n <div className=\"space-y-1\">\n <h4 className=\"text-sm font-semibold text-text-primary\">{provider.label}</h4>\n <p className=\"text-xs text-text-secondary\">{provider.description}</p>\n {primaryKeys.length > 1 ? (\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'ai.providerCredentials.eitherOr',\n `Save any one of: ${primaryKeys.join(' · ')}. You don't need to fill all of them.`\n )}\n </p>\n ) : null}\n </div>\n\n {/* Auth-satisfied banner — surfaces the moment any primary credential\n is persisted on the agent. The SDK-auth-missing modal pairs this\n with a Continue button so the user knows they can proceed. */}\n {authSatisfied ? (\n <div className=\"rounded-lg border border-status-success-border bg-status-success-bg px-3 py-2 text-xs text-status-success-text\">\n {tr(\n 'ai.providerCredentials.authSatisfied',\n `${provider.label} is ready. The agent will use ${satisfiedPrimaryKeys.join(' / ')} on the next SDK session.`\n )}\n </div>\n ) : null}\n\n {error ? (\n <div className=\"rounded-lg border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text\">\n {error}\n </div>\n ) : null}\n {success ? (\n <div className=\"rounded-lg border border-status-success-border bg-status-success-bg px-3 py-2 text-sm text-status-success-text\">\n {success}\n </div>\n ) : null}\n\n {loading ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {tr('ai.providerCredentials.loading', 'Loading…')}\n </div>\n ) : (\n <div className=\"space-y-3\">\n {provider.fields.map((f) => (\n <CredentialField\n key={f.key}\n label={f.key}\n secret={f.secret ?? false}\n value={values[f.key] ?? ''}\n onChange={(v) => setValues((cur) => ({ ...cur, [f.key]: v }))}\n saving={saving === f.key}\n visible={visible[f.key] ?? false}\n onToggleVisible={() => setVisible((cur) => ({ ...cur, [f.key]: !cur[f.key] }))}\n onSave={() => void persistValue(f.key, values[f.key] ?? '')}\n onClear={() => void persistValue(f.key, '')}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\ninterface CredentialFieldProps {\n label: string;\n secret: boolean;\n value: string;\n onChange: (value: string) => void;\n saving: boolean;\n visible: boolean;\n onToggleVisible: () => void;\n onSave: () => void;\n onClear: () => void;\n}\n\nfunction CredentialField({\n label,\n secret,\n value,\n onChange,\n saving,\n visible,\n onToggleVisible,\n onSave,\n onClear,\n}: CredentialFieldProps) {\n const showAsPassword = secret && !visible;\n return (\n <div className=\"space-y-2 rounded-lg border border-border-default bg-bg-primary p-3\">\n <label className=\"block text-xs font-medium text-text-secondary\">{label}</label>\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <input\n type={showAsPassword ? 'password' : 'text'}\n value={value}\n onChange={(event) => onChange(event.target.value)}\n autoComplete=\"off\"\n spellCheck={false}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 pr-10 text-sm text-text-primary\"\n />\n {secret ? (\n <button\n type=\"button\"\n onClick={onToggleVisible}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 text-text-secondary hover:text-text-primary\"\n >\n {visible ? <EyeOff className=\"h-4 w-4\" /> : <Eye className=\"h-4 w-4\" />}\n </button>\n ) : null}\n </div>\n <button\n type=\"button\"\n onClick={onSave}\n disabled={saving}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {saving ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Save className=\"h-4 w-4\" />}\n Save\n </button>\n <button\n type=\"button\"\n onClick={onClear}\n disabled={saving || !value}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-default px-3 py-2 text-sm font-medium text-text-secondary hover:text-text-primary hover:border-border-strong disabled:opacity-50\"\n >\n <Trash2 className=\"h-4 w-4\" />\n Clear\n </button>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AIProviderCredentialsCard.js","names":[],"sources":["../../../src/components/ai/AIProviderCredentialsCard.tsx"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\nimport {\n ArrowLeft,\n ChevronRight,\n Eye,\n EyeOff,\n KeyRound,\n Loader2,\n Save,\n Trash2,\n} from 'lucide-react';\n\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n deleteAgentConfigValue,\n getAgentConfigValue,\n setAgentConfigValue,\n type AgentRef,\n} from '@/services/aiApi';\n\n/**\n * One provider definition — what env-var-style keys the agent stores under\n * its config bag and what to call the provider in the UI. Kept as a flat\n * static list rather than discovered from the plugin manifest because the\n * config keys are part of the upstream SDK contract (ANTHROPIC_API_KEY,\n * OPENAI_API_KEY, etc.) and don't change with each plugin release.\n */\nexport interface AIProviderCredentialDef {\n /** Stable provider id — usually matches the plugin's pluginName. */\n providerId: string;\n /** Display name shown in the card heading. */\n label: string;\n /** Short helper text under the heading. */\n description: string;\n /**\n * One row per key. The `kind` distinguishes a primary credential (for\n * which the SDK refuses to start without ONE of the listed keys) from\n * an optional URL/host override.\n */\n fields: Array<{\n /** Agent-config key name, also rendered as the input label. */\n key: string;\n /**\n * Whether the field is a secret (rendered as password with eye toggle)\n * or a plain string (URL / host overrides — visible by default).\n */\n secret?: boolean;\n /**\n * Whether saving this field alone (or any sibling field with\n * primary=true) is enough to authenticate the provider. The card\n * surfaces an \"at least one set\" indicator using this — used by the\n * SDK-auth-missing modal to enable the Continue button as soon as\n * either ANTHROPIC_API_KEY *or* ANTHROPIC_AUTH_TOKEN is saved.\n *\n * Defaults to `true` for secret fields and `false` for plain-text\n * overrides (URLs/hosts). Set explicitly when a secret field is\n * supplementary rather than authentication-conferring.\n */\n primary?: boolean;\n }>;\n}\n\n/**\n * Catalog of every AI provider whose plugin reads credentials from the\n * agent's config bag (POST /api/config). CLI-only providers (Aider, Amp,\n * Copilot, Crush, Goose, Plandex, Pi) authenticate via their own harness\n * binary (~/.claude/, ~/.config/opencode/ etc.) — Bridle handles those\n * and they don't appear here.\n */\nexport const AI_PROVIDER_CREDENTIALS: AIProviderCredentialDef[] = [\n {\n providerId: 'claude',\n label: 'Anthropic Claude',\n description:\n 'Claude SDK sessions require either ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN on the agent. Save one of them to make SDK sessions work.',\n fields: [\n { key: 'ANTHROPIC_API_KEY', secret: true },\n { key: 'ANTHROPIC_AUTH_TOKEN', secret: true },\n ],\n },\n {\n providerId: 'codex',\n label: 'OpenAI Codex',\n description:\n 'Codex SDK / CLI sessions read OPENAI_API_KEY from the agent. Set it here to enable Codex-backed sessions.',\n fields: [{ key: 'OPENAI_API_KEY', secret: true }],\n },\n {\n providerId: 'gemini',\n label: 'Google Gemini',\n description:\n 'Gemini SDK reads GEMINI_API_KEY (preferred) or GOOGLE_API_KEY. Either one works — both shown so an existing GOOGLE_API_KEY in your environment keeps working.',\n fields: [\n { key: 'GEMINI_API_KEY', secret: true },\n { key: 'GOOGLE_API_KEY', secret: true },\n ],\n },\n {\n providerId: 'openai-compat',\n label: 'OpenAI-Compatible',\n description:\n 'Generic OpenAI-compatible provider (Together, Groq, Mistral, etc.). Set the API key and the base URL the SDK should hit.',\n fields: [{ key: 'OPENAI_COMPAT_API_KEY', secret: true }, { key: 'OPENAI_COMPAT_BASE_URL' }],\n },\n {\n providerId: 'cursor',\n label: 'Cursor',\n description:\n 'Cursor agent (CLI + SDK) reads CURSOR_API_KEY from the agent. Set it here to enable both modes.',\n fields: [{ key: 'CURSOR_API_KEY', secret: true }],\n },\n {\n providerId: 'openrouter',\n label: 'OpenRouter',\n description: 'OpenRouter aggregator (300+ models, SDK only). Set OPENROUTER_API_KEY to enable.',\n fields: [{ key: 'OPENROUTER_API_KEY', secret: true }],\n },\n {\n providerId: 'minimax',\n label: 'Minimax',\n description:\n 'Minimax provider (CLI + Anthropic-compatible SDK). Set MINIMAX_API_KEY to enable.',\n fields: [{ key: 'MINIMAX_API_KEY', secret: true }],\n },\n {\n providerId: 'ollama',\n label: 'Ollama',\n description:\n 'Ollama Cloud + self-hosted (CLI + SDK). OLLAMA_API_KEY for Cloud; OLLAMA_HOST optional override (defaults: https://ollama.com when key present, http://localhost:11434 otherwise).',\n fields: [{ key: 'OLLAMA_API_KEY', secret: true }, { key: 'OLLAMA_HOST' }],\n },\n];\n\ninterface AIProviderCredentialsCardProps {\n agent: AgentRef;\n /**\n * Optional filter — when present, only render providers whose providerId\n * appears in the list. Lets a future caller scope the card to \"only the\n * providers I have plugins installed for\" without changing the catalog.\n */\n providerIds?: string[];\n /**\n * When provided, the staged picker preselects this provider so the\n * caller (e.g. the SDK-auth-missing modal) can drop the user straight\n * onto the right form without an extra click.\n */\n initialProviderId?: string;\n /**\n * Fires whenever the currently-selected provider's saved-state changes\n * on disk. `satisfied` is true when at least one primary (auth-conferring)\n * field has a saved non-empty value. Used by the SDK-auth-missing modal\n * to enable a Continue affordance the moment the user persists any\n * acceptable credential, without forcing them to also fill the others.\n */\n onProviderAuthSatisfiedChange?: (satisfied: boolean) => void;\n}\n\n/**\n * Staged credentials manager: shows a provider picker first, then on\n * select renders one card with that provider's config keys plus a back\n * button. Keeps the rendered DOM small (and the surrounding modal\n * viewport-friendly) regardless of how many providers are in the catalog.\n *\n * Replaces the older Claude-only ClaudeCredentialsCard.\n */\nexport function AIProviderCredentialsCard({\n agent,\n providerIds,\n initialProviderId,\n onProviderAuthSatisfiedChange,\n}: AIProviderCredentialsCardProps) {\n const tr = useTr();\n const providers = providerIds\n ? AI_PROVIDER_CREDENTIALS.filter((p) => providerIds.includes(p.providerId))\n : AI_PROVIDER_CREDENTIALS;\n\n // Preselect when the catalog narrows to exactly one provider so the\n // single-provider case (e.g. claude-only filter) skips the picker.\n const [selectedId, setSelectedId] = useState<string | null>(() => {\n if (initialProviderId && providers.some((p) => p.providerId === initialProviderId)) {\n return initialProviderId;\n }\n return providers.length === 1 ? (providers[0]?.providerId ?? null) : null;\n });\n\n const selected = providers.find((p) => p.providerId === selectedId) ?? null;\n\n if (!selected) {\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"rounded-lg bg-bg-sunken p-2 text-text-secondary\">\n <KeyRound className=\"h-4 w-4\" />\n </div>\n <div className=\"space-y-1\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {tr('ai.providerCredentials.pickTitle', 'Choose a provider')}\n </h3>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'ai.providerCredentials.pickDescription',\n 'Pick the provider whose credentials you want to manage. Keys are stored on the agent and read at session start.'\n )}\n </p>\n </div>\n </div>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-2\">\n {providers.map((p) => (\n <button\n key={p.providerId}\n type=\"button\"\n onClick={() => setSelectedId(p.providerId)}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-default bg-bg-surface px-3 py-2 text-left hover:border-border-strong hover:bg-bg-sunken transition-colors\"\n >\n <div className=\"min-w-0\">\n <div className=\"text-sm font-medium text-text-primary truncate\">{p.label}</div>\n <div className=\"text-xs text-text-secondary truncate\">\n {p.fields.map((f) => f.key).join(', ')}\n </div>\n </div>\n <ChevronRight className=\"h-4 w-4 flex-shrink-0 text-text-secondary\" />\n </button>\n ))}\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n {providers.length > 1 ? (\n <button\n type=\"button\"\n onClick={() => setSelectedId(null)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-default px-2 py-1 text-xs text-text-secondary hover:text-text-primary hover:border-border-strong\"\n aria-label={tr('ai.providerCredentials.back', 'Back to providers')}\n >\n <ArrowLeft className=\"h-3.5 w-3.5\" />\n {tr('ai.providerCredentials.back', 'Back')}\n </button>\n ) : null}\n <div className=\"rounded-lg bg-bg-sunken p-2 text-text-secondary\">\n <KeyRound className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary truncate\">{selected.label}</h3>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'ai.providerCredentials.scopedDescription',\n 'Keys are stored on the agent and read at session start.'\n )}\n </p>\n </div>\n </div>\n <ProviderCard\n agent={agent}\n provider={selected}\n onAuthSatisfiedChange={onProviderAuthSatisfiedChange}\n />\n </div>\n );\n}\n\nfunction isPrimaryField(f: AIProviderCredentialDef['fields'][number]): boolean {\n // Defaults: secret fields gate auth; URL/host overrides don't.\n return f.primary ?? f.secret ?? false;\n}\n\nfunction ProviderCard({\n agent,\n provider,\n onAuthSatisfiedChange,\n}: {\n agent: AgentRef;\n provider: AIProviderCredentialDef;\n onAuthSatisfiedChange?: (satisfied: boolean) => void;\n}) {\n const tr = useTr();\n const [values, setValues] = useState<Record<string, string>>({});\n // savedValues tracks what's actually on the agent (last successful\n // load/persist), not the unsaved input buffer. This is what drives the\n // \"auth satisfied\" indicator — typing an API key in the field doesn't\n // count until the user hits Save.\n const [savedValues, setSavedValues] = useState<Record<string, string>>({});\n const [visible, setVisible] = useState<Record<string, boolean>>({});\n const [loading, setLoading] = useState(true);\n const [saving, setSaving] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState<string | null>(null);\n\n const primaryKeys = provider.fields.filter(isPrimaryField).map((f) => f.key);\n const satisfiedPrimaryKeys = primaryKeys.filter((k) => (savedValues[k] ?? '').trim().length > 0);\n const authSatisfied = satisfiedPrimaryKeys.length > 0;\n\n // Notify parent whenever satisfied flips. Effect (not inline call) so\n // children don't render-during-render of the parent.\n useEffect(() => {\n onAuthSatisfiedChange?.(authSatisfied);\n }, [authSatisfied, onAuthSatisfiedChange]);\n\n const loadValues = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const entries = await Promise.all(\n provider.fields.map(async (f) => [f.key, (await getAgentConfigValue(agent, f.key)) ?? ''])\n );\n const next = Object.fromEntries(entries);\n setValues(next);\n setSavedValues(next);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr('ai.providerCredentials.loadFailed', 'Failed to load provider credentials')\n );\n } finally {\n setLoading(false);\n }\n }, [agent, provider.fields, tr]);\n\n useEffect(() => {\n void loadValues();\n }, [loadValues]);\n\n const persistValue = useCallback(\n async (key: string, value: string) => {\n setSaving(key);\n setError(null);\n setSuccess(null);\n const trimmed = value.trim();\n try {\n if (trimmed) {\n await setAgentConfigValue(agent, key, trimmed);\n } else {\n await deleteAgentConfigValue(agent, key);\n }\n // Mirror the persisted result into savedValues so the auth-\n // satisfied banner / Continue button update immediately.\n setSavedValues((cur) => ({ ...cur, [key]: trimmed }));\n setSuccess(\n trimmed\n ? tr('ai.providerCredentials.saved', `${key} saved`)\n : tr('ai.providerCredentials.cleared', `${key} cleared`)\n );\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr('ai.providerCredentials.saveFailed', 'Failed to save credential')\n );\n } finally {\n setSaving(null);\n }\n },\n [agent, tr]\n );\n\n return (\n <div className=\"rounded-xl border border-border-default bg-bg-surface p-4 space-y-3\">\n <div className=\"space-y-1\">\n <h4 className=\"text-sm font-semibold text-text-primary\">{provider.label}</h4>\n <p className=\"text-xs text-text-secondary\">{provider.description}</p>\n {primaryKeys.length > 1 ? (\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'ai.providerCredentials.eitherOr',\n `Save any one of: ${primaryKeys.join(' · ')}. You don't need to fill all of them.`\n )}\n </p>\n ) : null}\n </div>\n\n {/* Auth-satisfied banner — surfaces the moment any primary credential\n is persisted on the agent. The SDK-auth-missing modal pairs this\n with a Continue button so the user knows they can proceed. */}\n {authSatisfied ? (\n <div className=\"rounded-lg border border-status-success-border bg-status-success-bg px-3 py-2 text-xs text-status-success-text\">\n {tr(\n 'ai.providerCredentials.authSatisfied',\n `${provider.label} is ready. The agent will use ${satisfiedPrimaryKeys.join(' / ')} on the next SDK session.`\n )}\n </div>\n ) : null}\n\n {error ? (\n <div className=\"rounded-lg border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text\">\n {error}\n </div>\n ) : null}\n {success ? (\n <div className=\"rounded-lg border border-status-success-border bg-status-success-bg px-3 py-2 text-sm text-status-success-text\">\n {success}\n </div>\n ) : null}\n\n {loading ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {tr('ai.providerCredentials.loading', 'Loading…')}\n </div>\n ) : (\n <div className=\"space-y-3\">\n {provider.fields.map((f) => (\n <CredentialField\n key={f.key}\n label={f.key}\n secret={f.secret ?? false}\n value={values[f.key] ?? ''}\n onChange={(v) => setValues((cur) => ({ ...cur, [f.key]: v }))}\n saving={saving === f.key}\n visible={visible[f.key] ?? false}\n onToggleVisible={() => setVisible((cur) => ({ ...cur, [f.key]: !cur[f.key] }))}\n onSave={() => void persistValue(f.key, values[f.key] ?? '')}\n onClear={() => void persistValue(f.key, '')}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\ninterface CredentialFieldProps {\n label: string;\n secret: boolean;\n value: string;\n onChange: (value: string) => void;\n saving: boolean;\n visible: boolean;\n onToggleVisible: () => void;\n onSave: () => void;\n onClear: () => void;\n}\n\nfunction CredentialField({\n label,\n secret,\n value,\n onChange,\n saving,\n visible,\n onToggleVisible,\n onSave,\n onClear,\n}: CredentialFieldProps) {\n const showAsPassword = secret && !visible;\n return (\n <div className=\"space-y-2 rounded-lg border border-border-default bg-bg-primary p-3\">\n <label className=\"block text-xs font-medium text-text-secondary\">{label}</label>\n <div className=\"flex gap-2\">\n <div className=\"relative flex-1\">\n <input\n type={showAsPassword ? 'password' : 'text'}\n value={value}\n onChange={(event) => onChange(event.target.value)}\n autoComplete=\"off\"\n spellCheck={false}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 pr-10 text-sm text-text-primary\"\n />\n {secret ? (\n <button\n type=\"button\"\n onClick={onToggleVisible}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 text-text-secondary hover:text-text-primary\"\n >\n {visible ? <EyeOff className=\"h-4 w-4\" /> : <Eye className=\"h-4 w-4\" />}\n </button>\n ) : null}\n </div>\n <button\n type=\"button\"\n onClick={onSave}\n disabled={saving}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {saving ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Save className=\"h-4 w-4\" />}\n Save\n </button>\n <button\n type=\"button\"\n onClick={onClear}\n disabled={saving || !value}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-default px-3 py-2 text-sm font-medium text-text-secondary hover:text-text-primary hover:border-border-strong disabled:opacity-50\"\n >\n <Trash2 className=\"h-4 w-4\" />\n Clear\n </button>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -6,7 +6,55 @@ import { useMutation as s, useQuery as c } from "@apollo/client/react";
|
|
|
6
6
|
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
7
7
|
//#region src/components/security/SecurityProviderDrawer.tsx
|
|
8
8
|
function d({ vibeId: d, stage: f, onClose: p }) {
|
|
9
|
-
let { data: m, loading: h } = c(t), [g, { loading: _ }] = s(n), [v, { loading: y }] = s(e), [b, x] = a(null), S = i(() => (m?.securityProviders ?? []).filter((e) => e.stage === f && e.enabled), [m, f]), C = r.get(f), w = C?.slug ?? f.toLowerCase().replace(/_/g, "-")
|
|
9
|
+
let { data: m, loading: h } = c(t), [g, { loading: _ }] = s(n), [v, { loading: y }] = s(e), [b, x] = a(null), S = i(() => (m?.securityProviders ?? []).filter((e) => e.stage === f && e.enabled), [m, f]), C = r.get(f), w = C?.slug ?? f.toLowerCase().replace(/_/g, "-"), T = {
|
|
10
|
+
SECRETS_SCAN: [{
|
|
11
|
+
pkg: "@burdenoff/vibe-plugin-gitleaks",
|
|
12
|
+
desc: "Gitleaks — git secret scanner"
|
|
13
|
+
}, {
|
|
14
|
+
pkg: "@burdenoff/vibe-plugin-trufflehog",
|
|
15
|
+
desc: "TruffleHog — credential scanner"
|
|
16
|
+
}],
|
|
17
|
+
SAST: [{
|
|
18
|
+
pkg: "@burdenoff/vibe-plugin-semgrep",
|
|
19
|
+
desc: "Semgrep — static analysis"
|
|
20
|
+
}, {
|
|
21
|
+
pkg: "@burdenoff/vibe-plugin-codeql",
|
|
22
|
+
desc: "CodeQL — deep SAST (slow)"
|
|
23
|
+
}],
|
|
24
|
+
DEPENDENCY_SCAN: [{
|
|
25
|
+
pkg: "@burdenoff/vibe-plugin-trivy",
|
|
26
|
+
desc: "Trivy — deps & CVE scan"
|
|
27
|
+
}, {
|
|
28
|
+
pkg: "@burdenoff/vibe-plugin-osv-scanner",
|
|
29
|
+
desc: "OSV — OSS vuln DB"
|
|
30
|
+
}],
|
|
31
|
+
LICENSE_SCAN: [{
|
|
32
|
+
pkg: "@burdenoff/vibe-plugin-license-finder",
|
|
33
|
+
desc: "license-finder"
|
|
34
|
+
}],
|
|
35
|
+
SBOM: [{
|
|
36
|
+
pkg: "@burdenoff/vibe-plugin-syft",
|
|
37
|
+
desc: "Syft — SBOM generator"
|
|
38
|
+
}],
|
|
39
|
+
CONTAINER_SCAN: [{
|
|
40
|
+
pkg: "@burdenoff/vibe-plugin-trivy",
|
|
41
|
+
desc: "Trivy — image CVE scan"
|
|
42
|
+
}, {
|
|
43
|
+
pkg: "@burdenoff/vibe-plugin-grype",
|
|
44
|
+
desc: "Grype — image scanner"
|
|
45
|
+
}],
|
|
46
|
+
IAC_SCAN: [{
|
|
47
|
+
pkg: "@burdenoff/vibe-plugin-checkov",
|
|
48
|
+
desc: "Checkov — IaC linter"
|
|
49
|
+
}, {
|
|
50
|
+
pkg: "@burdenoff/vibe-plugin-tfsec",
|
|
51
|
+
desc: "tfsec — Terraform"
|
|
52
|
+
}],
|
|
53
|
+
DAST: [{
|
|
54
|
+
pkg: "@burdenoff/vibe-plugin-zap",
|
|
55
|
+
desc: "OWASP ZAP — dynamic scan"
|
|
56
|
+
}]
|
|
57
|
+
}[f] ?? [];
|
|
10
58
|
return /* @__PURE__ */ u("aside", {
|
|
11
59
|
"data-testid": "security-provider-drawer",
|
|
12
60
|
className: "fixed right-0 top-0 bottom-0 w-[28rem] bg-bg-elevated border-l border-border-default shadow-lg z-50 overflow-y-auto",
|
|
@@ -33,9 +81,9 @@ function d({ vibeId: d, stage: f, onClose: p }) {
|
|
|
33
81
|
className: "text-xs text-text-secondary",
|
|
34
82
|
children: "Loading providers…"
|
|
35
83
|
}),
|
|
36
|
-
!h && S.length === 0 && /* @__PURE__ */ u("
|
|
37
|
-
className: "text-xs text-text-secondary",
|
|
38
|
-
children: [
|
|
84
|
+
!h && S.length === 0 && /* @__PURE__ */ u("div", {
|
|
85
|
+
className: "space-y-2 text-xs text-text-secondary",
|
|
86
|
+
children: [/* @__PURE__ */ u("p", { children: [
|
|
39
87
|
"No providers registered for this stage. Install one via",
|
|
40
88
|
" ",
|
|
41
89
|
/* @__PURE__ */ l("code", {
|
|
@@ -43,7 +91,25 @@ function d({ vibeId: d, stage: f, onClose: p }) {
|
|
|
43
91
|
children: "vibe plugin install …"
|
|
44
92
|
}),
|
|
45
93
|
"."
|
|
46
|
-
]
|
|
94
|
+
] }), T.length > 0 && /* @__PURE__ */ u("div", {
|
|
95
|
+
className: "rounded-md border border-border-subtle p-3 space-y-2",
|
|
96
|
+
children: [/* @__PURE__ */ l("p", {
|
|
97
|
+
className: "text-text-primary font-medium",
|
|
98
|
+
children: "Suggested providers"
|
|
99
|
+
}), /* @__PURE__ */ l("ul", {
|
|
100
|
+
className: "space-y-1.5",
|
|
101
|
+
children: T.map((e) => /* @__PURE__ */ u("li", {
|
|
102
|
+
className: "flex flex-col",
|
|
103
|
+
children: [/* @__PURE__ */ u("code", {
|
|
104
|
+
className: "text-text-primary text-[11px] break-all",
|
|
105
|
+
children: ["vibe plugin install ", e.pkg]
|
|
106
|
+
}), /* @__PURE__ */ l("span", {
|
|
107
|
+
className: "text-text-secondary",
|
|
108
|
+
children: e.desc
|
|
109
|
+
})]
|
|
110
|
+
}, e.pkg))
|
|
111
|
+
})]
|
|
112
|
+
})]
|
|
47
113
|
}),
|
|
48
114
|
S.map((e) => /* @__PURE__ */ u("label", {
|
|
49
115
|
className: "flex items-start gap-3 p-3 rounded-md border border-border-subtle hover:border-border-default cursor-pointer",
|
|
@@ -75,32 +141,41 @@ function d({ vibeId: d, stage: f, onClose: p }) {
|
|
|
75
141
|
}, e.id)),
|
|
76
142
|
/* @__PURE__ */ u("div", {
|
|
77
143
|
className: "flex gap-2 pt-2 border-t border-border-subtle",
|
|
78
|
-
children: [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
144
|
+
children: [
|
|
145
|
+
/* @__PURE__ */ l("button", {
|
|
146
|
+
type: "button",
|
|
147
|
+
disabled: !b || _,
|
|
148
|
+
onClick: async () => {
|
|
149
|
+
b && await g({ variables: { input: {
|
|
150
|
+
vibeId: d,
|
|
151
|
+
stage: f,
|
|
152
|
+
providerKey: b,
|
|
153
|
+
config: {}
|
|
154
|
+
} } });
|
|
155
|
+
},
|
|
156
|
+
className: "flex-1 px-3 py-2 rounded-md bg-action-primary-bg text-action-primary-text disabled:opacity-50",
|
|
157
|
+
children: "Save"
|
|
158
|
+
}),
|
|
159
|
+
/* @__PURE__ */ l("button", {
|
|
160
|
+
type: "button",
|
|
161
|
+
disabled: y,
|
|
162
|
+
"data-testid": `security-run-scan-${w}`,
|
|
163
|
+
onClick: async () => {
|
|
164
|
+
await v({ variables: {
|
|
165
|
+
vibeId: d,
|
|
166
|
+
stage: f
|
|
167
|
+
} });
|
|
168
|
+
},
|
|
169
|
+
className: "flex-1 px-3 py-2 rounded-md border border-border-default bg-bg-sunken hover:bg-bg-surface text-text-primary disabled:opacity-50",
|
|
170
|
+
children: "Run scan"
|
|
171
|
+
}),
|
|
172
|
+
/* @__PURE__ */ l("button", {
|
|
173
|
+
type: "button",
|
|
174
|
+
onClick: p,
|
|
175
|
+
className: "px-3 py-2 rounded-md border border-border-default text-text-primary hover:bg-bg-sunken",
|
|
176
|
+
children: "Close"
|
|
177
|
+
})
|
|
178
|
+
]
|
|
104
179
|
})
|
|
105
180
|
]
|
|
106
181
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecurityProviderDrawer.js","names":[],"sources":["../../../src/components/security/SecurityProviderDrawer.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { X } from 'lucide-react';\n\nimport {\n RUN_SECURITY_SCAN_MUTATION,\n SECURITY_PROVIDERS_QUERY,\n SET_SECURITY_PROVIDER_MUTATION,\n} from './gql';\nimport { STAGE_BY_ID, type SecurityProviderRow, type SecurityStage } from './types';\n\ninterface DrawerProps {\n vibeId: string;\n stage: SecurityStage;\n onClose: () => void;\n}\n\ninterface ProvidersResp {\n securityProviders: SecurityProviderRow[];\n}\n\nexport function SecurityProviderDrawer({\n vibeId,\n stage,\n onClose,\n}: DrawerProps): React.ReactElement {\n const { data, loading } = useQuery<ProvidersResp>(SECURITY_PROVIDERS_QUERY);\n const [setProvider, { loading: saving }] = useMutation(SET_SECURITY_PROVIDER_MUTATION);\n const [runScan, { loading: scanning }] = useMutation(RUN_SECURITY_SCAN_MUTATION);\n const [selected, setSelected] = useState<string | null>(null);\n\n const available = useMemo(\n () => (data?.securityProviders ?? []).filter((p) => p.stage === stage && p.enabled),\n [data, stage]\n );\n const stageInfo = STAGE_BY_ID.get(stage);\n const stageSlug = stageInfo?.slug ?? stage.toLowerCase().replace(/_/g, '-');\n\n return (\n <aside\n data-testid=\"security-provider-drawer\"\n className=\"fixed right-0 top-0 bottom-0 w-[28rem] bg-bg-elevated border-l border-border-default shadow-lg z-50 overflow-y-auto\"\n >\n <div className=\"flex items-center justify-between px-4 py-3 border-b border-border-subtle\">\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">{stageInfo?.label}</h3>\n <p className=\"text-xs text-text-secondary\">Configure provider for this stage</p>\n </div>\n <button\n type=\"button\"\n aria-label=\"Close drawer\"\n onClick={onClose}\n className=\"text-text-secondary hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n\n <div className=\"p-4 space-y-4\" data-testid=\"security-provider-config-form\">\n {loading && <p className=\"text-xs text-text-secondary\">Loading providers…</p>}\n {!loading && available.length === 0 && (\n <p className=\"text-xs text-text-secondary\">\n No providers registered for this stage. Install one via{' '}\n <code className=\"text-text-primary\">vibe plugin install …</code>.\n </p>\n )}\n {available.map((p) => (\n <label\n key={p.id}\n className=\"flex items-start gap-3 p-3 rounded-md border border-border-subtle hover:border-border-default cursor-pointer\"\n >\n <input\n type=\"radio\"\n name=\"provider\"\n value={p.providerName}\n checked={selected === p.providerName || (selected === null && p.isDefault)}\n onChange={() => setSelected(p.providerName)}\n aria-label={p.providerName}\n />\n <div className=\"flex-1\">\n <div className=\"text-sm font-medium text-text-primary\">{p.providerName}</div>\n <div className=\"text-xs text-text-secondary\">\n {p.pluginName} · v{p.toolVersion}\n {p.isDefault && <span className=\"ml-2 text-action-primary-bg\">default</span>}\n </div>\n </div>\n </label>\n ))}\n\n <div className=\"flex gap-2 pt-2 border-t border-border-subtle\">\n <button\n type=\"button\"\n disabled={!selected || saving}\n onClick={async () => {\n if (!selected) return;\n await setProvider({\n variables: {\n input: { vibeId, stage, providerKey: selected, config: {} },\n },\n });\n }}\n className=\"flex-1 px-3 py-2 rounded-md bg-action-primary-bg text-action-primary-text disabled:opacity-50\"\n >\n Save\n </button>\n <button\n type=\"button\"\n disabled={scanning}\n data-testid={`security-run-scan-${stageSlug}`}\n onClick={async () => {\n await runScan({ variables: { vibeId, stage } });\n }}\n className=\"flex-1 px-3 py-2 rounded-md border border-border-default bg-bg-sunken hover:bg-bg-surface text-text-primary disabled:opacity-50\"\n >\n Run scan\n </button>\n </div>\n </div>\n </aside>\n );\n}\n"],"mappings":";;;;;;;AAqBA,SAAgB,EAAuB,EACrC,WACA,UACA,cACkC;CAClC,IAAM,EAAE,SAAM,eAAY,EAAwB,EAAyB,EACrE,CAAC,GAAa,EAAE,SAAS,OAAY,EAAY,EAA+B,EAChF,CAAC,GAAS,EAAE,SAAS,OAAc,EAAY,EAA2B,EAC1E,CAAC,GAAU,KAAe,EAAwB,KAAK,EAEvD,IAAY,SACT,GAAM,qBAAqB,EAAE,EAAE,QAAQ,MAAM,EAAE,UAAU,KAAS,EAAE,QAAQ,EACnF,CAAC,GAAM,EAAM,CACd,EACK,IAAY,EAAY,IAAI,EAAM,EAClC,IAAY,GAAW,QAAQ,EAAM,aAAa,CAAC,QAAQ,MAAM,IAAI;AAE3E,QACE,kBAAC,SAAD;EACE,eAAY;EACZ,WAAU;YAFZ,CAIE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;IAAI,WAAU;cAA2C,GAAW;IAAW,CAAA,EAC/E,kBAAC,KAAD;IAAG,WAAU;cAA8B;IAAqC,CAAA,CAC5E,EAAA,CAAA,EACN,kBAAC,UAAD;IACE,MAAK;IACL,cAAW;IACX,SAAS;IACT,WAAU;cAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;IAClB,CAAA,CACL;MAEN,kBAAC,OAAD;GAAK,WAAU;GAAgB,eAAY;aAA3C;IACG,KAAW,kBAAC,KAAD;KAAG,WAAU;eAA8B;KAAsB,CAAA;IAC5E,CAAC,KAAW,EAAU,WAAW,KAChC,kBAAC,KAAD;KAAG,WAAU;eAAb;MAA2C;MACe;MACxD,kBAAC,QAAD;OAAM,WAAU;iBAAoB;OAA4B,CAAA;;MAC9D;;IAEL,EAAU,KAAK,MACd,kBAAC,SAAD;KAEE,WAAU;eAFZ,CAIE,kBAAC,SAAD;MACE,MAAK;MACL,MAAK;MACL,OAAO,EAAE;MACT,SAAS,MAAa,EAAE,gBAAiB,MAAa,QAAQ,EAAE;MAChE,gBAAgB,EAAY,EAAE,aAAa;MAC3C,cAAY,EAAE;MACd,CAAA,EACF,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAyC,EAAE;OAAmB,CAAA,EAC7E,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACG,EAAE;QAAW;QAAK,EAAE;QACpB,EAAE,aAAa,kBAAC,QAAD;SAAM,WAAU;mBAA8B;SAAc,CAAA;QACxE;SACF;QACA;OAlBD,EAAE,GAkBD,CACR;IAEF,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,UAAU,CAAC,KAAY;MACvB,SAAS,YAAY;AACd,YACL,MAAM,EAAY,EAChB,WAAW,EACT,OAAO;QAAE;QAAQ;QAAO,aAAa;QAAU,QAAQ,EAAE;QAAE,EAC5D,EACF,CAAC;;MAEJ,WAAU;gBACX;MAEQ,CAAA,EACT,kBAAC,UAAD;MACE,MAAK;MACL,UAAU;MACV,eAAa,qBAAqB;MAClC,SAAS,YAAY;AACnB,aAAM,EAAQ,EAAE,WAAW;QAAE;QAAQ;QAAO,EAAE,CAAC;;MAEjD,WAAU;gBACX;MAEQ,CAAA,CACL;;IACF;KACA"}
|
|
1
|
+
{"version":3,"file":"SecurityProviderDrawer.js","names":[],"sources":["../../../src/components/security/SecurityProviderDrawer.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { X } from 'lucide-react';\n\nimport {\n RUN_SECURITY_SCAN_MUTATION,\n SECURITY_PROVIDERS_QUERY,\n SET_SECURITY_PROVIDER_MUTATION,\n} from './gql';\nimport { STAGE_BY_ID, type SecurityProviderRow, type SecurityStage } from './types';\n\ninterface DrawerProps {\n vibeId: string;\n stage: SecurityStage;\n onClose: () => void;\n}\n\ninterface ProvidersResp {\n securityProviders: SecurityProviderRow[];\n}\n\nexport function SecurityProviderDrawer({\n vibeId,\n stage,\n onClose,\n}: DrawerProps): React.ReactElement {\n const { data, loading } = useQuery<ProvidersResp>(SECURITY_PROVIDERS_QUERY);\n const [setProvider, { loading: saving }] = useMutation(SET_SECURITY_PROVIDER_MUTATION);\n const [runScan, { loading: scanning }] = useMutation(RUN_SECURITY_SCAN_MUTATION);\n const [selected, setSelected] = useState<string | null>(null);\n\n const available = useMemo(\n () => (data?.securityProviders ?? []).filter((p) => p.stage === stage && p.enabled),\n [data, stage]\n );\n const stageInfo = STAGE_BY_ID.get(stage);\n const stageSlug = stageInfo?.slug ?? stage.toLowerCase().replace(/_/g, '-');\n\n // Suggested installable providers by stage — these are the well-known\n // vibe-plugin packages we ship and recommend for each security lifecycle\n // step. Surfaced when the workspace has no providers registered yet so the\n // user has a concrete next step (\"vibe plugin install …\") instead of a\n // dead-end empty state.\n const SUGGESTIONS_BY_STAGE: Record<string, ReadonlyArray<{ pkg: string; desc: string }>> = {\n SECRETS_SCAN: [\n { pkg: '@burdenoff/vibe-plugin-gitleaks', desc: 'Gitleaks — git secret scanner' },\n { pkg: '@burdenoff/vibe-plugin-trufflehog', desc: 'TruffleHog — credential scanner' },\n ],\n SAST: [\n { pkg: '@burdenoff/vibe-plugin-semgrep', desc: 'Semgrep — static analysis' },\n { pkg: '@burdenoff/vibe-plugin-codeql', desc: 'CodeQL — deep SAST (slow)' },\n ],\n DEPENDENCY_SCAN: [\n { pkg: '@burdenoff/vibe-plugin-trivy', desc: 'Trivy — deps & CVE scan' },\n { pkg: '@burdenoff/vibe-plugin-osv-scanner', desc: 'OSV — OSS vuln DB' },\n ],\n LICENSE_SCAN: [{ pkg: '@burdenoff/vibe-plugin-license-finder', desc: 'license-finder' }],\n SBOM: [{ pkg: '@burdenoff/vibe-plugin-syft', desc: 'Syft — SBOM generator' }],\n CONTAINER_SCAN: [\n { pkg: '@burdenoff/vibe-plugin-trivy', desc: 'Trivy — image CVE scan' },\n { pkg: '@burdenoff/vibe-plugin-grype', desc: 'Grype — image scanner' },\n ],\n IAC_SCAN: [\n { pkg: '@burdenoff/vibe-plugin-checkov', desc: 'Checkov — IaC linter' },\n { pkg: '@burdenoff/vibe-plugin-tfsec', desc: 'tfsec — Terraform' },\n ],\n DAST: [{ pkg: '@burdenoff/vibe-plugin-zap', desc: 'OWASP ZAP — dynamic scan' }],\n };\n const suggestions = SUGGESTIONS_BY_STAGE[stage] ?? [];\n\n return (\n <aside\n data-testid=\"security-provider-drawer\"\n className=\"fixed right-0 top-0 bottom-0 w-[28rem] bg-bg-elevated border-l border-border-default shadow-lg z-50 overflow-y-auto\"\n >\n <div className=\"flex items-center justify-between px-4 py-3 border-b border-border-subtle\">\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">{stageInfo?.label}</h3>\n <p className=\"text-xs text-text-secondary\">Configure provider for this stage</p>\n </div>\n <button\n type=\"button\"\n aria-label=\"Close drawer\"\n onClick={onClose}\n className=\"text-text-secondary hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n\n <div className=\"p-4 space-y-4\" data-testid=\"security-provider-config-form\">\n {loading && <p className=\"text-xs text-text-secondary\">Loading providers…</p>}\n {!loading && available.length === 0 && (\n <div className=\"space-y-2 text-xs text-text-secondary\">\n <p>\n No providers registered for this stage. Install one via{' '}\n <code className=\"text-text-primary\">vibe plugin install …</code>.\n </p>\n {suggestions.length > 0 && (\n <div className=\"rounded-md border border-border-subtle p-3 space-y-2\">\n <p className=\"text-text-primary font-medium\">Suggested providers</p>\n <ul className=\"space-y-1.5\">\n {suggestions.map((s) => (\n <li key={s.pkg} className=\"flex flex-col\">\n <code className=\"text-text-primary text-[11px] break-all\">\n vibe plugin install {s.pkg}\n </code>\n <span className=\"text-text-secondary\">{s.desc}</span>\n </li>\n ))}\n </ul>\n </div>\n )}\n </div>\n )}\n {available.map((p) => (\n <label\n key={p.id}\n className=\"flex items-start gap-3 p-3 rounded-md border border-border-subtle hover:border-border-default cursor-pointer\"\n >\n <input\n type=\"radio\"\n name=\"provider\"\n value={p.providerName}\n checked={selected === p.providerName || (selected === null && p.isDefault)}\n onChange={() => setSelected(p.providerName)}\n aria-label={p.providerName}\n />\n <div className=\"flex-1\">\n <div className=\"text-sm font-medium text-text-primary\">{p.providerName}</div>\n <div className=\"text-xs text-text-secondary\">\n {p.pluginName} · v{p.toolVersion}\n {p.isDefault && <span className=\"ml-2 text-action-primary-bg\">default</span>}\n </div>\n </div>\n </label>\n ))}\n\n <div className=\"flex gap-2 pt-2 border-t border-border-subtle\">\n <button\n type=\"button\"\n disabled={!selected || saving}\n onClick={async () => {\n if (!selected) return;\n await setProvider({\n variables: {\n input: { vibeId, stage, providerKey: selected, config: {} },\n },\n });\n }}\n className=\"flex-1 px-3 py-2 rounded-md bg-action-primary-bg text-action-primary-text disabled:opacity-50\"\n >\n Save\n </button>\n <button\n type=\"button\"\n disabled={scanning}\n data-testid={`security-run-scan-${stageSlug}`}\n onClick={async () => {\n await runScan({ variables: { vibeId, stage } });\n }}\n className=\"flex-1 px-3 py-2 rounded-md border border-border-default bg-bg-sunken hover:bg-bg-surface text-text-primary disabled:opacity-50\"\n >\n Run scan\n </button>\n <button\n type=\"button\"\n onClick={onClose}\n className=\"px-3 py-2 rounded-md border border-border-default text-text-primary hover:bg-bg-sunken\"\n >\n Close\n </button>\n </div>\n </div>\n </aside>\n );\n}\n"],"mappings":"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"}
|