@burdenoff/microfe-vibecontrols 2026.522.6 → 2026.522.7

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.
@@ -8,21 +8,21 @@ import { useToggleAgentBookmark as o } from "../../hooks/useToggleAgentBookmark.
8
8
  import { AgentManagerMessageList as s } from "./AgentManagerMessageList.js";
9
9
  import { AgentManagerInputArea as c } from "./AgentManagerInputArea.js";
10
10
  import { AgentManagerBookmarksDrawer as l } from "./AgentManagerBookmarksDrawer.js";
11
- import { PluginInstallModal as ee } from "./PluginInstallModal.js";
12
- import { AIProviderCredentialsCard as u } from "../ai/AIProviderCredentialsCard.js";
13
- import { useCallback as d, useEffect as f, useMemo as p, useRef as m, useState as h } from "react";
14
- import { Bookmark as g, PackagePlus as _, Search as v, X as y } from "lucide-react";
11
+ import { PluginInstallModal as u } from "./PluginInstallModal.js";
12
+ import { AIProviderCredentialsCard as d } from "../ai/AIProviderCredentialsCard.js";
13
+ import { useCallback as f, useEffect as p, useMemo as m, useRef as h, useState as g } from "react";
14
+ import { Bookmark as _, PackagePlus as v, Search as ee, X as y } from "lucide-react";
15
15
  import { useSearchParams as b } from "react-router-dom";
16
16
  import { jsx as x, jsxs as S } from "react/jsx-runtime";
17
17
  //#region src/components/agent-manager/AgentManagerChatPane.tsx
