@burdenoff/microfe-vibecontrols 2026.522.5 → 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.
- package/dist/components/agent-manager/AgentManagerChatPane.js +78 -56
- package/dist/components/agent-manager/AgentManagerChatPane.js.map +1 -1
- package/dist/components/ai/AIModeToggle.js +43 -0
- package/dist/components/ai/AIModeToggle.js.map +1 -0
- package/dist/components/ai/AIProviderCredentialsCard.js +76 -53
- package/dist/components/ai/AIProviderCredentialsCard.js.map +1 -1
- package/dist/components/vibe/VibeAgentEmptyState.js +4 -1
- package/dist/components/vibe/VibeAgentEmptyState.js.map +1 -1
- package/dist/pages/CatalogPage.js.map +1 -1
- package/dist/pages/ComponentsPage.js.map +1 -1
- package/dist/pages/TemplatesPage.js.map +1 -1
- package/dist/pages/VibeDetailsPage.js.map +1 -1
- package/dist/pages/ai/AIPage.js +91 -104
- package/dist/pages/ai/AIPage.js.map +1 -1
- package/dist/pages/ai/AgentManagerPage.js +99 -86
- package/dist/pages/ai/AgentManagerPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
12
|
-
import { AIProviderCredentialsCard as
|
|
13
|
-
import { useCallback as
|
|
14
|
-
import { Bookmark as
|
|
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:
|
|
19
|
-
let [T, E] =
|
|
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:
|
|
25
|
-
}), { bookmarkedIds: Z, toggle: Q } = o(
|
|
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 =
|
|
33
|
+
}, []), ne = f((e) => {
|
|
34
34
|
Q({
|
|
35
35
|
agentId: C,
|
|
36
|
-
sessionId:
|
|
36
|
+
sessionId: d,
|
|
37
37
|
messageId: e
|
|
38
38
|
});
|
|
39
39
|
}, [
|
|
40
40
|
Q,
|
|
41
41
|
C,
|
|
42
|
-
|
|
43
|
-
]), re =
|
|
44
|
-
|
|
42
|
+
d
|
|
43
|
+
]), re = f(() => {
|
|
44
|
+
O((e) => (e && A(""), !e));
|
|
45
45
|
}, []);
|
|
46
|
-
return
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
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:
|
|
114
|
-
onChange: (e) =>
|
|
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
|
-
}),
|
|
118
|
+
}), k && /* @__PURE__ */ x("button", {
|
|
119
119
|
type: "button",
|
|
120
|
-
onClick: () =>
|
|
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(
|
|
133
|
+
children: /* @__PURE__ */ x(_, { className: "w-3.5 h-3.5" })
|
|
134
134
|
}),
|
|
135
135
|
/* @__PURE__ */ S("button", {
|
|
136
136
|
type: "button",
|
|
137
|
-
onClick: () =>
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
173
|
+
open: te,
|
|
174
174
|
onClose: () => N(!1),
|
|
175
|
-
sessionId:
|
|
175
|
+
sessionId: d,
|
|
176
176
|
messages: G,
|
|
177
177
|
onJump: $
|
|
178
178
|
}),
|
|
179
|
-
/* @__PURE__ */ x(
|
|
180
|
-
open:
|
|
179
|
+
/* @__PURE__ */ x(u, {
|
|
180
|
+
open: j,
|
|
181
181
|
agentId: C,
|
|
182
|
-
onClose: () =>
|
|
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
|
|
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: [
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
className: "
|
|
217
|
-
children:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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":";;;;;;;;;;;;;;;;;AA8BA,IAAa,KAAuD,EAAE,cAAW,iBAAc;CAC7F,IAAM,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,CAAC,GAAY,MAAiB,EAAS,GAAM,EAC7C,CAAC,GAAa,KAAkB,EAAS,GAAG,EAG5C,CAAC,GAAkB,KAAuB,EAAS,GAAM,EACzD,CAAC,GAAqB,KAA0B,EAAS,GAAM,EAC/D,CAAC,GAAgB,KAAqB,EAAwB,KAAK,EACnE,CAAC,GAAc,KAAmB,GAAiB,EACnD,IAAyB,EAAa,IAAI,YAAY,EACtD,IAA0B,EAAsB,KAAK,EACrD,IAAK,GAAO,EAEZ,EAAE,cAAW,GAAsB,EACnC,IAAc,QACZ,EAAO,MAAM,MAAM,EAAE,OAAO,EAAQ,IAAI,MAC9C,CAAC,GAAQ,EAAQ,CAClB,EAEK,IAAe,GAAsB,MAAM,EAAE,cAAc,GAAW,EACtE,IAAa,GAAsB,MAAM,EAAE,WAAW,EAEtD,IAAW,QAAc,GAAc,YAAY,EAAE,EAAE,CAAC,GAAc,SAAS,CAAC,EAEhF,EAAE,QAAQ,MAAkB,EAAsB,GAAa,MAAM,KAAK,EAC1E,IAAW,SACR;EAAE,IAAI;EAAS,SAAS;EAAe,GAC9C,CAAC,GAAS,EAAc,CACzB,EAEK,EAAE,gBAAa,iBAAc,mBAAgB,EAAyB;EAC1E,OAAO;EACP;EACD,CAAC,EAEI,EAAE,kBAAe,QAAQ,MAAmB,EAAuB,EAAU,EAE7E,IAAgB,GAAa,MAAsB;EACvD,IAAM,IAAK,SAAS,cAAc,qBAAqB,EAAU,IAAI;AAChE,QACL,EAAG,eAAe;GAAE,UAAU;GAAU,OAAO;GAAU,CAAC,EAC1D,EAAkB,EAAU,EAC5B,OAAO,iBAAiB;AACtB,MAAmB,MAAa,MAAY,IAAY,OAAO,EAAS;KACvE,KAAK;IACP,EAAE,CAAC,EAEA,KAAuB,GAC1B,MAAsB;AAChB,IAAe;GAAE;GAAS;GAAW;GAAW,CAAC;IAExD;EAAC;EAAgB;EAAS;EAAU,CACrC,EAEK,KAAe,QAAkB;AACrC,MAAe,OACT,KAAM,EAAe,GAAG,EACrB,CAAC,GACR;IACD,EAAE,CAAC;AA0EN,QAvEA,QAAgB;EACd,IAAI,IAAY;AA8ChB,UA5CyB,YAAY;AAE/B,WAAS,SAAS,IAEtB;MAAe,GAAK;AACpB,QAAI;KACF,IAAM,IAAO,MAAM,EAAiB,GAAU,GAAW,EACvD,OAAO,CAAC,SAAS,SAAS,EAC3B,CAAC;AAEF,SAAI,KAAa,EAAK,WAAW,EAAG;KAEpC,IAAM,oBAAc,IAAI,KAAa;AAKrC,UAAK,IAAM,KAAO,GAAM;MACtB,IAAM,IAAU,EAAI,WAAW,EAAI,WAAW,IACxC,IAAY,EAAI,aAAa,EAAI,WACjC,IAAkB,IAAY,KAAK,MAAM,IAAI,KAAK,EAAU,CAAC,SAAS,GAAG,IAAK,GAAG,GACjF,IAAS;OAAC,EAAI;OAAM;OAAS,EAAI,SAAS;OAAI;OAAgB,CAAC,KAAK,KAAS;AAC/E,QAAY,IAAI,EAAO,KAC3B,EAAY,IAAI,EAAO,EAUvB,EAAW,GARsB;OAC/B,IAAI,EAAI;OACR,MAAM,EAAI,SAAS,UAAU,SAAS;OACtC;OACA;OACA,QAAQ,EAAI,aAAa;QAAE,OAAO;QAAG,QAAQ,EAAI;QAAY,GAAG,KAAA;OAChE,OAAO,EAAI;OACZ,CACyB;;YAEtB,WAEE;AACR,KAAK,KAAW,EAAe,GAAM;;;MAIlB,QAEV;AACX,OAAY;;IAGb,CAAC,GAAS,EAAU,CAAC,EAIxB,QAAgB;AAKd,MAJI,CAAC,KAA0B,KAC3B,EAAwB,YAAY,KAGpC,CADW,EAAS,MAAM,MAAM,EAAE,OAAO,EAAuB,CACvD;AAGb,EADA,EAAwB,UAAU,GAClC,EAAc,EAAuB;EAGrC,IAAM,IAAO,IAAI,gBAAgB,EAAa;AAE9C,EADA,EAAK,OAAO,YAAY,EACxB,EAAgB,GAAM,EAAE,SAAS,IAAM,CAAC;IACvC;EAAC;EAAwB;EAAa;EAAU;EAAe;EAAc;EAAgB,CAAC,EAG/F,kBAAC,OAAD;EAAK,WAAU;EAA+B,eAAY;YAA1D;GAKE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAW,0CACT,IACI,qDACA;OAEN,OAAO,EACL,sDACA,6BACD;OACD,cAAY,EACV,sDACA,oBACD;iBAED,kBAAC,GAAD,EAAQ,WAAU,eAAgB,CAAA;OAC3B,CAAA,EACR,KACC,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,SAAD;QACE,MAAK;QACL,OAAO;QACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;QAC/C,aAAa,EACX,wDACA,4BACD;QACD,WAAA;QACA,WAAU;QACV,CAAA,EACD,KACC,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAe,GAAG;QACjC,WAAU;kBAEV,kBAAC,GAAD,EAAG,WAAU,8BAA+B,CAAA;QACrC,CAAA,CAEP;SAEJ;;KACN,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAuB,GAAK;MAC3C,WAAU;MACV,OAAO,EACL,uDACA,2BACD;MACD,cAAY,EAAG,qDAAqD,YAAY;MAChF,eAAY;gBAEZ,kBAAC,GAAD,EAAU,WAAU,eAAgB,CAAA;MAC7B,CAAA;KACT,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAoB,GAAK;MACxC,WAAU;MACV,OAAO,EACL,6DACA,mEACD;MACD,cAAY,EACV,sDACA,sBACD;MACD,eAAY;gBAZd,CAcE,kBAAC,GAAD,EAAa,WAAU,eAAgB,CAAA,EACvC,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAG,sDAAsD,sBAAsB;OAC3E,CAAA,CACA;;KACL;;GAGL,IACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,0DACA,0BACD;KACC,CAAA;IACA,CAAA,GAEN,kBAAC,GAAD;IACY;IACG;IACb,gBAAgB;IACH;IACE;IACf,kBAAkB;IACF;IAChB,CAAA;GAIJ,kBAAC,GAAD;IACW;IACE;IACX,SAAS,MAAS,KAAK,EAAY,EAAK;IACxC,UAAU;IACV,gBAAgB,GAAa,aAAa,KAAA;IAC1C,aAAa,GAAa,eAAe,KAAA;IACzC,CAAA;GAGF,kBAAC,GAAD;IACE,MAAM;IACN,eAAe,EAAuB,GAAM;IACjC;IACD;IACV,QAAQ;IACR,CAAA;GAIF,kBAAC,IAAD;IACE,MAAM;IACG;IACT,eAAe,EAAoB,GAAM;IACzC,CAAA;GAOF,kBAAC,GAAD;IACW;IACC;IACV,mBAAmB,GAAc,OAAO,KAAA;IACxC,CAAA;GACE;;GAUJ,KAAmD,EACvD,YACA,aACA,2BACI;CACJ,IAAM,IAAgB,GAAsB,MAAM,EAAE,cAAc,EAC5D,IAAmB,GAAsB,MAAM,EAAE,iBAAiB,EAClE,IAAK,GAAO;AAIlB,QAFI,CAAC,KAAiB,EAAc,YAAY,IAAgB,OAG9D,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GACE,WAAU;GACV,eAAe,EAAiB,KAAK;GACrC,eAAA;GACA,CAAA,EAKF,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,iCAAiC,oCAAoC;KACtE,CAAA,EACL,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAiB,KAAK;KACrC,WAAU;KACV,cAAY,EAAG,6BAA6B,QAAQ;eAEpD,kBAAC,GAAD,EAAG,WAAU,+BAAgC,CAAA;KACtC,CAAA,CACL;OACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,gCACA,gIACD;KACC,CAAA,EACJ,kBAAC,GAAD;KAA2B,OAAO;KAA6B;KAAqB,CAAA,CAChF;MACF;KACF"}
|
|
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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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useVibeNavigate as e } from "../../providers/VibeControlsProvider.js";
|
|
2
|
+
import { useCallback as t } from "react";
|
|
3
|
+
import { useI18n as n } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
4
|
+
import { Brain as r, MapIcon as i } from "lucide-react";
|
|
5
|
+
import { useLocation as a } from "react-router-dom";
|
|
6
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
7
|
+
//#region src/components/ai/AIModeToggle.tsx
|
|
8
|
+
var c = "/ai-tools/plan";
|
|
9
|
+
function l(e) {
|
|
10
|
+
return e.endsWith("/ai-tools/plan") || e.includes("/ai-tools/plan/");
|
|
11
|
+
}
|
|
12
|
+
function u({ planHref: u = c, aiHref: d, activeMode: f, className: p }) {
|
|
13
|
+
let { t: m } = n(), { pathname: h } = a(), g = e(), _ = t((e, t) => {
|
|
14
|
+
let n = m(e);
|
|
15
|
+
return n === e ? t : n;
|
|
16
|
+
}, [m]), v = f ?? (l(h) ? "plan" : "ai"), y = t((e) => {
|
|
17
|
+
g(e === "plan" ? u : d);
|
|
18
|
+
}, [
|
|
19
|
+
d,
|
|
20
|
+
g,
|
|
21
|
+
u
|
|
22
|
+
]);
|
|
23
|
+
return /* @__PURE__ */ o("div", {
|
|
24
|
+
role: "tablist",
|
|
25
|
+
"aria-label": _("ai.modeToggleLabel", "AI workspace mode"),
|
|
26
|
+
className: `inline-flex rounded-lg border border-border-default bg-bg-sunken p-0.5 ${p ?? ""}`,
|
|
27
|
+
children: ["plan", "ai"].map((e) => {
|
|
28
|
+
let t = v === e, n = e === "plan" ? i : r;
|
|
29
|
+
return /* @__PURE__ */ s("button", {
|
|
30
|
+
role: "tab",
|
|
31
|
+
"aria-selected": t,
|
|
32
|
+
"data-testid": `ai-mode-toggle-${e}`,
|
|
33
|
+
onClick: () => y(e),
|
|
34
|
+
className: `flex items-center gap-2 px-4 py-1.5 text-sm font-medium rounded-md transition-colors ${t ? "bg-bg-surface text-action-primary-bg shadow-sm" : "text-text-secondary hover:text-text-primary"}`,
|
|
35
|
+
children: [/* @__PURE__ */ o(n, { className: "h-4 w-4" }), e === "plan" ? _("ai.mode.plan", "Plan") : _("ai.mode.ai", "AI")]
|
|
36
|
+
}, e);
|
|
37
|
+
})
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { u as AIModeToggle };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=AIModeToggle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AIModeToggle.js","names":[],"sources":["../../../src/components/ai/AIModeToggle.tsx"],"sourcesContent":["/**\n * AIModeToggle — Plan / AI mode pivot.\n *\n * Rendered both on /ai-tools (AIPage) above the 9-tab sub-nav and on\n * /ai (AgentManagerPage) in the page header. Active state is computed\n * by matching the current pathname against the two route patterns.\n * Plan click navigates to the canonical Plan workspace\n * (/ai-tools/plan); AI click takes the caller's preferred AI landing\n * page (the AgentManagerPage points back to /ai, while AIPage stays\n * inside /ai-tools/ai/:tab so the active sub-tab is preserved).\n */\n\nimport { useCallback } from 'react';\nimport { useLocation } from 'react-router-dom';\nimport { Brain, MapIcon } from 'lucide-react';\n\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useVibeNavigate } from '@/providers/VibeControlsProvider';\n\nexport type AIMode = 'plan' | 'ai';\n\ninterface AIModeToggleProps {\n /** Path Plan toggle navigates to. Defaults to `/ai-tools/plan`. */\n planHref?: string;\n /** Path AI toggle navigates to when active mode is `plan`. */\n aiHref: string;\n /**\n * Optional explicit active-mode override. When unset the component\n * derives the active mode from the current pathname.\n */\n activeMode?: AIMode;\n className?: string;\n}\n\nconst DEFAULT_PLAN_HREF = '/ai-tools/plan';\n\nfunction isPlanRoute(pathname: string): boolean {\n return pathname.endsWith('/ai-tools/plan') || pathname.includes('/ai-tools/plan/');\n}\n\nexport function AIModeToggle({\n planHref = DEFAULT_PLAN_HREF,\n aiHref,\n activeMode,\n className,\n}: AIModeToggleProps) {\n const { t } = useI18n();\n const { pathname } = useLocation();\n const navigate = useVibeNavigate();\n\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n\n const resolvedMode: AIMode = activeMode ?? (isPlanRoute(pathname) ? 'plan' : 'ai');\n\n const onClick = useCallback(\n (mode: AIMode) => {\n navigate(mode === 'plan' ? planHref : aiHref);\n },\n [aiHref, navigate, planHref]\n );\n\n return (\n <div\n role=\"tablist\"\n aria-label={tr('ai.modeToggleLabel', 'AI workspace mode')}\n className={`inline-flex rounded-lg border border-border-default bg-bg-sunken p-0.5 ${className ?? ''}`}\n >\n {(['plan', 'ai'] as const).map((m) => {\n const isActive = resolvedMode === m;\n const Icon = m === 'plan' ? MapIcon : Brain;\n return (\n <button\n key={m}\n role=\"tab\"\n aria-selected={isActive}\n data-testid={`ai-mode-toggle-${m}`}\n onClick={() => onClick(m)}\n className={`flex items-center gap-2 px-4 py-1.5 text-sm font-medium rounded-md transition-colors ${\n isActive\n ? 'bg-bg-surface text-action-primary-bg shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n <Icon className=\"h-4 w-4\" />\n {m === 'plan' ? tr('ai.mode.plan', 'Plan') : tr('ai.mode.ai', 'AI')}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAkCA,IAAM,IAAoB;AAE1B,SAAS,EAAY,GAA2B;AAC9C,QAAO,EAAS,SAAS,iBAAiB,IAAI,EAAS,SAAS,kBAAkB;;AAGpF,SAAgB,EAAa,EAC3B,cAAW,GACX,WACA,eACA,gBACoB;CACpB,IAAM,EAAE,SAAM,GAAS,EACjB,EAAE,gBAAa,GAAa,EAC5B,IAAW,GAAiB,EAE5B,IAAK,GACR,GAAa,MAA6B;EACzC,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAEpC,CAAC,EAAE,CACJ,EAEK,IAAuB,MAAe,EAAY,EAAS,GAAG,SAAS,OAEvE,IAAU,GACb,MAAiB;AAChB,IAAS,MAAS,SAAS,IAAW,EAAO;IAE/C;EAAC;EAAQ;EAAU;EAAS,CAC7B;AAED,QACE,kBAAC,OAAD;EACE,MAAK;EACL,cAAY,EAAG,sBAAsB,oBAAoB;EACzD,WAAW,0EAA0E,KAAa;YAEhG,CAAC,QAAQ,KAAK,CAAW,KAAK,MAAM;GACpC,IAAM,IAAW,MAAiB,GAC5B,IAAO,MAAM,SAAS,IAAU;AACtC,UACE,kBAAC,UAAD;IAEE,MAAK;IACL,iBAAe;IACf,eAAa,kBAAkB;IAC/B,eAAe,EAAQ,EAAE;IACzB,WAAW,wFACT,IACI,mDACA;cATR,CAYE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAC3B,MAAM,SAAS,EAAG,gBAAgB,OAAO,GAAG,EAAG,cAAc,KAAK,CAC5D;MAbF,EAaE;IAEX;EACE,CAAA"}
|
|
@@ -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
|
|
89
|
-
return
|
|
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
|
-
|
|
94
|
+
l.length > 1 ? /* @__PURE__ */ g("button", {
|
|
95
95
|
type: "button",
|
|
96
|
-
onClick: () =>
|
|
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":
|
|
99
|
-
children: [/* @__PURE__ */ h(s, { className: "h-3.5 w-3.5" }),
|
|
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:
|
|
109
|
+
children: p.label
|
|
110
110
|
}), /* @__PURE__ */ h("p", {
|
|
111
111
|
className: "text-xs text-text-secondary",
|
|
112
|
-
children:
|
|
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(
|
|
116
|
+
}), /* @__PURE__ */ h(b, {
|
|
117
117
|
agent: t,
|
|
118
|
-
provider:
|
|
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:
|
|
132
|
+
children: a("ai.providerCredentials.pickTitle", "Choose a provider")
|
|
132
133
|
}), /* @__PURE__ */ h("p", {
|
|
133
134
|
className: "text-sm text-text-secondary",
|
|
134
|
-
children:
|
|
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:
|
|
140
|
+
children: l.map((e) => /* @__PURE__ */ g("button", {
|
|
140
141
|
type: "button",
|
|
141
|
-
onClick: () =>
|
|
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(
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
+
D(e instanceof Error ? e.message : u("ai.providerCredentials.loadFailed", "Failed to load provider credentials"));
|
|
165
173
|
} finally {
|
|
166
|
-
|
|
174
|
+
C(!1);
|
|
167
175
|
}
|
|
168
176
|
}, [
|
|
169
177
|
s,
|
|
170
178
|
c.fields,
|
|
171
|
-
|
|
179
|
+
u
|
|
172
180
|
]);
|
|
173
181
|
a(() => {
|
|
174
|
-
|
|
175
|
-
}, [
|
|
176
|
-
let
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
+
D(e instanceof Error ? e.message : u("ai.providerCredentials.saveFailed", "Failed to save credential"));
|
|
182
194
|
} finally {
|
|
183
|
-
|
|
195
|
+
T(null);
|
|
184
196
|
}
|
|
185
|
-
}, [s,
|
|
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: [
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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:
|
|
224
|
+
children: E
|
|
202
225
|
}) : null,
|
|
203
|
-
|
|
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:
|
|
228
|
+
children: O
|
|
206
229
|
}) : null,
|
|
207
|
-
|
|
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" }),
|
|
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(
|
|
235
|
+
children: c.fields.map((e) => /* @__PURE__ */ h(x, {
|
|
213
236
|
label: e.key,
|
|
214
237
|
secret: e.secret ?? !1,
|
|
215
|
-
value:
|
|
216
|
-
onChange: (t) =>
|
|
238
|
+
value: d[e.key] ?? "",
|
|
239
|
+
onChange: (t) => p((n) => ({
|
|
217
240
|
...n,
|
|
218
241
|
[e.key]: t
|
|
219
242
|
})),
|
|
220
|
-
saving:
|
|
221
|
-
visible:
|
|
222
|
-
onToggleVisible: () =>
|
|
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
|
|
227
|
-
onClear: () => void
|
|
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
|
|
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", {
|