@circuitwall/jarela 1.9.1 → 1.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +2 -2
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page.js +3989 -3805
- package/.next/standalone/.next/server/app/page.js.map +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +2 -2
- package/.next/standalone/.next/server/chunks/1813.js.map +1 -1
- package/.next/standalone/.next/server/chunks/319.js.map +1 -1
- package/.next/standalone/.next/server/chunks/4045.js.map +1 -1
- package/.next/standalone/.next/server/chunks/4741.js.map +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/.next/standalone/.next/server/pages/404.html +2 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/proxy.js.map +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/2747-4a6287cacd57d231.js.map +1 -1
- package/.next/standalone/.next/static/chunks/3457-6d51726379cee3b7.js.map +1 -1
- package/.next/standalone/.next/static/chunks/{1998-31a617131197a83a.js → 962-fe2372e00f85e23a.js} +111 -2
- package/.next/standalone/.next/static/chunks/962-fe2372e00f85e23a.js.map +1 -0
- package/.next/standalone/.next/static/chunks/app/layout-84c6f211a7a1ca36.js.map +1 -1
- package/.next/standalone/.next/static/chunks/app/{page-cd662565eba5ef59.js → page-c5b9f4407416c3f9.js} +3493 -3390
- package/.next/standalone/.next/static/chunks/app/page-c5b9f4407416c3f9.js.map +1 -0
- package/.next/standalone/.next/static/chunks/main-3eb94471f04b2368.js.map +1 -1
- package/.next/standalone/.next/static/css/b8e04d59a2bfff04.css +5 -0
- package/.next/standalone/.next/static/css/b8e04d59a2bfff04.css.map +1 -0
- package/.next/standalone/package.json +1 -1
- package/CHANGELOG.md +47 -0
- package/README.md +6 -6
- package/components/agents/agent-editor/VoiceFields.tsx +1 -1
- package/components/credentials/AddCredentialDialog.tsx +30 -141
- package/components/credentials/CredentialsPanel.tsx +146 -49
- package/components/{integrations/IntegrationsPanel.tsx → credentials/IntegrationCard.tsx} +9 -168
- package/components/documents/AddSourceForm.tsx +4 -4
- package/components/documents/DocumentsPanel.tsx +1 -1
- package/components/integrations/NetworkPanel.tsx +104 -0
- package/components/layout/AppShell.tsx +6 -0
- package/components/layout/MenuPanel.tsx +15 -91
- package/components/profile/ProfileEditor.tsx +1 -1
- package/components/proposals/ApprovalsBanner.tsx +2 -2
- package/components/settings/AppearancePanel.tsx +93 -0
- package/components/settings/SettingsPanel.tsx +94 -0
- package/contexts/AppContext.tsx +1 -1
- package/hooks/useUrlSync.ts +1 -1
- package/lib/ui/navigate.ts +1 -1
- package/package.json +1 -1
- package/.next/standalone/.next/static/chunks/1998-31a617131197a83a.js.map +0 -1
- package/.next/standalone/.next/static/chunks/app/page-cd662565eba5ef59.js.map +0 -1
- package/.next/standalone/.next/static/css/11aaed27d2989cc1.css +0 -5
- package/.next/standalone/.next/static/css/11aaed27d2989cc1.css.map +0 -1
- /package/.next/standalone/.next/static/{tTk-KuLcT7O-E0z6PdMmO → EOkgU73YJOpR-vFcKMgL0}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{tTk-KuLcT7O-E0z6PdMmO → EOkgU73YJOpR-vFcKMgL0}/_ssgManifest.js +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { ChevronLeft,
|
|
3
|
-
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { ChevronLeft, X } from "lucide-react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { api } from "@/api/client";
|
|
5
5
|
import type { IntegrationDefinition, IntegrationStatus } from "@/api/types";
|
|
6
6
|
import { useEscapeKey } from "@/hooks/useEscapeKey";
|
|
7
7
|
import { pushErrorToast } from "@/lib/ui/error-report";
|
|
8
|
-
|
|
9
|
-
const SECRET_MASK = "********";
|
|
8
|
+
import { IntegrationCard } from "./IntegrationCard";
|
|
10
9
|
|
|
11
10
|
type Category = NonNullable<IntegrationDefinition["category"]>;
|
|
12
11
|
|
|
@@ -46,17 +45,13 @@ interface Props {
|
|
|
46
45
|
// Called after a successful save with the integration name that was
|
|
47
46
|
// configured. Hosts can use this to refresh their views.
|
|
48
47
|
onSaved?: (integrationName: string) => void;
|
|
49
|
-
// Hook for "open in Built-in integrations sub-tab" — host wires it to
|
|
50
|
-
// flip the Credentials tab onto the integrations sub. When omitted the
|
|
51
|
-
// link is hidden.
|
|
52
|
-
onOpenInIntegrations?: (integrationName: string) => void;
|
|
53
48
|
}
|
|
54
49
|
|
|
55
50
|
// Unified picker for adding/editing any credential. Shows manifest
|
|
56
|
-
// providers grouped by category, then drops the user into
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
export function AddCredentialDialog({ initialCategory, directProviderName, lockCategory, onClose, onSaved
|
|
51
|
+
// providers grouped by category, then drops the user into the same
|
|
52
|
+
// IntegrationCard editor the Credentials panel renders inline — so
|
|
53
|
+
// OAuth Connect, Test, and the setup guides are all in one place.
|
|
54
|
+
export function AddCredentialDialog({ initialCategory, directProviderName, lockCategory, onClose, onSaved }: Props) {
|
|
60
55
|
const [defs, setDefs] = useState<IntegrationDefinition[]>([]);
|
|
61
56
|
const [statuses, setStatuses] = useState<Record<string, IntegrationStatus>>({});
|
|
62
57
|
const [loading, setLoading] = useState(true);
|
|
@@ -66,6 +61,16 @@ export function AddCredentialDialog({ initialCategory, directProviderName, lockC
|
|
|
66
61
|
|
|
67
62
|
useEscapeKey(onClose);
|
|
68
63
|
|
|
64
|
+
const reload = useCallback(async () => {
|
|
65
|
+
try {
|
|
66
|
+
const res = await api.integrations.list();
|
|
67
|
+
setDefs(res.definitions);
|
|
68
|
+
setStatuses(Object.fromEntries(res.statuses.map((s) => [s.name, s])));
|
|
69
|
+
} catch (e) {
|
|
70
|
+
pushErrorToast({ title: "Couldn't load credentials catalog", error: e, context: { panel: "credentials", action: "catalog.load" } });
|
|
71
|
+
}
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
69
74
|
useEffect(() => {
|
|
70
75
|
let cancelled = false;
|
|
71
76
|
(async () => {
|
|
@@ -178,16 +183,19 @@ export function AddCredentialDialog({ initialCategory, directProviderName, lockC
|
|
|
178
183
|
)}
|
|
179
184
|
|
|
180
185
|
{step === "form" && activeDef && (
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
<div className="p-4 max-h-[70vh] overflow-y-auto">
|
|
187
|
+
<IntegrationCard
|
|
188
|
+
definition={activeDef}
|
|
189
|
+
status={activeStatus ?? undefined}
|
|
190
|
+
onChanged={() => {
|
|
191
|
+
if (typeof window !== "undefined") {
|
|
192
|
+
window.dispatchEvent(new CustomEvent("jarela:credentials-changed"));
|
|
193
|
+
}
|
|
194
|
+
onSaved?.(activeDef.name);
|
|
195
|
+
void reload();
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
191
199
|
)}
|
|
192
200
|
{step === "form" && !activeDef && (
|
|
193
201
|
<p className="text-fg-faint text-sm text-center py-8">Loading…</p>
|
|
@@ -196,122 +204,3 @@ export function AddCredentialDialog({ initialCategory, directProviderName, lockC
|
|
|
196
204
|
</div>
|
|
197
205
|
);
|
|
198
206
|
}
|
|
199
|
-
|
|
200
|
-
interface ProviderFormProps {
|
|
201
|
-
def: IntegrationDefinition;
|
|
202
|
-
status: IntegrationStatus | null;
|
|
203
|
-
onSaved: () => void;
|
|
204
|
-
onCancel: () => void;
|
|
205
|
-
onOpenInIntegrations?: () => void;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function ProviderForm({ def, status, onSaved, onCancel, onOpenInIntegrations }: ProviderFormProps) {
|
|
209
|
-
const [values, setValues] = useState<Record<string, string>>(() => ({ ...(status?.values ?? {}) }));
|
|
210
|
-
const [saving, setSaving] = useState(false);
|
|
211
|
-
const [error, setError] = useState<string | null>(null);
|
|
212
|
-
|
|
213
|
-
async function save() {
|
|
214
|
-
setError(null);
|
|
215
|
-
setSaving(true);
|
|
216
|
-
try {
|
|
217
|
-
await api.integrations.save(def.name, values);
|
|
218
|
-
// Notify both panels that the credentials surface changed so they
|
|
219
|
-
// refresh without a full reload.
|
|
220
|
-
if (typeof window !== "undefined") {
|
|
221
|
-
window.dispatchEvent(new CustomEvent("jarela:credentials-changed"));
|
|
222
|
-
}
|
|
223
|
-
onSaved();
|
|
224
|
-
} catch (e) {
|
|
225
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
226
|
-
pushErrorToast({
|
|
227
|
-
title: `Couldn't save ${def.label}`,
|
|
228
|
-
error: e,
|
|
229
|
-
context: { panel: "credentials", action: "integration.save", integration: def.name },
|
|
230
|
-
});
|
|
231
|
-
} finally {
|
|
232
|
-
setSaving(false);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// OAuth providers benefit from the IntegrationsPanel's "Connect" flow.
|
|
237
|
-
// Surface a hint and a shortcut rather than forcing the user to paste
|
|
238
|
-
// refresh tokens by hand.
|
|
239
|
-
const isOauth = def.fields.some((f) => f.key === "client_id" || f.key === "refresh_token");
|
|
240
|
-
|
|
241
|
-
return (
|
|
242
|
-
<div className="p-4 space-y-3 max-h-[70vh] overflow-y-auto">
|
|
243
|
-
<p className="text-[11px] text-fg-muted leading-snug">{def.description}</p>
|
|
244
|
-
|
|
245
|
-
{isOauth && onOpenInIntegrations && (
|
|
246
|
-
<div className="px-3 py-2 rounded border border-sky-700/40 bg-sky-900/15 text-[11px] text-sky-700 dark:text-sky-300 flex items-start gap-2">
|
|
247
|
-
<span className="flex-1">
|
|
248
|
-
This provider supports one-click OAuth. Built-in integrations has the “Connect” button that walks you through it.
|
|
249
|
-
</span>
|
|
250
|
-
<button
|
|
251
|
-
type="button"
|
|
252
|
-
onClick={onOpenInIntegrations}
|
|
253
|
-
className="text-[11px] underline hover:text-sky-600 dark:hover:text-sky-200 shrink-0"
|
|
254
|
-
>
|
|
255
|
-
Open
|
|
256
|
-
</button>
|
|
257
|
-
</div>
|
|
258
|
-
)}
|
|
259
|
-
|
|
260
|
-
{def.fields.map((f) => (
|
|
261
|
-
<label key={f.key} className="block text-xs text-fg-subtle">
|
|
262
|
-
<span className="flex items-center gap-1.5">
|
|
263
|
-
{f.label}
|
|
264
|
-
{f.required && <span className="text-rose-700 dark:text-rose-400 ml-0.5">*</span>}
|
|
265
|
-
</span>
|
|
266
|
-
<input
|
|
267
|
-
type={f.secret ? "password" : "text"}
|
|
268
|
-
value={values[f.key] ?? ""}
|
|
269
|
-
onChange={(e) => setValues((p) => ({ ...p, [f.key]: e.target.value }))}
|
|
270
|
-
onFocus={(e) => {
|
|
271
|
-
// Clicking a masked secret field clears it so the user can type a fresh
|
|
272
|
-
// value without manually selecting and replacing the dots.
|
|
273
|
-
if (f.secret && e.target.value === SECRET_MASK) {
|
|
274
|
-
setValues((p) => ({ ...p, [f.key]: "" }));
|
|
275
|
-
}
|
|
276
|
-
}}
|
|
277
|
-
placeholder={f.placeholder}
|
|
278
|
-
className="mt-1 w-full px-2 py-1.5 text-sm rounded border border-border bg-surface-3 text-fg font-mono"
|
|
279
|
-
/>
|
|
280
|
-
</label>
|
|
281
|
-
))}
|
|
282
|
-
|
|
283
|
-
{error && (
|
|
284
|
-
<div className="px-2 py-1.5 rounded bg-rose-950/40 border border-rose-800 text-xs text-rose-700 dark:text-rose-300">
|
|
285
|
-
{error}
|
|
286
|
-
</div>
|
|
287
|
-
)}
|
|
288
|
-
|
|
289
|
-
<div className="flex items-center gap-2 pt-1">
|
|
290
|
-
<button
|
|
291
|
-
onClick={save}
|
|
292
|
-
disabled={saving}
|
|
293
|
-
className="px-3 py-1.5 text-xs rounded bg-accent text-white hover:bg-accent-hover disabled:opacity-50 inline-flex items-center gap-1.5"
|
|
294
|
-
>
|
|
295
|
-
{saving && <Loader2 size={11} className="animate-spin" />}
|
|
296
|
-
{saving ? "Saving…" : status?.configured ? "Update" : "Save"}
|
|
297
|
-
</button>
|
|
298
|
-
<button
|
|
299
|
-
onClick={onCancel}
|
|
300
|
-
className="px-3 py-1.5 text-xs rounded border border-border text-fg-muted hover:bg-surface-3"
|
|
301
|
-
>
|
|
302
|
-
Cancel
|
|
303
|
-
</button>
|
|
304
|
-
{onOpenInIntegrations && (
|
|
305
|
-
<button
|
|
306
|
-
type="button"
|
|
307
|
-
onClick={onOpenInIntegrations}
|
|
308
|
-
className="ml-auto inline-flex items-center gap-1 text-[11px] text-accent hover:text-accent/80"
|
|
309
|
-
title="Open in Built-in integrations — full editor with Test/OAuth"
|
|
310
|
-
>
|
|
311
|
-
Advanced <ExternalLink size={10} />
|
|
312
|
-
</button>
|
|
313
|
-
)}
|
|
314
|
-
</div>
|
|
315
|
-
</div>
|
|
316
|
-
);
|
|
317
|
-
}
|
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { Key, Pencil, Plus, Trash2 } from "lucide-react";
|
|
2
|
+
import { Filter, Key, Pencil, Plus, Trash2 } from "lucide-react";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { api } from "@/api/client";
|
|
5
|
-
import type { Credential, IntegrationDefinition } from "@/api/types";
|
|
5
|
+
import type { Credential, IntegrationDefinition, IntegrationStatus, UserProfile } from "@/api/types";
|
|
6
6
|
import { useAppContext } from "@/contexts/AppContext";
|
|
7
7
|
import { useDeepLinkScroll } from "@/hooks/useDeepLinkScroll";
|
|
8
|
-
import {
|
|
8
|
+
import { NetworkPanel } from "@/components/integrations/NetworkPanel";
|
|
9
|
+
import { PRESET_CATEGORIES } from "@/lib/integrations/categories";
|
|
9
10
|
import { AddCredentialDialog } from "./AddCredentialDialog";
|
|
11
|
+
import { IntegrationCard } from "./IntegrationCard";
|
|
10
12
|
|
|
11
13
|
// "Credentials" is the single home for every auth surface. The default
|
|
12
|
-
// sub-tab is the
|
|
13
|
-
// (
|
|
14
|
-
//
|
|
15
|
-
//
|
|
14
|
+
// sub-tab is the unified list: model API keys, integration keys, and
|
|
15
|
+
// OAuth (Gmail/Outlook/etc.) all live here, grouped by category, with
|
|
16
|
+
// each known integration rendered as an inline editor (save / test /
|
|
17
|
+
// Connect). The sibling "Network & environment" sub-tab hosts the
|
|
18
|
+
// non-auth bits — HTTP proxy, allowed sites, env-var aliases, and the
|
|
19
|
+
// env-sync button.
|
|
16
20
|
//
|
|
17
|
-
// MCP servers (a capability, not an auth)
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// "what accounts has this agent been given access to".
|
|
21
|
+
// MCP servers (a capability, not an auth) live under Tools. Result: one
|
|
22
|
+
// mental model — *Credentials* answers "what accounts has this agent
|
|
23
|
+
// been given access to".
|
|
21
24
|
|
|
22
|
-
type Sub = "list" | "
|
|
25
|
+
type Sub = "list" | "network";
|
|
23
26
|
|
|
24
27
|
const SUB_TITLES: Record<Sub, string> = {
|
|
25
|
-
list: "
|
|
26
|
-
|
|
28
|
+
list: "Credentials",
|
|
29
|
+
network: "Network & environment",
|
|
27
30
|
};
|
|
28
31
|
|
|
29
32
|
type Category = NonNullable<IntegrationDefinition["category"]>;
|
|
@@ -48,6 +51,13 @@ const CATEGORY_LABELS: Record<Category, string> = {
|
|
|
48
51
|
other: "Other",
|
|
49
52
|
};
|
|
50
53
|
|
|
54
|
+
const PRESET_LABELS: Record<NonNullable<UserProfile["preset"]>, string> = {
|
|
55
|
+
home: "Home",
|
|
56
|
+
work: "Work",
|
|
57
|
+
dev: "Developer",
|
|
58
|
+
custom: "Everything",
|
|
59
|
+
};
|
|
60
|
+
|
|
51
61
|
function describeCredential(c: Credential): string {
|
|
52
62
|
const keys = Object.keys(c.params).filter((k) => k !== "base_url" && k !== "extra_headers");
|
|
53
63
|
if (keys.length === 0) return "Not configured";
|
|
@@ -60,7 +70,11 @@ function describeCredential(c: Credential): string {
|
|
|
60
70
|
export function CredentialsPanel() {
|
|
61
71
|
const { state, dispatch } = useAppContext();
|
|
62
72
|
const raw = state.selectedItem.credentials;
|
|
63
|
-
|
|
73
|
+
// Backward compatibility: the second sub-tab used to be keyed
|
|
74
|
+
// "integrations" (when it was called "Built-in integrations"). Honour
|
|
75
|
+
// older deep links and saved selections by routing them to the new
|
|
76
|
+
// "network" key.
|
|
77
|
+
const active: Sub = raw === "network" || raw === "integrations" ? "network" : "list";
|
|
64
78
|
|
|
65
79
|
const setSub = (s: Sub) =>
|
|
66
80
|
dispatch({ type: "SET_SELECTION", tab: "credentials", itemId: s });
|
|
@@ -72,7 +86,7 @@ export function CredentialsPanel() {
|
|
|
72
86
|
aria-label="Credentials sub-section"
|
|
73
87
|
className="flex gap-1 border-b border-[var(--border)] bg-[var(--bg-secondary)] px-3 pt-2"
|
|
74
88
|
>
|
|
75
|
-
{(["list", "
|
|
89
|
+
{(["list", "network"] as Sub[]).map((s) => {
|
|
76
90
|
const selected = s === active;
|
|
77
91
|
return (
|
|
78
92
|
<button
|
|
@@ -94,32 +108,39 @@ export function CredentialsPanel() {
|
|
|
94
108
|
})}
|
|
95
109
|
</div>
|
|
96
110
|
<div className="flex-1 min-h-0 overflow-auto">
|
|
97
|
-
{active === "list" ? <CredentialsListPanel /> : <
|
|
111
|
+
{active === "list" ? <CredentialsListPanel /> : <NetworkPanel />}
|
|
98
112
|
</div>
|
|
99
113
|
</div>
|
|
100
114
|
);
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
function CredentialsListPanel() {
|
|
117
|
+
export function CredentialsListPanel() {
|
|
104
118
|
const { dispatch } = useAppContext();
|
|
105
119
|
const [credentials, setCredentials] = useState<Credential[]>([]);
|
|
106
120
|
const [defs, setDefs] = useState<IntegrationDefinition[]>([]);
|
|
121
|
+
const [statuses, setStatuses] = useState<Record<string, IntegrationStatus>>({});
|
|
122
|
+
const [preset, setPreset] = useState<UserProfile["preset"]>(null);
|
|
107
123
|
const [loading, setLoading] = useState(true);
|
|
108
124
|
const [addOpen, setAddOpen] = useState(false);
|
|
109
125
|
const [editingProvider, setEditingProvider] = useState<string | null>(null);
|
|
126
|
+
const [expandedProvider, setExpandedProvider] = useState<string | null>(null);
|
|
110
127
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
111
128
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
112
129
|
useDeepLinkScroll("credentials", "credential", containerRef);
|
|
130
|
+
useDeepLinkScroll("credentials", "integration", containerRef);
|
|
113
131
|
|
|
114
132
|
const refresh = useCallback(async () => {
|
|
115
133
|
setLoading(true);
|
|
116
134
|
try {
|
|
117
|
-
const [rows, ints] = await Promise.all([
|
|
135
|
+
const [rows, ints, profile] = await Promise.all([
|
|
118
136
|
api.credentials.list(),
|
|
119
|
-
api.integrations.list().catch(() => ({ definitions: [] as IntegrationDefinition[], statuses: [] })),
|
|
137
|
+
api.integrations.list().catch(() => ({ definitions: [] as IntegrationDefinition[], statuses: [] as IntegrationStatus[] })),
|
|
138
|
+
api.profile.get().catch(() => null),
|
|
120
139
|
]);
|
|
121
140
|
setCredentials(rows);
|
|
122
141
|
setDefs(ints.definitions);
|
|
142
|
+
setStatuses(Object.fromEntries(ints.statuses.map((s) => [s.name, s])));
|
|
143
|
+
setPreset(profile?.preset ?? null);
|
|
123
144
|
} finally {
|
|
124
145
|
setLoading(false);
|
|
125
146
|
}
|
|
@@ -154,35 +175,74 @@ function CredentialsListPanel() {
|
|
|
154
175
|
}
|
|
155
176
|
}
|
|
156
177
|
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
178
|
+
// Persona filter: if the user has chosen a preset, hide unconfigured
|
|
179
|
+
// integrations outside that bucket. Configured-but-out-of-bucket
|
|
180
|
+
// entries stay visible so a saved credential never silently vanishes.
|
|
181
|
+
const visibleDefs = useMemo(() => {
|
|
182
|
+
if (!preset) return defs;
|
|
183
|
+
const allowed = PRESET_CATEGORIES[preset];
|
|
184
|
+
if (allowed === null) return defs;
|
|
185
|
+
return defs.filter((def) => {
|
|
186
|
+
if (!def.category) return true;
|
|
187
|
+
if (allowed.has(def.category)) return true;
|
|
188
|
+
return statuses[def.name]?.configured === true;
|
|
189
|
+
});
|
|
190
|
+
}, [defs, preset, statuses]);
|
|
163
191
|
|
|
192
|
+
const hiddenCount = defs.length - visibleDefs.length;
|
|
193
|
+
|
|
194
|
+
// Group: every visible definition belongs in its category bucket
|
|
195
|
+
// (renders as IntegrationCard); credentials WITHOUT a matching
|
|
196
|
+
// definition (legacy model rows etc.) also fall into the right
|
|
197
|
+
// bucket so the user sees a single grouped list.
|
|
164
198
|
const grouped = useMemo(() => {
|
|
165
|
-
|
|
199
|
+
type Row =
|
|
200
|
+
| { kind: "def"; def: IntegrationDefinition }
|
|
201
|
+
| { kind: "credential"; credential: Credential };
|
|
202
|
+
const byCat = new Map<Category, Row[]>();
|
|
203
|
+
const defNames = new Set(visibleDefs.map((d) => d.name));
|
|
204
|
+
|
|
205
|
+
for (const def of visibleDefs) {
|
|
206
|
+
const cat = (def.category ?? "other") as Category;
|
|
207
|
+
const arr = byCat.get(cat) ?? [];
|
|
208
|
+
arr.push({ kind: "def", def });
|
|
209
|
+
byCat.set(cat, arr);
|
|
210
|
+
}
|
|
166
211
|
for (const c of credentials) {
|
|
212
|
+
if (defNames.has(c.provider)) continue; // covered by the integration card
|
|
167
213
|
const cat = (defByName.get(c.provider)?.category ?? "other") as Category;
|
|
168
214
|
const arr = byCat.get(cat) ?? [];
|
|
169
|
-
arr.push(c);
|
|
215
|
+
arr.push({ kind: "credential", credential: c });
|
|
170
216
|
byCat.set(cat, arr);
|
|
171
217
|
}
|
|
172
218
|
return CATEGORY_ORDER
|
|
173
219
|
.filter((c) => byCat.has(c))
|
|
174
220
|
.map((c) => [c, byCat.get(c)!] as const);
|
|
175
|
-
}, [credentials, defByName]);
|
|
221
|
+
}, [visibleDefs, credentials, defByName]);
|
|
176
222
|
|
|
177
223
|
return (
|
|
178
224
|
<div className="flex flex-col h-full">
|
|
179
225
|
<div className="border-b border-border px-4 py-3 flex items-center gap-2">
|
|
180
226
|
<Key size={14} className="text-fg-subtle" />
|
|
181
227
|
<h2 className="text-sm font-semibold text-fg mr-auto">Credentials</h2>
|
|
228
|
+
{preset && preset !== "custom" && (
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
onClick={() => dispatch({ type: "SET_TAB", tab: "profile" })}
|
|
232
|
+
title={`Filtered to "${PRESET_LABELS[preset]}" preset. Click to change in Profile.`}
|
|
233
|
+
className="inline-flex items-center gap-1 px-2 py-1 text-[11px] rounded-full border border-border bg-surface-2 text-fg-muted hover:bg-surface-3"
|
|
234
|
+
>
|
|
235
|
+
<Filter size={11} />
|
|
236
|
+
<span>{PRESET_LABELS[preset]}</span>
|
|
237
|
+
{hiddenCount > 0 && (
|
|
238
|
+
<span className="text-fg-faint">· {hiddenCount} hidden</span>
|
|
239
|
+
)}
|
|
240
|
+
</button>
|
|
241
|
+
)}
|
|
182
242
|
<button
|
|
183
243
|
onClick={() => setAddOpen(true)}
|
|
184
244
|
className="flex items-center gap-1 text-xs text-accent hover:text-accent-hover transition-colors"
|
|
185
|
-
title="Pick a provider and connect it. Same
|
|
245
|
+
title="Pick a provider and connect it. Same editors are available inline below."
|
|
186
246
|
>
|
|
187
247
|
<Plus size={14} /> Add credential
|
|
188
248
|
</button>
|
|
@@ -190,8 +250,10 @@ function CredentialsListPanel() {
|
|
|
190
250
|
|
|
191
251
|
<div ref={containerRef} className="flex-1 overflow-y-auto">
|
|
192
252
|
<div className="px-4 py-2 space-y-4">
|
|
193
|
-
{loading && credentials.length === 0 &&
|
|
194
|
-
|
|
253
|
+
{loading && credentials.length === 0 && defs.length === 0 && (
|
|
254
|
+
<p className="text-fg-faint text-sm py-6 text-center">Loading…</p>
|
|
255
|
+
)}
|
|
256
|
+
{!loading && grouped.length === 0 && (
|
|
195
257
|
<p className="text-fg-faint text-sm py-6 text-center">
|
|
196
258
|
No credentials yet. Click <span className="font-medium">+ Add credential</span> to connect a model provider (OpenAI, Anthropic, Gemini…) or an integration (Gmail, GitHub, Atlassian…).
|
|
197
259
|
</p>
|
|
@@ -202,30 +264,67 @@ function CredentialsListPanel() {
|
|
|
202
264
|
{grouped.map(([cat, rows]) => (
|
|
203
265
|
<section key={cat}>
|
|
204
266
|
<h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-1 px-1">{CATEGORY_LABELS[cat]}</h3>
|
|
205
|
-
<div className="
|
|
206
|
-
{rows.map((
|
|
207
|
-
|
|
208
|
-
|
|
267
|
+
<div className="space-y-2">
|
|
268
|
+
{rows.map((row) => {
|
|
269
|
+
if (row.kind === "def") {
|
|
270
|
+
const def = row.def;
|
|
271
|
+
const status = statuses[def.name];
|
|
272
|
+
const isOpen = expandedProvider === def.name || !!status?.configured;
|
|
273
|
+
if (!isOpen) {
|
|
274
|
+
// Collapse unconfigured integrations into a compact row.
|
|
275
|
+
return (
|
|
276
|
+
<div
|
|
277
|
+
key={def.name}
|
|
278
|
+
data-deep-link-id={def.name}
|
|
279
|
+
className="flex items-center gap-3 px-3 py-2.5 rounded-lg border border-border/60 bg-surface-2/40"
|
|
280
|
+
>
|
|
281
|
+
<div className="w-1.5 h-1.5 rounded-full bg-fg-faint" />
|
|
282
|
+
<div className="flex-1 min-w-0">
|
|
283
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
284
|
+
<span className="text-sm font-medium text-fg truncate">{def.label}</span>
|
|
285
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded border bg-surface-3 text-fg-muted border-border font-mono">{def.name}</span>
|
|
286
|
+
</div>
|
|
287
|
+
<p className="text-[11px] text-fg-subtle truncate">{def.description}</p>
|
|
288
|
+
</div>
|
|
289
|
+
<button
|
|
290
|
+
onClick={() => setExpandedProvider(def.name)}
|
|
291
|
+
className="text-[11px] text-accent hover:text-accent-hover shrink-0"
|
|
292
|
+
title="Open editor"
|
|
293
|
+
>
|
|
294
|
+
Connect
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return (
|
|
300
|
+
<IntegrationCard
|
|
301
|
+
key={def.name}
|
|
302
|
+
definition={def}
|
|
303
|
+
status={status}
|
|
304
|
+
onChanged={refresh}
|
|
305
|
+
/>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
// Legacy credential row (no matching integration definition).
|
|
309
|
+
const c = row.credential;
|
|
209
310
|
return (
|
|
210
|
-
<div key={c.id} data-deep-link-id={c.id} className="flex items-center gap-3 px-3 py-2.5 group">
|
|
311
|
+
<div key={c.id} data-deep-link-id={c.id} className="flex items-center gap-3 px-3 py-2.5 rounded-lg border border-border/60 bg-surface-2/40 group">
|
|
211
312
|
<div className="flex-1 min-w-0">
|
|
212
313
|
<div className="flex items-center gap-2 mb-0.5">
|
|
213
|
-
<span className="text-sm font-medium text-fg truncate">{
|
|
314
|
+
<span className="text-sm font-medium text-fg truncate">{c.provider}</span>
|
|
214
315
|
<span className="text-[10px] px-1.5 py-0.5 rounded border bg-surface-3 text-fg-muted border-border font-mono">{c.provider}</span>
|
|
215
316
|
<span className="text-[10px] text-fg-faint">{c.auth_method}</span>
|
|
216
317
|
</div>
|
|
217
318
|
<p className="text-[11px] text-fg-subtle">{describeCredential(c)}</p>
|
|
218
319
|
</div>
|
|
219
320
|
<div className="flex gap-1 opacity-40 group-hover:opacity-100 pointer-coarse:opacity-100 transition-opacity shrink-0">
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
</button>
|
|
228
|
-
)}
|
|
321
|
+
<button
|
|
322
|
+
onClick={() => setEditingProvider(c.provider)}
|
|
323
|
+
className="p-1 text-fg-subtle hover:text-fg transition-colors"
|
|
324
|
+
title="Edit credential"
|
|
325
|
+
>
|
|
326
|
+
<Pencil size={13} />
|
|
327
|
+
</button>
|
|
229
328
|
<button
|
|
230
329
|
onClick={() => handleDelete(c)}
|
|
231
330
|
className="p-1 text-fg-subtle hover:text-red-700 dark:hover:text-red-400 transition-colors"
|
|
@@ -247,7 +346,6 @@ function CredentialsListPanel() {
|
|
|
247
346
|
<AddCredentialDialog
|
|
248
347
|
onClose={() => { setAddOpen(false); refresh(); }}
|
|
249
348
|
onSaved={() => refresh()}
|
|
250
|
-
onOpenInIntegrations={() => { setAddOpen(false); openIntegrationsSubTab(); }}
|
|
251
349
|
/>
|
|
252
350
|
)}
|
|
253
351
|
{editingProvider && (
|
|
@@ -255,7 +353,6 @@ function CredentialsListPanel() {
|
|
|
255
353
|
directProviderName={editingProvider}
|
|
256
354
|
onClose={() => { setEditingProvider(null); refresh(); }}
|
|
257
355
|
onSaved={() => refresh()}
|
|
258
|
-
onOpenInIntegrations={() => { setEditingProvider(null); openIntegrationsSubTab(); }}
|
|
259
356
|
/>
|
|
260
357
|
)}
|
|
261
358
|
</div>
|