18
- var C = ({ sessionId: u, agentId: C }) => {
19
- let [T, E] = h(!1), [D, te] = h(!1), [O, k] = h(""), [A, j] = h(!1), [M, N] = h(!1), [P, F] = h(null), [I, L] = b(), R = I.get("messageId"), z = m(null), B = e(), { agents: V } = t(), H = p(() => V.find((e) => e.id === C) ?? null, [V, C]), U = i((e) => e.sessionStates[u]), W = i((e) => e.addMessage), G = p(() => U?.messages ?? [], [U?.messages]), { active: K } = n(H?.id ?? null), q = p(() => ({
18
+ var C = ({ sessionId: d, agentId: C }) => {
19
+ let [T, E] = g(!1), [D, O] = g(!1), [k, A] = g(""), [j, M] = g(!1), [te, N] = g(!1), [P, F] = g(null), [I, L] = b(), R = I.get("messageId"), z = h(null), B = e(), { agents: V } = t(), H = m(() => V.find((e) => e.id === C) ?? null, [V, C]), U = i((e) => e.sessionStates[d]), W = i((e) => e.addMessage), G = m(() => U?.messages ?? [], [U?.messages]), { active: K } = n(H?.id ?? null), q = m(() => ({
20
20
  id: C,
21
21
  profile: K
22
22
  }), [C, K]), { sendMessage: J, cancelStream: Y, isStreaming: X } = a({
23
23
  agent: q,
24
- sessionId: u
25
- }), { bookmarkedIds: Z, toggle: Q } = o(u), $ = d((e) => {
24
+ sessionId: d
25
+ }), { bookmarkedIds: Z, toggle: Q } = o(d), $ = f((e) => {
26
26
  let t = document.querySelector(`[data-message-id="${e}"]`);
27
27
  t && (t.scrollIntoView({
28
28
  behavior: "smooth",
@@ -30,26 +30,26 @@ var C = ({ sessionId: u, agentId: C }) => {
30
30
  }), F(e), window.setTimeout(() => {
31
31
  F((t) => t === e ? null : t);
32
32
  }, 1500));
33
- }, []), ne = d((e) => {
33
+ }, []), ne = f((e) => {
34
34
  Q({
35
35
  agentId: C,
36
- sessionId: u,
36
+ sessionId: d,
37
37
  messageId: e
38
38
  });
39
39
  }, [
40
40
  Q,
41
41
  C,
42
- u
43
- ]), re = d(() => {
44
- te((e) => (e && k(""), !e));
42
+ d
43
+ ]), re = f(() => {
44
+ O((e) => (e && A(""), !e));
45
45
  }, []);
46
- return f(() => {
46
+ return p(() => {
47
47
  let e = !1;
48
48
  return (async () => {
49
49
  if (!(G.length > 0)) {
50
50
  E(!0);
51
51
  try {
52
- let t = await r(q, u, { types: ["input", "output"] });
52
+ let t = await r(q, d, { types: ["input", "output"] });
53
53
  if (e || t.length === 0) return;
54
54
  let n = /* @__PURE__ */ new Set();
55
55
  for (let e of t) {
@@ -59,7 +59,7 @@ var C = ({ sessionId: u, agentId: C }) => {
59
59
  e.model ?? "",
60
60
  i
61
61
  ].join("\0");
62
- n.has(a) || (n.add(a), W(u, {
62
+ n.has(a) || (n.add(a), W(d, {
63
63
  id: e.id,
64
64
  role: e.type === "input" ? "user" : "assistant",
65
65
  content: t,
@@ -78,7 +78,7 @@ var C = ({ sessionId: u, agentId: C }) => {
78
78
  })(), () => {
79
79
  e = !0;
80
80
  };
81
- }, [C, u]), f(() => {
81
+ }, [C, d]), p(() => {
82
82
  if (!R || T || z.current === R || !G.some((e) => e.id === R)) return;
83
83
  z.current = R, $(R);
84
84
  let e = new URLSearchParams(I);
@@ -105,19 +105,19 @@ var C = ({ sessionId: u, agentId: C }) => {
105
105
  className: `p-1 rounded transition-colors shrink-0 ${D ? "bg-action-primary-bg/10 text-action-primary-text" : "text-text-tertiary hover:text-text-secondary hover:bg-bg-elevated"}`,
106
106
  title: B("vibecontrols.agentManager.chatPane.searchInSession", "Search in session (Ctrl+F)"),
107
107
  "aria-label": B("vibecontrols.agentManager.chatPane.searchInSession", "Search in session"),
108
- children: /* @__PURE__ */ x(v, { className: "w-3.5 h-3.5" })
108
+ children: /* @__PURE__ */ x(ee, { className: "w-3.5 h-3.5" })
109
109
  }), D && /* @__PURE__ */ S("div", {
110
110
  className: "flex items-center gap-1.5 flex-1 min-w-0",
111
111
  children: [/* @__PURE__ */ x("input", {
112
112
  type: "text",
113
- value: O,
114
- onChange: (e) => k(e.target.value),
113
+ value: k,
114
+ onChange: (e) => A(e.target.value),
115
115
  placeholder: B("vibecontrols.agentManager.chatPane.searchPlaceholder", "Search in conversation..."),
116
116
  autoFocus: !0,
117
117
  className: "flex-1 min-w-0 bg-transparent text-xs text-text-primary placeholder:text-text-tertiary focus:outline-none"
118
- }), O && /* @__PURE__ */ x("button", {
118
+ }), k && /* @__PURE__ */ x("button", {
119
119
  type: "button",
120
- onClick: () => k(""),
120
+ onClick: () => A(""),
121
121
  className: "p-0.5 rounded hover:bg-bg-elevated transition-colors",
122
122
  children: /* @__PURE__ */ x(y, { className: "w-3 h-3 text-text-tertiary" })
123
123
  })]
@@ -130,16 +130,16 @@ var C = ({ sessionId: u, agentId: C }) => {
130
130
  title: B("vibecontrols.agentManager.chatPane.bookmarksTooltip", "View bookmarked messages"),
131
131
  "aria-label": B("vibecontrols.agentManager.chatPane.bookmarksLabel", "Bookmarks"),
132
132
  "data-testid": "agent-manager-bookmarks-trigger",
133
- children: /* @__PURE__ */ x(g, { className: "w-3.5 h-3.5" })
133
+ children: /* @__PURE__ */ x(_, { className: "w-3.5 h-3.5" })
134
134
  }),
135
135
  /* @__PURE__ */ S("button", {
136
136
  type: "button",
137
- onClick: () => j(!0),
137
+ onClick: () => M(!0),
138
138
  className: "shrink-0 inline-flex items-center gap-1.5 rounded-md border border-border-default bg-bg-elevated px-2 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-bg-sunken hover:border-action-primary-bg/40",
139
139
  title: B("vibecontrols.agentManager.chatPane.manageProvidersTooltip", "Install or remove AI providers (Claude, Codex, Gemini, OpenCode)"),
140
140
  "aria-label": B("vibecontrols.agentManager.chatPane.manageProviders", "Manage AI providers"),
141
141
  "data-testid": "agent-manager-manage-providers",
142
- children: [/* @__PURE__ */ x(_, { className: "w-3.5 h-3.5" }), /* @__PURE__ */ x("span", {
142
+ children: [/* @__PURE__ */ x(v, { className: "w-3.5 h-3.5" }), /* @__PURE__ */ x("span", {
143
143
  className: "hidden sm:inline",
144
144
  children: B("vibecontrols.agentManager.chatPane.manageProviders", "Manage AI providers")
145
145
  })]
@@ -156,30 +156,30 @@ var C = ({ sessionId: u, agentId: C }) => {
156
156
  messages: G,
157
157
  isStreaming: X,
158
158
  onCancelStream: Y,
159
- searchQuery: O,
159
+ searchQuery: k,
160
160
  bookmarkedIds: Z,
161
161
  onToggleBookmark: ne,
162
162
  flashMessageId: P
163
163
  }),
164
164
  /* @__PURE__ */ x(c, {
165
165
  agentId: C,
166
- sessionId: u,
166
+ sessionId: d,
167
167
  onSend: (e) => void J(e),
168
168
  onCancel: Y,
169
169
  agentTunnelUrl: H?.tunnelUrl ?? void 0,
170
170
  agentApiKey: H?.agentApiKey ?? void 0
171
171
  }),
172
172
  /* @__PURE__ */ x(l, {
173
- open: M,
173
+ open: te,
174
174
  onClose: () => N(!1),
175
- sessionId: u,
175
+ sessionId: d,
176
176
  messages: G,
177
177
  onJump: $
178
178
  }),
179
- /* @__PURE__ */ x(ee, {
180
- open: A,
179
+ /* @__PURE__ */ x(u, {
180
+ open: j,
181
181
  agentId: C,
182
- onClose: () => j(!1)
182
+ onClose: () => M(!1)
183
183
  }),
184
184
  /* @__PURE__ */ x(w, {
185
185
  agentId: C,
@@ -189,8 +189,10 @@ var C = ({ sessionId: u, agentId: C }) => {
189
189
  ]
190
190
  });
191
191
  }, w = ({ agentId: t, agentRef: n, initialProviderId: r }) => {
192
- let a = i((e) => e.sdkAuthPrompt), o = i((e) => e.setSdkAuthPrompt), s = e();
193
- return !a || a.agentId !== t ? null : /* @__PURE__ */ S("div", {
192
+ let a = i((e) => e.sdkAuthPrompt), o = i((e) => e.setSdkAuthPrompt), [s, c] = g(!1), l = e();
193
+ return p(() => {
194
+ a && a.agentId === t && c(!1);
195
+ }, [a, t]), !a || a.agentId !== t ? null : /* @__PURE__ */ S("div", {
194
196
  className: "fixed inset-0 z-50 flex items-center justify-center p-4",
195
197
  children: [/* @__PURE__ */ x("div", {
196
198
  className: "absolute inset-0 bg-bg-overlay",
@@ -198,28 +200,48 @@ var C = ({ sessionId: u, agentId: C }) => {
198
200
  "aria-hidden": !0
199
201
  }), /* @__PURE__ */ S("div", {
200
202
  className: "relative w-full max-w-xl max-h-[85vh] flex flex-col rounded-lg border border-border-default bg-bg-surface shadow-xl overflow-hidden",
201
- children: [/* @__PURE__ */ S("div", {
202
- className: "flex items-center justify-between px-4 py-3 border-b border-border-default bg-bg-surface flex-shrink-0",
203
- children: [/* @__PURE__ */ x("h3", {
204
- className: "text-base font-semibold text-text-primary truncate pr-2",
205
- children: s("vibecontrols.ai.sdkAuth.title", "Configure AI provider credentials")
206
- }), /* @__PURE__ */ x("button", {
207
- type: "button",
208
- onClick: () => o(null),
209
- className: "p-1 rounded hover:bg-bg-sunken transition-colors flex-shrink-0",
210
- "aria-label": s("vibecontrols.common.close", "Close"),
211
- children: /* @__PURE__ */ x(y, { className: "w-5 h-5 text-text-secondary" })
212
- })]
213
- }), /* @__PURE__ */ S("div", {
214
- className: "flex-1 min-h-0 overflow-y-auto px-4 py-3 space-y-3",
215
- children: [/* @__PURE__ */ x("p", {
216
- className: "text-sm text-text-secondary",
217
- children: s("vibecontrols.ai.sdkAuth.body", "The agent does not have a credential configured for this provider. Pick a provider and save one to continue the conversation.")
218
- }), /* @__PURE__ */ x(u, {
219
- agent: n,
220
- initialProviderId: r
221
- })]
222
- })]
203
+ children: [
204
+ /* @__PURE__ */ S("div", {
205
+ className: "flex items-center justify-between px-4 py-3 border-b border-border-default bg-bg-surface flex-shrink-0",
206
+ children: [/* @__PURE__ */ x("h3", {
207
+ className: "text-base font-semibold text-text-primary truncate pr-2",
208
+ children: l("vibecontrols.ai.sdkAuth.title", "Configure AI provider credentials")
209
+ }), /* @__PURE__ */ x("button", {
210
+ type: "button",
211
+ onClick: () => o(null),
212
+ className: "p-1 rounded hover:bg-bg-sunken transition-colors flex-shrink-0",
213
+ "aria-label": l("vibecontrols.common.close", "Close"),
214
+ children: /* @__PURE__ */ x(y, { className: "w-5 h-5 text-text-secondary" })
215
+ })]
216
+ }),
217
+ /* @__PURE__ */ S("div", {
218
+ className: "flex-1 min-h-0 overflow-y-auto px-4 py-3 space-y-3",
219
+ children: [/* @__PURE__ */ x("p", {
220
+ className: "text-sm text-text-secondary",
221
+ children: l("vibecontrols.ai.sdkAuth.body", "The agent doesn't have a credential configured for this provider. Save any one of the listed keys (you don't need all of them) and click Continue to retry.")
222
+ }), /* @__PURE__ */ x(d, {
223
+ agent: n,
224
+ initialProviderId: r,
225
+ onProviderAuthSatisfiedChange: c
226
+ })]
227
+ }),
228
+ /* @__PURE__ */ S("div", {
229
+ className: "flex items-center justify-end gap-2 px-4 py-3 border-t border-border-default bg-bg-surface flex-shrink-0",
230
+ children: [/* @__PURE__ */ x("button", {
231
+ type: "button",
232
+ onClick: () => o(null),
233
+ className: "px-3 py-1.5 rounded-md text-sm text-text-secondary hover:text-text-primary hover:bg-bg-sunken transition-colors",
234
+ children: l("vibecontrols.common.close", "Close")
235
+ }), /* @__PURE__ */ x("button", {
236
+ type: "button",
237
+ onClick: () => o(null),
238
+ disabled: !s,
239
+ title: s ? void 0 : l("vibecontrols.ai.sdkAuth.saveOneFirst", "Save at least one of the listed credentials to continue"),
240
+ className: "px-3 py-1.5 rounded-md text-sm font-medium bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors",
241
+ children: l("vibecontrols.ai.sdkAuth.continue", "Continue")
242
+ })]
243
+ })
244
+ ]
223
245
  })]
224
246
  });
225
247
  };
@@ -1 +1 @@
1
- {"version":3,"file":"AgentManagerChatPane.js","names":[],"sources":["../../../src/components/agent-manager/AgentManagerChatPane.tsx"],"sourcesContent":["/**\n * Main chat interface for a single Agent Manager session.\n *\n * Renders message list + input area, manages the send/receive flow,\n * fetches existing conversation logs on mount, and provides\n * auto-scroll, cancel, and in-session search support.\n */\n\nimport { useEffect, useRef, useState, useCallback, useMemo, type FC } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { Bookmark, PackagePlus, Search, X } from 'lucide-react';\nimport { useAgentManagerStore } from '@/store/agentManagerStore';\nimport { useAgentManagerStreaming } from '@/hooks/useAgentManagerStreaming';\nimport { useCachedInitialData } from '@/hooks/useCachedInitialData';\nimport { fetchSessionLogs } from '@/services/aiApi';\nimport { useActiveAgentProfile } from '@/hooks/useActiveAgentProfile';\nimport { useToggleAgentBookmark } from '@/hooks/useToggleAgentBookmark';\nimport { AgentManagerMessageList } from './AgentManagerMessageList';\nimport { AgentManagerInputArea } from './AgentManagerInputArea';\nimport { AgentManagerBookmarksDrawer } from './AgentManagerBookmarksDrawer';\nimport { PluginInstallModal } from './PluginInstallModal';\nimport { AIProviderCredentialsCard } from '@/components/ai/AIProviderCredentialsCard';\nimport type { AgentManagerMessage } from '@/types/agentManager';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface AgentManagerChatPaneProps {\n sessionId: string;\n agentId: string;\n}\n\nexport const AgentManagerChatPane: FC<AgentManagerChatPaneProps> = ({ sessionId, agentId }) => {\n const [loadingLogs, setLoadingLogs] = useState(false);\n const [searchOpen, setSearchOpen] = useState(false);\n const [searchQuery, setSearchQuery] = useState('');\n // Provider-install modal triggered from the header. Lets users add a\n // claude/codex/gemini/opencode plugin without leaving the conversation.\n const [installModalOpen, setInstallModalOpen] = useState(false);\n const [bookmarksDrawerOpen, setBookmarksDrawerOpen] = useState(false);\n const [flashMessageId, setFlashMessageId] = useState<string | null>(null);\n const [searchParams, setSearchParams] = useSearchParams();\n const pendingScrollMessageId = searchParams.get('messageId');\n const consumedScrollTargetRef = useRef<string | null>(null);\n const tr = useTr();\n\n const { agents } = useCachedInitialData();\n const activeAgent = useMemo(\n () => agents.find((a) => a.id === agentId) ?? null,\n [agents, agentId]\n );\n\n const sessionState = useAgentManagerStore((s) => s.sessionStates[sessionId]);\n const addMessage = useAgentManagerStore((s) => s.addMessage);\n\n const messages = useMemo(() => sessionState?.messages ?? [], [sessionState?.messages]);\n\n const { active: activeProfile } = useActiveAgentProfile(activeAgent?.id ?? null);\n const agentRef = useMemo(\n () => ({ id: agentId, profile: activeProfile }),\n [agentId, activeProfile]\n );\n\n const { sendMessage, cancelStream, isStreaming } = useAgentManagerStreaming({\n agent: agentRef,\n sessionId,\n });\n\n const { bookmarkedIds, toggle: toggleBookmark } = useToggleAgentBookmark(sessionId);\n\n const jumpToMessage = useCallback((messageId: string) => {\n const el = document.querySelector(`[data-message-id=\"${messageId}\"]`);\n if (!el) return;\n el.scrollIntoView({ behavior: 'smooth', block: 'center' });\n setFlashMessageId(messageId);\n window.setTimeout(() => {\n setFlashMessageId((current) => (current === messageId ? null : current));\n }, 1500);\n }, []);\n\n const handleToggleBookmark = useCallback(\n (messageId: string) => {\n void toggleBookmark({ agentId, sessionId, messageId });\n },\n [toggleBookmark, agentId, sessionId]\n );\n\n const toggleSearch = useCallback(() => {\n setSearchOpen((prev) => {\n if (prev) setSearchQuery('');\n return !prev;\n });\n }, []);\n\n // Fetch existing conversation logs on mount\n useEffect(() => {\n let cancelled = false;\n\n const loadExistingLogs = async () => {\n // Only load if session has no messages yet\n if (messages.length > 0) return;\n\n setLoadingLogs(true);\n try {\n const logs = await fetchSessionLogs(agentRef, sessionId, {\n types: ['input', 'output'],\n });\n\n if (cancelled || logs.length === 0) return;\n\n const seenLogKeys = new Set<string>();\n\n // Convert session logs to AgentManagerMessage format. The core AI\n // route owns logging, but older provider builds also wrote the same\n // input/output records; collapse same-second duplicates on restore.\n for (const log of logs) {\n const content = log.message || log.content || '';\n const timestamp = log.timestamp || log.createdAt;\n const timestampSecond = timestamp ? Math.floor(new Date(timestamp).getTime() / 1000) : 0;\n const logKey = [log.type, content, log.model ?? '', timestampSecond].join('\\u0000');\n if (seenLogKeys.has(logKey)) continue;\n seenLogKeys.add(logKey);\n\n const msg: AgentManagerMessage = {\n id: log.id,\n role: log.type === 'input' ? 'user' : 'assistant',\n content,\n timestamp,\n tokens: log.tokenCount ? { input: 0, output: log.tokenCount } : undefined,\n model: log.model,\n };\n addMessage(sessionId, msg);\n }\n } catch {\n // Silent fail -- session may not have logs yet\n } finally {\n if (!cancelled) setLoadingLogs(false);\n }\n };\n\n void loadExistingLogs();\n\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- only run on mount\n }, [agentId, sessionId]);\n\n // Auto-scroll to a message linked via ?messageId=... (from drawer / cross-session page).\n // Waits for logs to finish loading and the target to be in the DOM before scrolling.\n useEffect(() => {\n if (!pendingScrollMessageId || loadingLogs) return;\n if (consumedScrollTargetRef.current === pendingScrollMessageId) return;\n\n const exists = messages.some((m) => m.id === pendingScrollMessageId);\n if (!exists) return;\n\n consumedScrollTargetRef.current = pendingScrollMessageId;\n jumpToMessage(pendingScrollMessageId);\n\n // Strip the query param so a manual refresh doesn't re-trigger the flash.\n const next = new URLSearchParams(searchParams);\n next.delete('messageId');\n setSearchParams(next, { replace: true });\n }, [pendingScrollMessageId, loadingLogs, messages, jumpToMessage, searchParams, setSearchParams]);\n\n return (\n <div className=\"flex flex-col h-full min-h-0\" data-testid=\"agent-manager-chat-pane\">\n {/* In-session search bar.\n Layout: [search controls (flex-1)] ... [Manage AI providers].\n The search input expands inside the LEFT cluster only, so the\n Manage button stays anchored on the right and never shifts. */}\n <div className=\"flex items-center gap-1 px-3 py-1 border-b border-border-default bg-bg-surface flex-shrink-0\">\n <div className=\"flex items-center gap-1.5 min-w-0 flex-1\">\n <button\n type=\"button\"\n onClick={toggleSearch}\n className={`p-1 rounded transition-colors shrink-0 ${\n searchOpen\n ? 'bg-action-primary-bg/10 text-action-primary-text'\n : 'text-text-tertiary hover:text-text-secondary hover:bg-bg-elevated'\n }`}\n title={tr(\n 'vibecontrols.agentManager.chatPane.searchInSession',\n 'Search in session (Ctrl+F)'\n )}\n aria-label={tr(\n 'vibecontrols.agentManager.chatPane.searchInSession',\n 'Search in session'\n )}\n >\n <Search className=\"w-3.5 h-3.5\" />\n </button>\n {searchOpen && (\n <div className=\"flex items-center gap-1.5 flex-1 min-w-0\">\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder={tr(\n 'vibecontrols.agentManager.chatPane.searchPlaceholder',\n 'Search in conversation...'\n )}\n autoFocus\n className=\"flex-1 min-w-0 bg-transparent text-xs text-text-primary placeholder:text-text-tertiary focus:outline-none\"\n />\n {searchQuery && (\n <button\n type=\"button\"\n onClick={() => setSearchQuery('')}\n className=\"p-0.5 rounded hover:bg-bg-elevated transition-colors\"\n >\n <X className=\"w-3 h-3 text-text-tertiary\" />\n </button>\n )}\n </div>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => setBookmarksDrawerOpen(true)}\n className=\"shrink-0 p-1 rounded transition-colors text-text-tertiary hover:text-text-secondary hover:bg-bg-elevated\"\n title={tr(\n 'vibecontrols.agentManager.chatPane.bookmarksTooltip',\n 'View bookmarked messages'\n )}\n aria-label={tr('vibecontrols.agentManager.chatPane.bookmarksLabel', 'Bookmarks')}\n data-testid=\"agent-manager-bookmarks-trigger\"\n >\n <Bookmark className=\"w-3.5 h-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setInstallModalOpen(true)}\n className=\"shrink-0 inline-flex items-center gap-1.5 rounded-md border border-border-default bg-bg-elevated px-2 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-bg-sunken hover:border-action-primary-bg/40\"\n title={tr(\n 'vibecontrols.agentManager.chatPane.manageProvidersTooltip',\n 'Install or remove AI providers (Claude, Codex, Gemini, OpenCode)'\n )}\n aria-label={tr(\n 'vibecontrols.agentManager.chatPane.manageProviders',\n 'Manage AI providers'\n )}\n data-testid=\"agent-manager-manage-providers\"\n >\n <PackagePlus className=\"w-3.5 h-3.5\" />\n <span className=\"hidden sm:inline\">\n {tr('vibecontrols.agentManager.chatPane.manageProviders', 'Manage AI providers')}\n </span>\n </button>\n </div>\n\n {/* Message list */}\n {loadingLogs ? (\n <div className=\"flex-1 flex items-center justify-center\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'vibecontrols.agentManager.chatPane.loadingConversation',\n 'Loading conversation...'\n )}\n </p>\n </div>\n ) : (\n <AgentManagerMessageList\n messages={messages}\n isStreaming={isStreaming}\n onCancelStream={cancelStream}\n searchQuery={searchQuery}\n bookmarkedIds={bookmarkedIds}\n onToggleBookmark={handleToggleBookmark}\n flashMessageId={flashMessageId}\n />\n )}\n\n {/* Rich input area with controls */}\n <AgentManagerInputArea\n agentId={agentId}\n sessionId={sessionId}\n onSend={(text) => void sendMessage(text)}\n onCancel={cancelStream}\n agentTunnelUrl={activeAgent?.tunnelUrl ?? undefined}\n agentApiKey={activeAgent?.agentApiKey ?? undefined}\n />\n\n {/* Per-session bookmarks drawer */}\n <AgentManagerBookmarksDrawer\n open={bookmarksDrawerOpen}\n onClose={() => setBookmarksDrawerOpen(false)}\n sessionId={sessionId}\n messages={messages}\n onJump={jumpToMessage}\n />\n\n {/* Header-triggered provider install/remove. Sits below the chat\n tree so it can render full-screen without disturbing layout. */}\n <PluginInstallModal\n open={installModalOpen}\n agentId={agentId}\n onClose={() => setInstallModalOpen(false)}\n />\n\n {/* SDK auth prompt: opens when the streaming hook detects a missing\n ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN, so the user can save the\n credential inline instead of being told to go look elsewhere.\n The session's currently-selected SDK is forwarded so the picker\n can preselect the right provider. */}\n <SdkAuthPromptModal\n agentId={agentId}\n agentRef={agentRef}\n initialProviderId={sessionState?.sdk ?? undefined}\n />\n </div>\n );\n};\n\ninterface SdkAuthPromptModalProps {\n agentId: string;\n agentRef: { id: string; profile: string };\n initialProviderId?: string;\n}\n\nconst SdkAuthPromptModal: FC<SdkAuthPromptModalProps> = ({\n agentId,\n agentRef,\n initialProviderId,\n}) => {\n const sdkAuthPrompt = useAgentManagerStore((s) => s.sdkAuthPrompt);\n const setSdkAuthPrompt = useAgentManagerStore((s) => s.setSdkAuthPrompt);\n const tr = useTr();\n\n if (!sdkAuthPrompt || sdkAuthPrompt.agentId !== agentId) return null;\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\">\n <div\n className=\"absolute inset-0 bg-bg-overlay\"\n onClick={() => setSdkAuthPrompt(null)}\n aria-hidden\n />\n {/* Constrain height to the viewport and let the inner body scroll so the\n modal never escapes the screen no matter how many providers/fields\n render inside AIProviderCredentialsCard. The header stays sticky so\n the close button is always reachable. */}\n <div className=\"relative w-full max-w-xl max-h-[85vh] flex flex-col rounded-lg border border-border-default bg-bg-surface shadow-xl overflow-hidden\">\n <div className=\"flex items-center justify-between px-4 py-3 border-b border-border-default bg-bg-surface flex-shrink-0\">\n <h3 className=\"text-base font-semibold text-text-primary truncate pr-2\">\n {tr('vibecontrols.ai.sdkAuth.title', 'Configure AI provider credentials')}\n </h3>\n <button\n type=\"button\"\n onClick={() => setSdkAuthPrompt(null)}\n className=\"p-1 rounded hover:bg-bg-sunken transition-colors flex-shrink-0\"\n aria-label={tr('vibecontrols.common.close', 'Close')}\n >\n <X className=\"w-5 h-5 text-text-secondary\" />\n </button>\n </div>\n <div className=\"flex-1 min-h-0 overflow-y-auto px-4 py-3 space-y-3\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'vibecontrols.ai.sdkAuth.body',\n 'The agent does not have a credential configured for this provider. Pick a provider and save one to continue the conversation.'\n )}\n </p>\n <AIProviderCredentialsCard agent={agentRef} initialProviderId={initialProviderId} />\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AgentManagerChatPane.js","names":[],"sources":["../../../src/components/agent-manager/AgentManagerChatPane.tsx"],"sourcesContent":["/**\n * Main chat interface for a single Agent Manager session.\n *\n * Renders message list + input area, manages the send/receive flow,\n * fetches existing conversation logs on mount, and provides\n * auto-scroll, cancel, and in-session search support.\n */\n\nimport { useEffect, useRef, useState, useCallback, useMemo, type FC } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { Bookmark, PackagePlus, Search, X } from 'lucide-react';\nimport { useAgentManagerStore } from '@/store/agentManagerStore';\nimport { useAgentManagerStreaming } from '@/hooks/useAgentManagerStreaming';\nimport { useCachedInitialData } from '@/hooks/useCachedInitialData';\nimport { fetchSessionLogs } from '@/services/aiApi';\nimport { useActiveAgentProfile } from '@/hooks/useActiveAgentProfile';\nimport { useToggleAgentBookmark } from '@/hooks/useToggleAgentBookmark';\nimport { AgentManagerMessageList } from './AgentManagerMessageList';\nimport { AgentManagerInputArea } from './AgentManagerInputArea';\nimport { AgentManagerBookmarksDrawer } from './AgentManagerBookmarksDrawer';\nimport { PluginInstallModal } from './PluginInstallModal';\nimport { AIProviderCredentialsCard } from '@/components/ai/AIProviderCredentialsCard';\nimport type { AgentManagerMessage } from '@/types/agentManager';\nimport { useTr } from '../../shared/hooks/useTr';\n\ninterface AgentManagerChatPaneProps {\n sessionId: string;\n agentId: string;\n}\n\nexport const AgentManagerChatPane: FC<AgentManagerChatPaneProps> = ({ sessionId, agentId }) => {\n const [loadingLogs, setLoadingLogs] = useState(false);\n const [searchOpen, setSearchOpen] = useState(false);\n const [searchQuery, setSearchQuery] = useState('');\n // Provider-install modal triggered from the header. Lets users add a\n // claude/codex/gemini/opencode plugin without leaving the conversation.\n const [installModalOpen, setInstallModalOpen] = useState(false);\n const [bookmarksDrawerOpen, setBookmarksDrawerOpen] = useState(false);\n const [flashMessageId, setFlashMessageId] = useState<string | null>(null);\n const [searchParams, setSearchParams] = useSearchParams();\n const pendingScrollMessageId = searchParams.get('messageId');\n const consumedScrollTargetRef = useRef<string | null>(null);\n const tr = useTr();\n\n const { agents } = useCachedInitialData();\n const activeAgent = useMemo(\n () => agents.find((a) => a.id === agentId) ?? null,\n [agents, agentId]\n );\n\n const sessionState = useAgentManagerStore((s) => s.sessionStates[sessionId]);\n const addMessage = useAgentManagerStore((s) => s.addMessage);\n\n const messages = useMemo(() => sessionState?.messages ?? [], [sessionState?.messages]);\n\n const { active: activeProfile } = useActiveAgentProfile(activeAgent?.id ?? null);\n const agentRef = useMemo(\n () => ({ id: agentId, profile: activeProfile }),\n [agentId, activeProfile]\n );\n\n const { sendMessage, cancelStream, isStreaming } = useAgentManagerStreaming({\n agent: agentRef,\n sessionId,\n });\n\n const { bookmarkedIds, toggle: toggleBookmark } = useToggleAgentBookmark(sessionId);\n\n const jumpToMessage = useCallback((messageId: string) => {\n const el = document.querySelector(`[data-message-id=\"${messageId}\"]`);\n if (!el) return;\n el.scrollIntoView({ behavior: 'smooth', block: 'center' });\n setFlashMessageId(messageId);\n window.setTimeout(() => {\n setFlashMessageId((current) => (current === messageId ? null : current));\n }, 1500);\n }, []);\n\n const handleToggleBookmark = useCallback(\n (messageId: string) => {\n void toggleBookmark({ agentId, sessionId, messageId });\n },\n [toggleBookmark, agentId, sessionId]\n );\n\n const toggleSearch = useCallback(() => {\n setSearchOpen((prev) => {\n if (prev) setSearchQuery('');\n return !prev;\n });\n }, []);\n\n // Fetch existing conversation logs on mount\n useEffect(() => {\n let cancelled = false;\n\n const loadExistingLogs = async () => {\n // Only load if session has no messages yet\n if (messages.length > 0) return;\n\n setLoadingLogs(true);\n try {\n const logs = await fetchSessionLogs(agentRef, sessionId, {\n types: ['input', 'output'],\n });\n\n if (cancelled || logs.length === 0) return;\n\n const seenLogKeys = new Set<string>();\n\n // Convert session logs to AgentManagerMessage format. The core AI\n // route owns logging, but older provider builds also wrote the same\n // input/output records; collapse same-second duplicates on restore.\n for (const log of logs) {\n const content = log.message || log.content || '';\n const timestamp = log.timestamp || log.createdAt;\n const timestampSecond = timestamp ? Math.floor(new Date(timestamp).getTime() / 1000) : 0;\n const logKey = [log.type, content, log.model ?? '', timestampSecond].join('\\u0000');\n if (seenLogKeys.has(logKey)) continue;\n seenLogKeys.add(logKey);\n\n const msg: AgentManagerMessage = {\n id: log.id,\n role: log.type === 'input' ? 'user' : 'assistant',\n content,\n timestamp,\n tokens: log.tokenCount ? { input: 0, output: log.tokenCount } : undefined,\n model: log.model,\n };\n addMessage(sessionId, msg);\n }\n } catch {\n // Silent fail -- session may not have logs yet\n } finally {\n if (!cancelled) setLoadingLogs(false);\n }\n };\n\n void loadExistingLogs();\n\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- only run on mount\n }, [agentId, sessionId]);\n\n // Auto-scroll to a message linked via ?messageId=... (from drawer / cross-session page).\n // Waits for logs to finish loading and the target to be in the DOM before scrolling.\n useEffect(() => {\n if (!pendingScrollMessageId || loadingLogs) return;\n if (consumedScrollTargetRef.current === pendingScrollMessageId) return;\n\n const exists = messages.some((m) => m.id === pendingScrollMessageId);\n if (!exists) return;\n\n consumedScrollTargetRef.current = pendingScrollMessageId;\n jumpToMessage(pendingScrollMessageId);\n\n // Strip the query param so a manual refresh doesn't re-trigger the flash.\n const next = new URLSearchParams(searchParams);\n next.delete('messageId');\n setSearchParams(next, { replace: true });\n }, [pendingScrollMessageId, loadingLogs, messages, jumpToMessage, searchParams, setSearchParams]);\n\n return (\n <div className=\"flex flex-col h-full min-h-0\" data-testid=\"agent-manager-chat-pane\">\n {/* In-session search bar.\n Layout: [search controls (flex-1)] ... [Manage AI providers].\n The search input expands inside the LEFT cluster only, so the\n Manage button stays anchored on the right and never shifts. */}\n <div className=\"flex items-center gap-1 px-3 py-1 border-b border-border-default bg-bg-surface flex-shrink-0\">\n <div className=\"flex items-center gap-1.5 min-w-0 flex-1\">\n <button\n type=\"button\"\n onClick={toggleSearch}\n className={`p-1 rounded transition-colors shrink-0 ${\n searchOpen\n ? 'bg-action-primary-bg/10 text-action-primary-text'\n : 'text-text-tertiary hover:text-text-secondary hover:bg-bg-elevated'\n }`}\n title={tr(\n 'vibecontrols.agentManager.chatPane.searchInSession',\n 'Search in session (Ctrl+F)'\n )}\n aria-label={tr(\n 'vibecontrols.agentManager.chatPane.searchInSession',\n 'Search in session'\n )}\n >\n <Search className=\"w-3.5 h-3.5\" />\n </button>\n {searchOpen && (\n <div className=\"flex items-center gap-1.5 flex-1 min-w-0\">\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder={tr(\n 'vibecontrols.agentManager.chatPane.searchPlaceholder',\n 'Search in conversation...'\n )}\n autoFocus\n className=\"flex-1 min-w-0 bg-transparent text-xs text-text-primary placeholder:text-text-tertiary focus:outline-none\"\n />\n {searchQuery && (\n <button\n type=\"button\"\n onClick={() => setSearchQuery('')}\n className=\"p-0.5 rounded hover:bg-bg-elevated transition-colors\"\n >\n <X className=\"w-3 h-3 text-text-tertiary\" />\n </button>\n )}\n </div>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => setBookmarksDrawerOpen(true)}\n className=\"shrink-0 p-1 rounded transition-colors text-text-tertiary hover:text-text-secondary hover:bg-bg-elevated\"\n title={tr(\n 'vibecontrols.agentManager.chatPane.bookmarksTooltip',\n 'View bookmarked messages'\n )}\n aria-label={tr('vibecontrols.agentManager.chatPane.bookmarksLabel', 'Bookmarks')}\n data-testid=\"agent-manager-bookmarks-trigger\"\n >\n <Bookmark className=\"w-3.5 h-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setInstallModalOpen(true)}\n className=\"shrink-0 inline-flex items-center gap-1.5 rounded-md border border-border-default bg-bg-elevated px-2 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-bg-sunken hover:border-action-primary-bg/40\"\n title={tr(\n 'vibecontrols.agentManager.chatPane.manageProvidersTooltip',\n 'Install or remove AI providers (Claude, Codex, Gemini, OpenCode)'\n )}\n aria-label={tr(\n 'vibecontrols.agentManager.chatPane.manageProviders',\n 'Manage AI providers'\n )}\n data-testid=\"agent-manager-manage-providers\"\n >\n <PackagePlus className=\"w-3.5 h-3.5\" />\n <span className=\"hidden sm:inline\">\n {tr('vibecontrols.agentManager.chatPane.manageProviders', 'Manage AI providers')}\n </span>\n </button>\n </div>\n\n {/* Message list */}\n {loadingLogs ? (\n <div className=\"flex-1 flex items-center justify-center\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'vibecontrols.agentManager.chatPane.loadingConversation',\n 'Loading conversation...'\n )}\n </p>\n </div>\n ) : (\n <AgentManagerMessageList\n messages={messages}\n isStreaming={isStreaming}\n onCancelStream={cancelStream}\n searchQuery={searchQuery}\n bookmarkedIds={bookmarkedIds}\n onToggleBookmark={handleToggleBookmark}\n flashMessageId={flashMessageId}\n />\n )}\n\n {/* Rich input area with controls */}\n <AgentManagerInputArea\n agentId={agentId}\n sessionId={sessionId}\n onSend={(text) => void sendMessage(text)}\n onCancel={cancelStream}\n agentTunnelUrl={activeAgent?.tunnelUrl ?? undefined}\n agentApiKey={activeAgent?.agentApiKey ?? undefined}\n />\n\n {/* Per-session bookmarks drawer */}\n <AgentManagerBookmarksDrawer\n open={bookmarksDrawerOpen}\n onClose={() => setBookmarksDrawerOpen(false)}\n sessionId={sessionId}\n messages={messages}\n onJump={jumpToMessage}\n />\n\n {/* Header-triggered provider install/remove. Sits below the chat\n tree so it can render full-screen without disturbing layout. */}\n <PluginInstallModal\n open={installModalOpen}\n agentId={agentId}\n onClose={() => setInstallModalOpen(false)}\n />\n\n {/* SDK auth prompt: opens when the streaming hook detects a missing\n ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN, so the user can save the\n credential inline instead of being told to go look elsewhere.\n The session's currently-selected SDK is forwarded so the picker\n can preselect the right provider. */}\n <SdkAuthPromptModal\n agentId={agentId}\n agentRef={agentRef}\n initialProviderId={sessionState?.sdk ?? undefined}\n />\n </div>\n );\n};\n\ninterface SdkAuthPromptModalProps {\n agentId: string;\n agentRef: { id: string; profile: string };\n initialProviderId?: string;\n}\n\nconst SdkAuthPromptModal: FC<SdkAuthPromptModalProps> = ({\n agentId,\n agentRef,\n initialProviderId,\n}) => {\n const sdkAuthPrompt = useAgentManagerStore((s) => s.sdkAuthPrompt);\n const setSdkAuthPrompt = useAgentManagerStore((s) => s.setSdkAuthPrompt);\n // Track whether the inner card has at least one primary credential\n // saved on the agent. The Continue button is gated on this so the user\n // can't dismiss the modal without actually persisting a key — but the\n // moment they save *any* of the either-or options it lights up.\n const [authSatisfied, setAuthSatisfied] = useState(false);\n const tr = useTr();\n\n // Reset the satisfied flag whenever the modal re-opens for a fresh\n // prompt (different agent / different session). Without this, dismissing\n // and re-opening would leave the flag stuck at its previous value.\n useEffect(() => {\n if (sdkAuthPrompt && sdkAuthPrompt.agentId === agentId) {\n setAuthSatisfied(false);\n }\n }, [sdkAuthPrompt, agentId]);\n\n if (!sdkAuthPrompt || sdkAuthPrompt.agentId !== agentId) return null;\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\">\n <div\n className=\"absolute inset-0 bg-bg-overlay\"\n onClick={() => setSdkAuthPrompt(null)}\n aria-hidden\n />\n {/* Constrain height to the viewport and let the inner body scroll so the\n modal never escapes the screen no matter how many providers/fields\n render inside AIProviderCredentialsCard. The header + footer stay\n sticky so the close + Continue buttons are always reachable. */}\n <div className=\"relative w-full max-w-xl max-h-[85vh] flex flex-col rounded-lg border border-border-default bg-bg-surface shadow-xl overflow-hidden\">\n <div className=\"flex items-center justify-between px-4 py-3 border-b border-border-default bg-bg-surface flex-shrink-0\">\n <h3 className=\"text-base font-semibold text-text-primary truncate pr-2\">\n {tr('vibecontrols.ai.sdkAuth.title', 'Configure AI provider credentials')}\n </h3>\n <button\n type=\"button\"\n onClick={() => setSdkAuthPrompt(null)}\n className=\"p-1 rounded hover:bg-bg-sunken transition-colors flex-shrink-0\"\n aria-label={tr('vibecontrols.common.close', 'Close')}\n >\n <X className=\"w-5 h-5 text-text-secondary\" />\n </button>\n </div>\n <div className=\"flex-1 min-h-0 overflow-y-auto px-4 py-3 space-y-3\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'vibecontrols.ai.sdkAuth.body',\n \"The agent doesn't have a credential configured for this provider. Save any one of the listed keys (you don't need all of them) and click Continue to retry.\"\n )}\n </p>\n <AIProviderCredentialsCard\n agent={agentRef}\n initialProviderId={initialProviderId}\n onProviderAuthSatisfiedChange={setAuthSatisfied}\n />\n </div>\n <div className=\"flex items-center justify-end gap-2 px-4 py-3 border-t border-border-default bg-bg-surface flex-shrink-0\">\n <button\n type=\"button\"\n onClick={() => setSdkAuthPrompt(null)}\n className=\"px-3 py-1.5 rounded-md text-sm text-text-secondary hover:text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n {tr('vibecontrols.common.close', 'Close')}\n </button>\n <button\n type=\"button\"\n onClick={() => setSdkAuthPrompt(null)}\n disabled={!authSatisfied}\n title={\n authSatisfied\n ? undefined\n : tr(\n 'vibecontrols.ai.sdkAuth.saveOneFirst',\n 'Save at least one of the listed credentials to continue'\n )\n }\n className=\"px-3 py-1.5 rounded-md text-sm font-medium bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {tr('vibecontrols.ai.sdkAuth.continue', 'Continue')}\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -84,19 +84,19 @@ var _ = [
84
84
  }, { key: "OLLAMA_HOST" }]
85
85
  }
86
86
  ];
87
- function v({ agent: t, providerIds: n, initialProviderId: r }) {
88
- let i = e(), a = n ? _.filter((e) => n.includes(e.providerId)) : _, [l, u] = o(() => r && a.some((e) => e.providerId === r) ? r : a.length === 1 ? a[0]?.providerId ?? null : null), f = a.find((e) => e.providerId === l) ?? null;
89
- return f ? /* @__PURE__ */ g("div", {
87
+ function v({ agent: t, providerIds: n, initialProviderId: r, onProviderAuthSatisfiedChange: i }) {
88
+ let a = e(), l = n ? _.filter((e) => n.includes(e.providerId)) : _, [u, f] = o(() => r && l.some((e) => e.providerId === r) ? r : l.length === 1 ? l[0]?.providerId ?? null : null), p = l.find((e) => e.providerId === u) ?? null;
89
+ return p ? /* @__PURE__ */ g("div", {
90
90
  className: "space-y-3",
91
91
  children: [/* @__PURE__ */ g("div", {
92
92
  className: "flex items-center gap-2",
93
93
  children: [
94
- a.length > 1 ? /* @__PURE__ */ g("button", {
94
+ l.length > 1 ? /* @__PURE__ */ g("button", {
95
95
  type: "button",
96
- onClick: () => u(null),
96
+ onClick: () => f(null),
97
97
  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",
98
- "aria-label": i("ai.providerCredentials.back", "Back to providers"),
99
- children: [/* @__PURE__ */ h(s, { className: "h-3.5 w-3.5" }), i("ai.providerCredentials.back", "Back")]
98
+ "aria-label": a("ai.providerCredentials.back", "Back to providers"),
99
+ children: [/* @__PURE__ */ h(s, { className: "h-3.5 w-3.5" }), a("ai.providerCredentials.back", "Back")]
100
100
  }) : null,
101
101
  /* @__PURE__ */ h("div", {
102
102
  className: "rounded-lg bg-bg-sunken p-2 text-text-secondary",
@@ -106,16 +106,17 @@ function v({ agent: t, providerIds: n, initialProviderId: r }) {
106
106
  className: "min-w-0",
107
107
  children: [/* @__PURE__ */ h("h3", {
108
108
  className: "text-sm font-semibold text-text-primary truncate",
109
- children: f.label
109
+ children: p.label
110
110
  }), /* @__PURE__ */ h("p", {
111
111
  className: "text-xs text-text-secondary",
112
- children: i("ai.providerCredentials.scopedDescription", "Keys are stored on the agent and read at session start.")
112
+ children: a("ai.providerCredentials.scopedDescription", "Keys are stored on the agent and read at session start.")
113
113
  })]
114
114
  })
115
115
  ]
116
- }), /* @__PURE__ */ h(y, {
116
+ }), /* @__PURE__ */ h(b, {
117
117
  agent: t,
118
- provider: f
118
+ provider: p,
119
+ onAuthSatisfiedChange: i
119
120
  })]
120
121
  }) : /* @__PURE__ */ g("div", {
121
122
  className: "space-y-3",
@@ -128,17 +129,17 @@ function v({ agent: t, providerIds: n, initialProviderId: r }) {
128
129
  className: "space-y-1",
129
130
  children: [/* @__PURE__ */ h("h3", {
130
131
  className: "text-sm font-semibold text-text-primary",
131
- children: i("ai.providerCredentials.pickTitle", "Choose a provider")
132
+ children: a("ai.providerCredentials.pickTitle", "Choose a provider")
132
133
  }), /* @__PURE__ */ h("p", {
133
134
  className: "text-sm text-text-secondary",
134
- children: i("ai.providerCredentials.pickDescription", "Pick the provider whose credentials you want to manage. Keys are stored on the agent and read at session start.")
135
+ children: a("ai.providerCredentials.pickDescription", "Pick the provider whose credentials you want to manage. Keys are stored on the agent and read at session start.")
135
136
  })]
136
137
  })]
137
138
  }), /* @__PURE__ */ h("div", {
138
139
  className: "grid grid-cols-1 sm:grid-cols-2 gap-2",
139
- children: a.map((e) => /* @__PURE__ */ g("button", {
140
+ children: l.map((e) => /* @__PURE__ */ g("button", {
140
141
  type: "button",
141
- onClick: () => u(e.providerId),
142
+ onClick: () => f(e.providerId),
142
143
  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",
143
144
  children: [/* @__PURE__ */ g("div", {
144
145
  className: "min-w-0",
@@ -154,83 +155,105 @@ function v({ agent: t, providerIds: n, initialProviderId: r }) {
154
155
  })]
155
156
  });
156
157
  }
157
- function y({ agent: s, provider: c }) {
158
- let l = e(), [u, d] = o({}), [p, m] = o({}), [_, v] = o(!0), [y, x] = o(null), [S, C] = o(null), [w, T] = o(null), E = i(async () => {
159
- v(!0), C(null);
158
+ function y(e) {
159
+ return e.primary ?? e.secret ?? !1;
160
+ }
161
+ function b({ agent: s, provider: c, onAuthSatisfiedChange: l }) {
162
+ let u = e(), [d, p] = o({}), [m, _] = o({}), [v, b] = o({}), [S, C] = o(!0), [w, T] = o(null), [E, D] = o(null), [O, k] = o(null), A = c.fields.filter(y).map((e) => e.key), j = A.filter((e) => (m[e] ?? "").trim().length > 0), M = j.length > 0;
163
+ a(() => {
164
+ l?.(M);
165
+ }, [M, l]);
166
+ let N = i(async () => {
167
+ C(!0), D(null);
160
168
  try {
161
- let e = await Promise.all(c.fields.map(async (e) => [e.key, await n(s, e.key) ?? ""]));
162
- d(Object.fromEntries(e));
169
+ let e = await Promise.all(c.fields.map(async (e) => [e.key, await n(s, e.key) ?? ""])), t = Object.fromEntries(e);
170
+ p(t), _(t);
163
171
  } catch (e) {
164
- C(e instanceof Error ? e.message : l("ai.providerCredentials.loadFailed", "Failed to load provider credentials"));
172
+ D(e instanceof Error ? e.message : u("ai.providerCredentials.loadFailed", "Failed to load provider credentials"));
165
173
  } finally {
166
- v(!1);
174
+ C(!1);
167
175
  }
168
176
  }, [
169
177
  s,
170
178
  c.fields,
171
- l
179
+ u
172
180
  ]);
173
181
  a(() => {
174
- E();
175
- }, [E]);
176
- let D = i(async (e, n) => {
177
- x(e), C(null), T(null);
182
+ N();
183
+ }, [N]);
184
+ let P = i(async (e, n) => {
185
+ T(e), D(null), k(null);
186
+ let i = n.trim();
178
187
  try {
179
- n.trim() ? await r(s, e, n.trim()) : await t(s, e), T(n.trim() ? l("ai.providerCredentials.saved", `${e} saved`) : l("ai.providerCredentials.cleared", `${e} cleared`));
188
+ i ? await r(s, e, i) : await t(s, e), _((t) => ({
189
+ ...t,
190
+ [e]: i
191
+ })), k(i ? u("ai.providerCredentials.saved", `${e} saved`) : u("ai.providerCredentials.cleared", `${e} cleared`));
180
192
  } catch (e) {
181
- C(e instanceof Error ? e.message : l("ai.providerCredentials.saveFailed", "Failed to save credential"));
193
+ D(e instanceof Error ? e.message : u("ai.providerCredentials.saveFailed", "Failed to save credential"));
182
194
  } finally {
183
- x(null);
195
+ T(null);
184
196
  }
185
- }, [s, l]);
197
+ }, [s, u]);
186
198
  return /* @__PURE__ */ g("div", {
187
199
  className: "rounded-xl border border-border-default bg-bg-surface p-4 space-y-3",
188
200
  children: [
189
201
  /* @__PURE__ */ g("div", {
190
202
  className: "space-y-1",
191
- children: [/* @__PURE__ */ h("h4", {
192
- className: "text-sm font-semibold text-text-primary",
193
- children: c.label
194
- }), /* @__PURE__ */ h("p", {
195
- className: "text-xs text-text-secondary",
196
- children: c.description
197
- })]
203
+ children: [
204
+ /* @__PURE__ */ h("h4", {
205
+ className: "text-sm font-semibold text-text-primary",
206
+ children: c.label
207
+ }),
208
+ /* @__PURE__ */ h("p", {
209
+ className: "text-xs text-text-secondary",
210
+ children: c.description
211
+ }),
212
+ A.length > 1 ? /* @__PURE__ */ h("p", {
213
+ className: "text-xs text-text-tertiary",
214
+ children: u("ai.providerCredentials.eitherOr", `Save any one of: ${A.join(" · ")}. You don't need to fill all of them.`)
215
+ }) : null
216
+ ]
198
217
  }),
199
- S ? /* @__PURE__ */ h("div", {
218
+ M ? /* @__PURE__ */ h("div", {
219
+ className: "rounded-lg border border-status-success-border bg-status-success-bg px-3 py-2 text-xs text-status-success-text",
220
+ children: u("ai.providerCredentials.authSatisfied", `${c.label} is ready. The agent will use ${j.join(" / ")} on the next SDK session.`)
221
+ }) : null,
222
+ E ? /* @__PURE__ */ h("div", {
200
223
  className: "rounded-lg border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text",
201
- children: S
224
+ children: E
202
225
  }) : null,
203
- w ? /* @__PURE__ */ h("div", {
226
+ O ? /* @__PURE__ */ h("div", {
204
227
  className: "rounded-lg border border-status-success-border bg-status-success-bg px-3 py-2 text-sm text-status-success-text",
205
- children: w
228
+ children: O
206
229
  }) : null,
207
- _ ? /* @__PURE__ */ g("div", {
230
+ S ? /* @__PURE__ */ g("div", {
208
231
  className: "flex items-center gap-2 text-sm text-text-secondary",
209
- children: [/* @__PURE__ */ h(f, { className: "h-4 w-4 animate-spin" }), l("ai.providerCredentials.loading", "Loading…")]
232
+ children: [/* @__PURE__ */ h(f, { className: "h-4 w-4 animate-spin" }), u("ai.providerCredentials.loading", "Loading…")]
210
233
  }) : /* @__PURE__ */ h("div", {
211
234
  className: "space-y-3",
212
- children: c.fields.map((e) => /* @__PURE__ */ h(b, {
235
+ children: c.fields.map((e) => /* @__PURE__ */ h(x, {
213
236
  label: e.key,
214
237
  secret: e.secret ?? !1,
215
- value: u[e.key] ?? "",
216
- onChange: (t) => d((n) => ({
238
+ value: d[e.key] ?? "",
239
+ onChange: (t) => p((n) => ({
217
240
  ...n,
218
241
  [e.key]: t
219
242
  })),
220
- saving: y === e.key,
221
- visible: p[e.key] ?? !1,
222
- onToggleVisible: () => m((t) => ({
243
+ saving: w === e.key,
244
+ visible: v[e.key] ?? !1,
245
+ onToggleVisible: () => b((t) => ({
223
246
  ...t,
224
247
  [e.key]: !t[e.key]
225
248
  })),
226
- onSave: () => void D(e.key, u[e.key] ?? ""),
227
- onClear: () => void D(e.key, "")
249
+ onSave: () => void P(e.key, d[e.key] ?? ""),
250
+ onClear: () => void P(e.key, "")
228
251
  }, e.key))
229
252
  })
230
253
  ]
231
254
  });
232
255
  }
233
- function b({ label: e, secret: t, value: n, onChange: r, saving: i, visible: a, onToggleVisible: o, onSave: s, onClear: c }) {
256
+ function x({ label: e, secret: t, value: n, onChange: r, saving: i, visible: a, onToggleVisible: o, onSave: s, onClear: c }) {
234
257
  return /* @__PURE__ */ g("div", {
235
258
  className: "space-y-2 rounded-lg border border-border-default bg-bg-primary p-3",
236
259
  children: [/* @__PURE__ */ h("label", {
@@ -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}\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\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}: 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 agent={agent} provider={selected} />\n </div>\n );\n}\n\nfunction ProviderCard({ agent, provider }: { agent: AgentRef; provider: AIProviderCredentialDef }) {\n const tr = useTr();\n const [values, setValues] = 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 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 setValues(Object.fromEntries(entries));\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 try {\n if (value.trim()) {\n await setAgentConfigValue(agent, key, value.trim());\n } else {\n await deleteAgentConfigValue(agent, key);\n }\n setSuccess(\n value.trim()\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 </div>\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":";;;;;;AAqEA,IAAa,IAAqD;CAChE;EACE,YAAY;EACZ,OAAO;EACP,aACE;EACF,QAAQ,CACN;GAAE,KAAK;GAAqB,QAAQ;GAAM,EAC1C;GAAE,KAAK;GAAwB,QAAQ;GAAM,CAC9C;EACF;CACD;EACE,YAAY;EACZ,OAAO;EACP,aACE;EACF,QAAQ,CAAC;GAAE,KAAK;GAAkB,QAAQ;GAAM,CAAC;EAClD;CACD;EACE,YAAY;EACZ,OAAO;EACP,aACE;EACF,QAAQ,CACN;GAAE,KAAK;GAAkB,QAAQ;GAAM,EACvC;GAAE,KAAK;GAAkB,QAAQ;GAAM,CACxC;EACF;CACD;EACE,YAAY;EACZ,OAAO;EACP,aACE;EACF,QAAQ,CAAC;GAAE,KAAK;GAAyB,QAAQ;GAAM,EAAE,EAAE,KAAK,0BAA0B,CAAC;EAC5F;CACD;EACE,YAAY;EACZ,OAAO;EACP,aACE;EACF,QAAQ,CAAC;GAAE,KAAK;GAAkB,QAAQ;GAAM,CAAC;EAClD;CACD;EACE,YAAY;EACZ,OAAO;EACP,aAAa;EACb,QAAQ,CAAC;GAAE,KAAK;GAAsB,QAAQ;GAAM,CAAC;EACtD;CACD;EACE,YAAY;EACZ,OAAO;EACP,aACE;EACF,QAAQ,CAAC;GAAE,KAAK;GAAmB,QAAQ;GAAM,CAAC;EACnD;CACD;EACE,YAAY;EACZ,OAAO;EACP,aACE;EACF,QAAQ,CAAC;GAAE,KAAK;GAAkB,QAAQ;GAAM,EAAE,EAAE,KAAK,eAAe,CAAC;EAC1E;CACF;AAkCD,SAAgB,EAA0B,EACxC,UACA,gBACA,sBACA,oCACiC;CACjC,IAAM,IAAK,GAAO,EACZ,IAAY,IACd,EAAwB,QAAQ,MAAM,EAAY,SAAS,EAAE,WAAW,CAAC,GACzE,GAIE,CAAC,GAAY,KAAiB,QAC9B,KAAqB,EAAU,MAAM,MAAM,EAAE,eAAe,EAAkB,GACzE,IAEF,EAAU,WAAW,IAAK,EAAU,IAAI,cAAc,OAAQ,KACrE,EAEI,IAAW,EAAU,MAAM,MAAM,EAAE,eAAe,EAAW,IAAI;AA2CvE,QAzCK,IA0CH,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,EAAU,SAAS,IAClB,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAc,KAAK;KAClC,WAAU;KACV,cAAY,EAAG,+BAA+B,oBAAoB;eAJpE,CAME,kBAAC,GAAD,EAAW,WAAU,eAAgB,CAAA,EACpC,EAAG,+BAA+B,OAAO,CACnC;SACP;IACJ,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;KAC5B,CAAA;IACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBAAoD,EAAS;MAAW,CAAA,EACtF,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,4CACA,0DACD;MACC,CAAA,CACA;;IACF;MACN,kBAAC,GAAD;GACS;GACP,UAAU;GACV,uBAAuB;GACvB,CAAA,CACE;MAvEJ,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;IAC5B,CAAA,EACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,oCAAoC,oBAAoB;KACzD,CAAA,EACL,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,0CACA,kHACD;KACC,CAAA,CACA;MACF;MACN,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAU,KAAK,MACd,kBAAC,UAAD;IAEE,MAAK;IACL,eAAe,EAAc,EAAE,WAAW;IAC1C,WAAU;cAJZ,CAME,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAkD,EAAE;MAAY,CAAA,EAC/E,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAE,OAAO,KAAK,MAAM,EAAE,IAAI,CAAC,KAAK,KAAK;MAClC,CAAA,CACF;QACN,kBAAC,GAAD,EAAc,WAAU,6CAA8C,CAAA,CAC/D;MAZF,EAAE,WAYA,CACT;GACE,CAAA,CACF;;;AAwCZ,SAAS,EAAe,GAAuD;AAE7E,QAAO,EAAE,WAAY,EAAE,UAAU;;AAGnC,SAAS,EAAa,EACpB,UACA,aACA,4BAKC;CACD,IAAM,IAAK,GAAO,EACZ,CAAC,GAAQ,KAAa,EAAiC,EAAE,CAAC,EAK1D,CAAC,GAAa,KAAkB,EAAiC,EAAE,CAAC,EACpE,CAAC,GAAS,KAAc,EAAkC,EAAE,CAAC,EAC7D,CAAC,GAAS,KAAc,EAAS,GAAK,EACtC,CAAC,GAAQ,KAAa,EAAwB,KAAK,EACnD,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,CAAC,GAAS,KAAc,EAAwB,KAAK,EAErD,IAAc,EAAS,OAAO,OAAO,EAAe,CAAC,KAAK,MAAM,EAAE,IAAI,EACtE,IAAuB,EAAY,QAAQ,OAAO,EAAY,MAAM,IAAI,MAAM,CAAC,SAAS,EAAE,EAC1F,IAAgB,EAAqB,SAAS;AAIpD,SAAgB;AACd,MAAwB,EAAc;IACrC,CAAC,GAAe,EAAsB,CAAC;CAE1C,IAAM,IAAa,EAAY,YAAY;AAEzC,EADA,EAAW,GAAK,EAChB,EAAS,KAAK;AACd,MAAI;GACF,IAAM,IAAU,MAAM,QAAQ,IAC5B,EAAS,OAAO,IAAI,OAAO,MAAM,CAAC,EAAE,KAAM,MAAM,EAAoB,GAAO,EAAE,IAAI,IAAK,GAAG,CAAC,CAC3F,EACK,IAAO,OAAO,YAAY,EAAQ;AAExC,GADA,EAAU,EAAK,EACf,EAAe,EAAK;WACb,GAAK;AACZ,KACE,aAAe,QACX,EAAI,UACJ,EAAG,qCAAqC,sCAAsC,CACnF;YACO;AACR,KAAW,GAAM;;IAElB;EAAC;EAAO,EAAS;EAAQ;EAAG,CAAC;AAEhC,SAAgB;AACT,KAAY;IAChB,CAAC,EAAW,CAAC;CAEhB,IAAM,IAAe,EACnB,OAAO,GAAa,MAAkB;AAGpC,EAFA,EAAU,EAAI,EACd,EAAS,KAAK,EACd,EAAW,KAAK;EAChB,IAAM,IAAU,EAAM,MAAM;AAC5B,MAAI;AASF,GARI,IACF,MAAM,EAAoB,GAAO,GAAK,EAAQ,GAE9C,MAAM,EAAuB,GAAO,EAAI,EAI1C,GAAgB,OAAS;IAAE,GAAG;KAAM,IAAM;IAAS,EAAE,EACrD,EACE,IACI,EAAG,gCAAgC,GAAG,EAAI,QAAQ,GAClD,EAAG,kCAAkC,GAAG,EAAI,UAAU,CAC3D;WACM,GAAK;AACZ,KACE,aAAe,QACX,EAAI,UACJ,EAAG,qCAAqC,4BAA4B,CACzE;YACO;AACR,KAAU,KAAK;;IAGnB,CAAC,GAAO,EAAG,CACZ;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,MAAD;MAAI,WAAU;gBAA2C,EAAS;MAAW,CAAA;KAC7E,kBAAC,KAAD;MAAG,WAAU;gBAA+B,EAAS;MAAgB,CAAA;KACpE,EAAY,SAAS,IACpB,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,mCACA,oBAAoB,EAAY,KAAK,MAAM,CAAC,uCAC7C;MACC,CAAA,GACF;KACA;;GAKL,IACC,kBAAC,OAAD;IAAK,WAAU;cACZ,EACC,wCACA,GAAG,EAAS,MAAM,gCAAgC,EAAqB,KAAK,MAAM,CAAC,2BACpF;IACG,CAAA,GACJ;GAEH,IACC,kBAAC,OAAD;IAAK,WAAU;cACZ;IACG,CAAA,GACJ;GACH,IACC,kBAAC,OAAD;IAAK,WAAU;cACZ;IACG,CAAA,GACJ;GAEH,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,EAC3C,EAAG,kCAAkC,WAAW,CAC7C;QAEN,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAS,OAAO,KAAK,MACpB,kBAAC,GAAD;KAEE,OAAO,EAAE;KACT,QAAQ,EAAE,UAAU;KACpB,OAAO,EAAO,EAAE,QAAQ;KACxB,WAAW,MAAM,GAAW,OAAS;MAAE,GAAG;OAAM,EAAE,MAAM;MAAG,EAAE;KAC7D,QAAQ,MAAW,EAAE;KACrB,SAAS,EAAQ,EAAE,QAAQ;KAC3B,uBAAuB,GAAY,OAAS;MAAE,GAAG;OAAM,EAAE,MAAM,CAAC,EAAI,EAAE;MAAM,EAAE;KAC9E,cAAc,KAAK,EAAa,EAAE,KAAK,EAAO,EAAE,QAAQ,GAAG;KAC3D,eAAe,KAAK,EAAa,EAAE,KAAK,GAAG;KAC3C,EAVK,EAAE,IAUP,CACF;IACE,CAAA;GAEJ;;;AAgBV,SAAS,EAAgB,EACvB,UACA,WACA,UACA,aACA,WACA,YACA,oBACA,WACA,cACuB;AAEvB,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,SAAD;GAAO,WAAU;aAAiD;GAAc,CAAA,EAChF,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,SAAD;MACE,MAPa,KAAU,CAAC,IAOD,aAAa;MAC7B;MACP,WAAW,MAAU,EAAS,EAAM,OAAO,MAAM;MACjD,cAAa;MACb,YAAY;MACZ,WAAU;MACV,CAAA,EACD,IACC,kBAAC,UAAD;MACE,MAAK;MACL,SAAS;MACT,WAAU;gBAEC,EAAV,IAAW,IAAiC,GAAlC,EAAQ,WAAU,WAAY,CAA8B;MAChE,CAAA,GACP,KACA;;IACN,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,UAAU;KACV,WAAU;eAJZ,CAMG,IAAS,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAAG,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAC,OAE/E;;IACT,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,UAAU,KAAU,CAAC;KACrB,WAAU;eAJZ,CAME,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA,EAAA,QAEvB;;IACL;KACF"}
@@ -10,7 +10,10 @@ var p = ({ vibeId: p, tr: m, onAttached: h }) => {
10
10
  let g = n(), [_, v] = o(!1), [y, b] = o(null), [x, S] = o(null), { data: C, loading: w } = t({
11
11
  skip: !_,
12
12
  fetchPolicy: "cache-and-network"
13
- }), [T] = e(), E = a(() => C?.vibecontrolsAgents ?? [], [C]), D = i(async (e) => {
13
+ }), [T] = e({
14
+ refetchQueries: ["GetVibe", "ListVibes"],
15
+ awaitRefetchQueries: !0
16
+ }), E = a(() => C?.vibecontrolsAgents ?? [], [C]), D = i(async (e) => {
14
17
  b(null), S(e);
15
18
  try {
16
19
  await T({ variables: { input: {
@@ -1 +1 @@
1
- {"version":3,"file":"VibeAgentEmptyState.js","names":[],"sources":["../../../src/components/vibe/VibeAgentEmptyState.tsx"],"sourcesContent":["/**\n * VibeAgentEmptyState — shown on the Vibe details page when the vibe has no\n * agent attached. Tabs that depend on an agent (sessions, gitops, GraphQL\n * playground, git) are hidden, and the user is invited to attach an agent.\n *\n * UX: opens an in-place modal listing the workspace's existing agents so the\n * user can attach one without leaving the vibe detail page. Falls back to\n * navigating to the agents page only if no agents exist in the workspace\n * (so the user can register their first one).\n */\nimport { useCallback, useMemo, useState } from 'react';\nimport type { FC } from 'react';\nimport { ArrowRight, Plus, Server } from 'lucide-react';\nimport { EmptyState } from '@/components/shared';\nimport { useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport {\n useListAgentsQuery,\n useAttachAgentToVibeMutation,\n type ListAgentsQuery,\n} from '@/generated/wspace-operations';\n\nexport interface VibeAgentEmptyStateProps {\n vibeId: string;\n tr: (key: string, fallback: string) => string;\n /** Optional: called after a successful attach so the parent can refetch. */\n onAttached?: () => void;\n}\n\nexport const VibeAgentEmptyState: FC<VibeAgentEmptyStateProps> = ({ vibeId, tr, onAttached }) => {\n const navigate = useVibeNavigate();\n const [open, setOpen] = useState(false);\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n const [busyAgentId, setBusyAgentId] = useState<string | null>(null);\n\n const { data, loading } = useListAgentsQuery({\n skip: !open,\n fetchPolicy: 'cache-and-network',\n });\n const [attach] = useAttachAgentToVibeMutation();\n\n const agents = useMemo<ListAgentsQuery['vibecontrolsAgents']>(\n () => data?.vibecontrolsAgents ?? [],\n [data]\n );\n\n const handleAttach = useCallback(\n async (agentId: string) => {\n setErrorMessage(null);\n setBusyAgentId(agentId);\n try {\n await attach({ variables: { input: { vibeId, agentId } } });\n setOpen(false);\n onAttached?.();\n } catch (err) {\n setErrorMessage(err instanceof Error ? err.message : String(err));\n } finally {\n setBusyAgentId(null);\n }\n },\n [attach, vibeId, onAttached]\n );\n\n const handleRegisterNew = useCallback(() => {\n // Deep-link to the agents page with vibeId so the agents UI can offer\n // a \"register and attach to this vibe\" affordance.\n const query = vibeId ? `?attachToVibe=${encodeURIComponent(vibeId)}` : '';\n navigate(`/agents${query}`);\n }, [navigate, vibeId]);\n\n return (\n <>\n <div className=\"rounded-lg border border-border-default bg-bg-surface\">\n <EmptyState\n icon={<Server className=\"w-8 h-8 text-text-secondary\" />}\n title={tr('vibeDetails.noAgentTitle', 'No agent attached')}\n description={tr(\n 'vibeDetails.noAgentDescription',\n 'This vibe is not attached to an agent yet. Sessions, GitOps, GraphQL playground, and git tooling all run through an agent — connect one to unlock those tabs.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n className=\"inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n {tr('vibeDetails.connectAnAgent', 'Connect an agent to this vibe')}\n <ArrowRight className=\"w-4 h-4\" />\n </button>\n }\n />\n </div>\n\n {open && (\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"vibe-attach-agent-title\"\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4\"\n onClick={(e) => {\n if (e.target === e.currentTarget) setOpen(false);\n }}\n >\n <div className=\"w-full max-w-md rounded-lg bg-bg-elevated border border-border-default shadow-xl\">\n <div className=\"flex items-center justify-between px-5 py-4 border-b border-border-subtle\">\n <h2\n id=\"vibe-attach-agent-title\"\n className=\"text-base font-semibold text-text-primary\"\n >\n {tr('vibeDetails.attachAgentTitle', 'Attach an agent')}\n </h2>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n className=\"text-text-secondary hover:text-text-primary transition-colors\"\n aria-label={tr('common.close', 'Close')}\n >\n ✕\n </button>\n </div>\n\n <div className=\"px-5 py-4 max-h-[60vh] overflow-y-auto\">\n {loading && agents.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">{tr('common.loading', 'Loading…')}</p>\n ) : agents.length === 0 ? (\n <div className=\"text-sm text-text-secondary space-y-3\">\n <p>\n {tr(\n 'vibeDetails.noWorkspaceAgents',\n 'No agents in this workspace yet. Register one first, then attach it to this vibe.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={handleRegisterNew}\n className=\"inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n <Plus className=\"w-4 h-4\" />\n {tr('vibeDetails.registerAgent', 'Register an agent')}\n </button>\n </div>\n ) : (\n <ul className=\"space-y-2\">\n {agents.map((a) => (\n <li key={a.id}>\n <button\n type=\"button\"\n onClick={() => handleAttach(a.id)}\n disabled={busyAgentId !== null}\n className=\"w-full flex items-center justify-between gap-3 px-3 py-2.5 rounded-md border border-border-subtle hover:border-border-default hover:bg-action-ghost-bg-hover transition-colors text-left disabled:opacity-60\"\n >\n <span className=\"flex items-center gap-3 min-w-0\">\n <Server className=\"w-4 h-4 text-text-secondary flex-shrink-0\" />\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium text-text-primary truncate\">\n {a.name}\n </span>\n <span className=\"block text-xs text-text-secondary truncate\">\n {a.hostname || '—'}{' '}\n {a.isActive\n ? `· ${tr('agents.active', 'Active')}`\n : `· ${tr('agents.inactive', 'Inactive')}`}\n </span>\n </span>\n </span>\n <span className=\"text-xs text-text-tertiary\">\n {busyAgentId === a.id\n ? tr('common.attaching', 'Attaching…')\n : tr('vibeDetails.attach', 'Attach')}\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n\n {errorMessage && (\n <p className=\"mt-3 text-sm text-status-error-text\">{errorMessage}</p>\n )}\n </div>\n\n {agents.length > 0 && (\n <div className=\"flex items-center justify-between px-5 py-3 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={handleRegisterNew}\n className=\"text-xs text-text-secondary hover:text-text-primary inline-flex items-center gap-1\"\n >\n <Plus className=\"w-3.5 h-3.5\" />\n {tr('vibeDetails.registerAgent', 'Register a new agent')}\n </button>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n className=\"text-sm px-3 py-1.5 rounded-md border border-border-default text-text-primary hover:bg-action-ghost-bg-hover\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n </div>\n )}\n </div>\n </div>\n )}\n </>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"VibeAgentEmptyState.js","names":[],"sources":["../../../src/components/vibe/VibeAgentEmptyState.tsx"],"sourcesContent":["/**\n * VibeAgentEmptyState — shown on the Vibe details page when the vibe has no\n * agent attached. Tabs that depend on an agent (sessions, gitops, GraphQL\n * playground, git) are hidden, and the user is invited to attach an agent.\n *\n * UX: opens an in-place modal listing the workspace's existing agents so the\n * user can attach one without leaving the vibe detail page. Falls back to\n * navigating to the agents page only if no agents exist in the workspace\n * (so the user can register their first one).\n */\nimport { useCallback, useMemo, useState } from 'react';\nimport type { FC } from 'react';\nimport { ArrowRight, Plus, Server } from 'lucide-react';\nimport { EmptyState } from '@/components/shared';\nimport { useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport {\n useListAgentsQuery,\n useAttachAgentToVibeMutation,\n type ListAgentsQuery,\n} from '@/generated/wspace-operations';\n\nexport interface VibeAgentEmptyStateProps {\n vibeId: string;\n tr: (key: string, fallback: string) => string;\n /** Optional: called after a successful attach so the parent can refetch. */\n onAttached?: () => void;\n}\n\nexport const VibeAgentEmptyState: FC<VibeAgentEmptyStateProps> = ({ vibeId, tr, onAttached }) => {\n const navigate = useVibeNavigate();\n const [open, setOpen] = useState(false);\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n const [busyAgentId, setBusyAgentId] = useState<string | null>(null);\n\n const { data, loading } = useListAgentsQuery({\n skip: !open,\n fetchPolicy: 'cache-and-network',\n });\n const [attach] = useAttachAgentToVibeMutation({\n refetchQueries: ['GetVibe', 'ListVibes'],\n awaitRefetchQueries: true,\n });\n\n const agents = useMemo<ListAgentsQuery['vibecontrolsAgents']>(\n () => data?.vibecontrolsAgents ?? [],\n [data]\n );\n\n const handleAttach = useCallback(\n async (agentId: string) => {\n setErrorMessage(null);\n setBusyAgentId(agentId);\n try {\n await attach({ variables: { input: { vibeId, agentId } } });\n setOpen(false);\n onAttached?.();\n } catch (err) {\n setErrorMessage(err instanceof Error ? err.message : String(err));\n } finally {\n setBusyAgentId(null);\n }\n },\n [attach, vibeId, onAttached]\n );\n\n const handleRegisterNew = useCallback(() => {\n // Deep-link to the agents page with vibeId so the agents UI can offer\n // a \"register and attach to this vibe\" affordance.\n const query = vibeId ? `?attachToVibe=${encodeURIComponent(vibeId)}` : '';\n navigate(`/agents${query}`);\n }, [navigate, vibeId]);\n\n return (\n <>\n <div className=\"rounded-lg border border-border-default bg-bg-surface\">\n <EmptyState\n icon={<Server className=\"w-8 h-8 text-text-secondary\" />}\n title={tr('vibeDetails.noAgentTitle', 'No agent attached')}\n description={tr(\n 'vibeDetails.noAgentDescription',\n 'This vibe is not attached to an agent yet. Sessions, GitOps, GraphQL playground, and git tooling all run through an agent — connect one to unlock those tabs.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n className=\"inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n {tr('vibeDetails.connectAnAgent', 'Connect an agent to this vibe')}\n <ArrowRight className=\"w-4 h-4\" />\n </button>\n }\n />\n </div>\n\n {open && (\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"vibe-attach-agent-title\"\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4\"\n onClick={(e) => {\n if (e.target === e.currentTarget) setOpen(false);\n }}\n >\n <div className=\"w-full max-w-md rounded-lg bg-bg-elevated border border-border-default shadow-xl\">\n <div className=\"flex items-center justify-between px-5 py-4 border-b border-border-subtle\">\n <h2\n id=\"vibe-attach-agent-title\"\n className=\"text-base font-semibold text-text-primary\"\n >\n {tr('vibeDetails.attachAgentTitle', 'Attach an agent')}\n </h2>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n className=\"text-text-secondary hover:text-text-primary transition-colors\"\n aria-label={tr('common.close', 'Close')}\n >\n ✕\n </button>\n </div>\n\n <div className=\"px-5 py-4 max-h-[60vh] overflow-y-auto\">\n {loading && agents.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">{tr('common.loading', 'Loading…')}</p>\n ) : agents.length === 0 ? (\n <div className=\"text-sm text-text-secondary space-y-3\">\n <p>\n {tr(\n 'vibeDetails.noWorkspaceAgents',\n 'No agents in this workspace yet. Register one first, then attach it to this vibe.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={handleRegisterNew}\n className=\"inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n <Plus className=\"w-4 h-4\" />\n {tr('vibeDetails.registerAgent', 'Register an agent')}\n </button>\n </div>\n ) : (\n <ul className=\"space-y-2\">\n {agents.map((a) => (\n <li key={a.id}>\n <button\n type=\"button\"\n onClick={() => handleAttach(a.id)}\n disabled={busyAgentId !== null}\n className=\"w-full flex items-center justify-between gap-3 px-3 py-2.5 rounded-md border border-border-subtle hover:border-border-default hover:bg-action-ghost-bg-hover transition-colors text-left disabled:opacity-60\"\n >\n <span className=\"flex items-center gap-3 min-w-0\">\n <Server className=\"w-4 h-4 text-text-secondary flex-shrink-0\" />\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium text-text-primary truncate\">\n {a.name}\n </span>\n <span className=\"block text-xs text-text-secondary truncate\">\n {a.hostname || '—'}{' '}\n {a.isActive\n ? `· ${tr('agents.active', 'Active')}`\n : `· ${tr('agents.inactive', 'Inactive')}`}\n </span>\n </span>\n </span>\n <span className=\"text-xs text-text-tertiary\">\n {busyAgentId === a.id\n ? tr('common.attaching', 'Attaching…')\n : tr('vibeDetails.attach', 'Attach')}\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n\n {errorMessage && (\n <p className=\"mt-3 text-sm text-status-error-text\">{errorMessage}</p>\n )}\n </div>\n\n {agents.length > 0 && (\n <div className=\"flex items-center justify-between px-5 py-3 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={handleRegisterNew}\n className=\"text-xs text-text-secondary hover:text-text-primary inline-flex items-center gap-1\"\n >\n <Plus className=\"w-3.5 h-3.5\" />\n {tr('vibeDetails.registerAgent', 'Register a new agent')}\n </button>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n className=\"text-sm px-3 py-1.5 rounded-md border border-border-default text-text-primary hover:bg-action-ghost-bg-hover\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n </div>\n )}\n </div>\n </div>\n )}\n </>\n );\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-vibecontrols",
3
- "version": "2026.522.6",
3
+ "version": "2026.522.7",
4
4
  "description": "VibeControls microfrontend for Burdenoff products",
5
5
  "type": "module",
6
6
  "files": [