@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,177 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { CheckCircle2, ExternalLink,
|
|
3
|
-
import { useEffect,
|
|
2
|
+
import { CheckCircle2, ExternalLink, Link as LinkIcon, Loader2, Terminal, Trash2, XCircle } from "lucide-react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { api } from "@/api/client";
|
|
5
|
-
import type { IntegrationDefinition, IntegrationStatus
|
|
6
|
-
import { useDeepLinkScroll } from "@/hooks/useDeepLinkScroll";
|
|
7
|
-
import { useAppContext } from "@/contexts/AppContext";
|
|
8
|
-
import { PRESET_CATEGORIES } from "@/lib/integrations/categories";
|
|
9
|
-
import { NetworkSection } from "./NetworkSection";
|
|
10
|
-
import { AllowedSitesSection } from "./AllowedSitesSection";
|
|
11
|
-
import { EnvAliasEditor } from "./EnvAliasEditor";
|
|
5
|
+
import type { IntegrationDefinition, IntegrationStatus } from "@/api/types";
|
|
12
6
|
|
|
13
7
|
const SECRET_MASK = "********";
|
|
14
8
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
work: "Work",
|
|
21
|
-
dev: "Developer",
|
|
22
|
-
custom: "Everything",
|
|
23
|
-
};
|
|
9
|
+
// Per-integration editor with inline OAuth Connect, Save, Test, Clear, and
|
|
10
|
+
// (for Gmail/Outlook) a collapsible setup guide. Extracted from the old
|
|
11
|
+
// `components/integrations/IntegrationsPanel.tsx` so the unified credentials
|
|
12
|
+
// panel can render one card per known integration — keeping OAuth and key
|
|
13
|
+
// editing in the same surface that handles model API keys.
|
|
24
14
|
|
|
25
|
-
export function
|
|
26
|
-
const { dispatch } = useAppContext();
|
|
27
|
-
const [defs, setDefs] = useState<IntegrationDefinition[]>([]);
|
|
28
|
-
const [statuses, setStatuses] = useState<Record<string, IntegrationStatus>>({});
|
|
29
|
-
const [loading, setLoading] = useState(true);
|
|
30
|
-
const [syncing, setSyncing] = useState(false);
|
|
31
|
-
const [syncMsg, setSyncMsg] = useState<string | null>(null);
|
|
32
|
-
const [aliasEditorOpen, setAliasEditorOpen] = useState(false);
|
|
33
|
-
const [preset, setPreset] = useState<UserProfile["preset"]>(null);
|
|
34
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
35
|
-
useDeepLinkScroll("credentials", "integration", containerRef);
|
|
36
|
-
|
|
37
|
-
async function load() {
|
|
38
|
-
setLoading(true);
|
|
39
|
-
try {
|
|
40
|
-
const [res, profile] = await Promise.all([
|
|
41
|
-
api.integrations.list(),
|
|
42
|
-
api.profile.get().catch(() => null),
|
|
43
|
-
]);
|
|
44
|
-
setDefs(res.definitions);
|
|
45
|
-
setStatuses(Object.fromEntries(res.statuses.map((s) => [s.name, s])));
|
|
46
|
-
setPreset(profile?.preset ?? null);
|
|
47
|
-
} catch (e) { console.error(e); }
|
|
48
|
-
finally { setLoading(false); }
|
|
49
|
-
}
|
|
50
|
-
useEffect(() => { void load(); }, []);
|
|
51
|
-
|
|
52
|
-
// Persona filter: if the user has chosen a preset, hide integrations
|
|
53
|
-
// outside that bucket. "custom" or unset → show everything (legacy
|
|
54
|
-
// behaviour). Configured-but-out-of-bucket entries are kept visible
|
|
55
|
-
// so a previously-saved credential is never silently hidden.
|
|
56
|
-
const visibleDefs = useMemo(() => {
|
|
57
|
-
if (!preset) return defs;
|
|
58
|
-
const allowed = PRESET_CATEGORIES[preset];
|
|
59
|
-
if (allowed === null) return defs;
|
|
60
|
-
return defs.filter((def) => {
|
|
61
|
-
if (!def.category) return true;
|
|
62
|
-
if (allowed.has(def.category)) return true;
|
|
63
|
-
// Don't hide something the user has already configured — bail out
|
|
64
|
-
// gracefully so credentials never appear to vanish.
|
|
65
|
-
return statuses[def.name]?.configured === true;
|
|
66
|
-
});
|
|
67
|
-
}, [defs, preset, statuses]);
|
|
68
|
-
|
|
69
|
-
const hiddenCount = defs.length - visibleDefs.length;
|
|
70
|
-
|
|
71
|
-
async function syncFromEnv() {
|
|
72
|
-
setSyncing(true);
|
|
73
|
-
setSyncMsg(null);
|
|
74
|
-
try {
|
|
75
|
-
const r = await api.envSync.apply();
|
|
76
|
-
const sourceLabel = r.discovered.source === "shell-rc"
|
|
77
|
-
? `your ${r.discovered.shell ?? "shell"} rc`
|
|
78
|
-
: r.discovered.source === "windows-registry"
|
|
79
|
-
? "your Windows User env"
|
|
80
|
-
: "the process env";
|
|
81
|
-
if (r.applied_count > 0) {
|
|
82
|
-
setSyncMsg(`Synced ${r.applied_count} field(s) from ${sourceLabel}.`);
|
|
83
|
-
} else {
|
|
84
|
-
const userSkipped = r.candidates.filter((c) => c.action === "skipped-user").length;
|
|
85
|
-
const equal = r.candidates.filter((c) => c.action === "skipped-equal").length;
|
|
86
|
-
const absent = r.candidates.filter((c) => c.action === "absent").length;
|
|
87
|
-
if (userSkipped > 0) {
|
|
88
|
-
setSyncMsg(`Nothing to write — ${userSkipped} field(s) were edited here and won't be overwritten.`);
|
|
89
|
-
} else if (equal > 0 && absent === r.candidates.length - equal) {
|
|
90
|
-
setSyncMsg(`Already up to date with ${sourceLabel}.`);
|
|
91
|
-
} else {
|
|
92
|
-
setSyncMsg(`No matching env vars set in ${sourceLabel}.`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
await load();
|
|
96
|
-
} catch (e) {
|
|
97
|
-
setSyncMsg(e instanceof Error ? e.message : String(e));
|
|
98
|
-
} finally {
|
|
99
|
-
setSyncing(false);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<div className="flex flex-col h-full">
|
|
105
|
-
<div className="border-b border-border px-4 py-3 flex items-center gap-2">
|
|
106
|
-
<Key size={14} className="text-fg-subtle" />
|
|
107
|
-
<h2 className="text-sm font-semibold text-fg mr-auto">Built-in integrations</h2>
|
|
108
|
-
{preset && preset !== "custom" && (
|
|
109
|
-
<button
|
|
110
|
-
type="button"
|
|
111
|
-
onClick={() => dispatch({ type: "SET_TAB", tab: "profile" })}
|
|
112
|
-
title={`Filtered to "${PRESET_LABELS[preset]}" preset. Click to change in Profile.`}
|
|
113
|
-
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"
|
|
114
|
-
>
|
|
115
|
-
<Filter size={11} />
|
|
116
|
-
<span>{PRESET_LABELS[preset]}</span>
|
|
117
|
-
{hiddenCount > 0 && (
|
|
118
|
-
<span className="text-fg-faint">· {hiddenCount} hidden</span>
|
|
119
|
-
)}
|
|
120
|
-
</button>
|
|
121
|
-
)}
|
|
122
|
-
<button
|
|
123
|
-
onClick={() => setAliasEditorOpen((v) => !v)}
|
|
124
|
-
title="Add additional env-var name aliases that env-sync should look for, per integration field."
|
|
125
|
-
className="inline-flex items-center gap-1 px-2 py-1 text-[11px] rounded border border-border text-fg-muted hover:bg-surface-3"
|
|
126
|
-
>
|
|
127
|
-
<Settings2 size={11} />
|
|
128
|
-
Aliases
|
|
129
|
-
</button>
|
|
130
|
-
<button
|
|
131
|
-
onClick={syncFromEnv}
|
|
132
|
-
disabled={syncing}
|
|
133
|
-
title="Pull standard credential env vars (GITHUB_TOKEN, ATLASSIAN_API_TOKEN, …) from your shell rc / Windows User env. Fields you've edited here are never overwritten."
|
|
134
|
-
className="inline-flex items-center gap-1 px-2 py-1 text-[11px] rounded border border-border text-fg-muted hover:bg-surface-3 disabled:opacity-50"
|
|
135
|
-
>
|
|
136
|
-
{syncing ? <Loader2 size={11} className="animate-spin" /> : <RefreshCw size={11} />}
|
|
137
|
-
Sync from environment
|
|
138
|
-
</button>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<div ref={containerRef} className="flex-1 overflow-y-auto px-4 py-3">
|
|
142
|
-
{syncMsg && (
|
|
143
|
-
<div className="mb-3 px-3 py-2 rounded border border-border bg-surface-2 text-[11px] text-fg-muted flex items-start gap-2">
|
|
144
|
-
<Terminal size={12} className="mt-0.5 text-fg-subtle shrink-0" />
|
|
145
|
-
<span className="flex-1">{syncMsg}</span>
|
|
146
|
-
<button onClick={() => setSyncMsg(null)} className="text-fg-faint hover:text-fg">
|
|
147
|
-
<XCircle size={12} />
|
|
148
|
-
</button>
|
|
149
|
-
</div>
|
|
150
|
-
)}
|
|
151
|
-
{aliasEditorOpen && (
|
|
152
|
-
<EnvAliasEditor
|
|
153
|
-
onClose={() => setAliasEditorOpen(false)}
|
|
154
|
-
onSaved={() => { /* re-sync happens on next click of Sync button; nothing to refresh here */ }}
|
|
155
|
-
/>
|
|
156
|
-
)}
|
|
157
|
-
<NetworkSection />
|
|
158
|
-
<AllowedSitesSection />
|
|
159
|
-
{loading && defs.length === 0 && <p className="text-fg-faint text-sm py-6 text-center">Loading…</p>}
|
|
160
|
-
{!loading && defs.length === 0 && <p className="text-fg-faint text-sm py-6 text-center">No integrations available.</p>}
|
|
161
|
-
{visibleDefs.map((def) => (
|
|
162
|
-
<IntegrationCard
|
|
163
|
-
key={def.name}
|
|
164
|
-
definition={def}
|
|
165
|
-
status={statuses[def.name]}
|
|
166
|
-
onChanged={load}
|
|
167
|
-
/>
|
|
168
|
-
))}
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function IntegrationCard({
|
|
15
|
+
export function IntegrationCard({
|
|
175
16
|
definition: def,
|
|
176
17
|
status,
|
|
177
18
|
onChanged,
|
|
@@ -87,10 +87,10 @@ function buildPayload(s: FormState): CreatePayload | null {
|
|
|
87
87
|
|
|
88
88
|
function kindHint(kind: DocumentSourceKind): string {
|
|
89
89
|
if (kind === "local_folder") return "Pick a folder on this machine.";
|
|
90
|
-
if (kind === "gmail_mail") return "Requires Gmail credentials (Credentials →
|
|
91
|
-
if (kind === "outlook_mail") return "Requires Outlook credentials (Credentials →
|
|
92
|
-
if (isGithubKind(kind)) return "Requires GitHub credentials (Credentials →
|
|
93
|
-
return "Requires Atlassian credentials (Credentials →
|
|
90
|
+
if (kind === "gmail_mail") return "Requires Gmail credentials (Credentials → Gmail).";
|
|
91
|
+
if (kind === "outlook_mail") return "Requires Outlook credentials (Credentials → Outlook).";
|
|
92
|
+
if (isGithubKind(kind)) return "Requires GitHub credentials (Credentials → GitHub).";
|
|
93
|
+
return "Requires Atlassian credentials (Credentials → Atlassian).";
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export function AddSourceForm({ disabled, onSubmit }: Props) {
|
|
@@ -31,7 +31,7 @@ export function DocumentsPanel() {
|
|
|
31
31
|
<p className="text-xs text-fg-faint leading-relaxed">
|
|
32
32
|
Sources listed here are indexed in the background. Text files in folders are chunked, embedded, and
|
|
33
33
|
made available to agents via the <code className="font-mono text-fg-muted">documents_search</code> tool.
|
|
34
|
-
Remote sources reuse credentials configured in <em>Credentials
|
|
34
|
+
Remote sources reuse credentials configured in <em>Credentials</em>:
|
|
35
35
|
{" "}Jira/Confluence under <em>Atlassian</em>, GitHub PRs/repos under <em>GitHub</em>, and mail under <em>Gmail</em>/<em>Outlook</em>.
|
|
36
36
|
Embedding uses your default model provider; without one, search falls back to substring match.
|
|
37
37
|
</p>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Globe, Loader2, RefreshCw, Settings2, Terminal, XCircle } from "lucide-react";
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
import { api } from "@/api/client";
|
|
5
|
+
import { useDeepLinkScroll } from "@/hooks/useDeepLinkScroll";
|
|
6
|
+
import { NetworkSection } from "./NetworkSection";
|
|
7
|
+
import { AllowedSitesSection } from "./AllowedSitesSection";
|
|
8
|
+
import { EnvAliasEditor } from "./EnvAliasEditor";
|
|
9
|
+
|
|
10
|
+
// "Network & environment" hosts everything that's NOT a credential: HTTP
|
|
11
|
+
// proxy, allowed sites, env-var aliases, and the env-sync button that
|
|
12
|
+
// pulls credential env vars (GITHUB_TOKEN, ATLASSIAN_API_TOKEN, …) from
|
|
13
|
+
// the user's shell rc / Windows User env into the unified credentials
|
|
14
|
+
// store. Per-integration auth (keys + OAuth) lives in the sibling
|
|
15
|
+
// Credentials sub-tab.
|
|
16
|
+
|
|
17
|
+
export function NetworkPanel() {
|
|
18
|
+
const [syncing, setSyncing] = useState(false);
|
|
19
|
+
const [syncMsg, setSyncMsg] = useState<string | null>(null);
|
|
20
|
+
const [aliasEditorOpen, setAliasEditorOpen] = useState(false);
|
|
21
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
useDeepLinkScroll("credentials", "network", containerRef);
|
|
23
|
+
|
|
24
|
+
async function syncFromEnv() {
|
|
25
|
+
setSyncing(true);
|
|
26
|
+
setSyncMsg(null);
|
|
27
|
+
try {
|
|
28
|
+
const r = await api.envSync.apply();
|
|
29
|
+
const sourceLabel = r.discovered.source === "shell-rc"
|
|
30
|
+
? `your ${r.discovered.shell ?? "shell"} rc`
|
|
31
|
+
: r.discovered.source === "windows-registry"
|
|
32
|
+
? "your Windows User env"
|
|
33
|
+
: "the process env";
|
|
34
|
+
if (r.applied_count > 0) {
|
|
35
|
+
setSyncMsg(`Synced ${r.applied_count} field(s) from ${sourceLabel}.`);
|
|
36
|
+
} else {
|
|
37
|
+
const userSkipped = r.candidates.filter((c) => c.action === "skipped-user").length;
|
|
38
|
+
const equal = r.candidates.filter((c) => c.action === "skipped-equal").length;
|
|
39
|
+
const absent = r.candidates.filter((c) => c.action === "absent").length;
|
|
40
|
+
if (userSkipped > 0) {
|
|
41
|
+
setSyncMsg(`Nothing to write — ${userSkipped} field(s) were edited here and won't be overwritten.`);
|
|
42
|
+
} else if (equal > 0 && absent === r.candidates.length - equal) {
|
|
43
|
+
setSyncMsg(`Already up to date with ${sourceLabel}.`);
|
|
44
|
+
} else {
|
|
45
|
+
setSyncMsg(`No matching env vars set in ${sourceLabel}.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Notify the Credentials list so it re-loads any newly-synced rows.
|
|
49
|
+
if (typeof window !== "undefined") {
|
|
50
|
+
window.dispatchEvent(new CustomEvent("jarela:credentials-changed"));
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
setSyncMsg(e instanceof Error ? e.message : String(e));
|
|
54
|
+
} finally {
|
|
55
|
+
setSyncing(false);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="flex flex-col h-full">
|
|
61
|
+
<div className="border-b border-border px-4 py-3 flex items-center gap-2">
|
|
62
|
+
<Globe size={14} className="text-fg-subtle" />
|
|
63
|
+
<h2 className="text-sm font-semibold text-fg mr-auto">Network & environment</h2>
|
|
64
|
+
<button
|
|
65
|
+
onClick={() => setAliasEditorOpen((v) => !v)}
|
|
66
|
+
title="Add additional env-var name aliases that env-sync should look for, per integration field."
|
|
67
|
+
className="inline-flex items-center gap-1 px-2 py-1 text-[11px] rounded border border-border text-fg-muted hover:bg-surface-3"
|
|
68
|
+
>
|
|
69
|
+
<Settings2 size={11} />
|
|
70
|
+
Aliases
|
|
71
|
+
</button>
|
|
72
|
+
<button
|
|
73
|
+
onClick={syncFromEnv}
|
|
74
|
+
disabled={syncing}
|
|
75
|
+
title="Pull standard credential env vars (GITHUB_TOKEN, ATLASSIAN_API_TOKEN, …) from your shell rc / Windows User env into the Credentials list. Fields you've edited there are never overwritten."
|
|
76
|
+
className="inline-flex items-center gap-1 px-2 py-1 text-[11px] rounded border border-border text-fg-muted hover:bg-surface-3 disabled:opacity-50"
|
|
77
|
+
>
|
|
78
|
+
{syncing ? <Loader2 size={11} className="animate-spin" /> : <RefreshCw size={11} />}
|
|
79
|
+
Sync from environment
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div ref={containerRef} className="flex-1 overflow-y-auto px-4 py-3">
|
|
84
|
+
{syncMsg && (
|
|
85
|
+
<div className="mb-3 px-3 py-2 rounded border border-border bg-surface-2 text-[11px] text-fg-muted flex items-start gap-2">
|
|
86
|
+
<Terminal size={12} className="mt-0.5 text-fg-subtle shrink-0" />
|
|
87
|
+
<span className="flex-1">{syncMsg}</span>
|
|
88
|
+
<button onClick={() => setSyncMsg(null)} className="text-fg-faint hover:text-fg">
|
|
89
|
+
<XCircle size={12} />
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
{aliasEditorOpen && (
|
|
94
|
+
<EnvAliasEditor
|
|
95
|
+
onClose={() => setAliasEditorOpen(false)}
|
|
96
|
+
onSaved={() => { /* re-sync happens on next click of Sync button; nothing to refresh here */ }}
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
<NetworkSection />
|
|
100
|
+
<AllowedSitesSection />
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -22,6 +22,7 @@ import { BridgesPanel } from "@/components/bridges/BridgesPanel";
|
|
|
22
22
|
import { HarnessPanel } from "@/components/harness/HarnessPanel";
|
|
23
23
|
import { LogsPanel } from "@/components/logs/LogsPanel";
|
|
24
24
|
import { EnvVarsPanel } from "@/components/env/EnvVarsPanel";
|
|
25
|
+
import { SettingsPanel } from "@/components/settings/SettingsPanel";
|
|
25
26
|
import { HeaderActivity } from "@/components/ui/HeaderActivity";
|
|
26
27
|
import { NotificationStatus } from "@/components/ui/NotificationStatus";
|
|
27
28
|
import { CryptoFallbackBanner } from "@/components/ui/CryptoFallbackBanner";
|
|
@@ -548,6 +549,11 @@ export function AppShell() {
|
|
|
548
549
|
<EnvVarsPanel />
|
|
549
550
|
</Activity>
|
|
550
551
|
)}
|
|
552
|
+
{mountedTabs.has("settings") && (
|
|
553
|
+
<Activity mode={state.activeTab === "settings" ? "visible" : "hidden"}>
|
|
554
|
+
<SettingsPanel />
|
|
555
|
+
</Activity>
|
|
556
|
+
)}
|
|
551
557
|
|
|
552
558
|
{showMenu && (
|
|
553
559
|
<div
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { BarChart3, Bot, Brain, Calendar, ChevronDown, Cpu, FolderSearch, Key, MessageSquare,
|
|
2
|
+
import { BarChart3, Bot, Brain, Calendar, ChevronDown, Cpu, FolderSearch, Key, MessageSquare, ScrollText, ServerCog, Settings, Shapes, Smartphone, User, Wrench } from "lucide-react";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import { useAppContext, type Tab } from "@/contexts/AppContext";
|
|
5
5
|
import type { AgentConfig } from "@/api/types";
|
|
6
6
|
import { api } from "@/api/client";
|
|
7
7
|
import { useUnreadByAgent } from "@/lib/ui/toasts";
|
|
8
|
-
import { useTheme, type Theme } from "@/contexts/ThemeContext";
|
|
9
8
|
|
|
10
9
|
interface Props {
|
|
11
10
|
activeTab: Tab;
|
|
@@ -32,6 +31,7 @@ const TAB_ICONS: Record<Tab, React.ReactNode> = {
|
|
|
32
31
|
harness: <Shapes size={13} />,
|
|
33
32
|
logs: <ScrollText size={13} />,
|
|
34
33
|
env: <ServerCog size={13} />,
|
|
34
|
+
settings: <Settings size={13} />,
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
const TAB_TITLES: Record<Tab, string> = {
|
|
@@ -51,6 +51,7 @@ const TAB_TITLES: Record<Tab, string> = {
|
|
|
51
51
|
harness: "Harness",
|
|
52
52
|
logs: "Logs",
|
|
53
53
|
env: "Defaults",
|
|
54
|
+
settings: "Settings",
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
const TAB_SHORT: Record<Tab, string> = {
|
|
@@ -70,11 +71,14 @@ const TAB_SHORT: Record<Tab, string> = {
|
|
|
70
71
|
harness: "Test",
|
|
71
72
|
logs: "Logs",
|
|
72
73
|
env: "Defaults",
|
|
74
|
+
settings: "Setup",
|
|
73
75
|
};
|
|
74
76
|
|
|
75
|
-
// Two-tier menu. "Common" surfaces the day-to-day verbs
|
|
76
|
-
//
|
|
77
|
-
//
|
|
77
|
+
// Two-tier menu. "Common" surfaces the day-to-day verbs. Settings is
|
|
78
|
+
// the single home for previously top-level chrome (credentials, models,
|
|
79
|
+
// harness, logs, defaults, appearance, networking) — those legacy Tab
|
|
80
|
+
// values still exist for deep-link compat but no longer appear in this
|
|
81
|
+
// menu grid.
|
|
78
82
|
//
|
|
79
83
|
// Capability-presence surfaces (documents, memory, MCP, extensions,
|
|
80
84
|
// bridges, built-in tool categories) all live under the "Tools" tab now —
|
|
@@ -82,15 +86,8 @@ const TAB_SHORT: Record<Tab, string> = {
|
|
|
82
86
|
// answering the same question: "what can the agent see / do?". The legacy
|
|
83
87
|
// top-level Tab entries remain wired so deep-links (?tab=documents&item=…)
|
|
84
88
|
// still resolve, but they're hidden from the menu grid.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// keys + built-in integrations (Gmail, GitHub, Atlassian…) live there as
|
|
88
|
-
// sub-tabs.
|
|
89
|
-
//
|
|
90
|
-
// "models" lives in Advanced — most users let the default model do its
|
|
91
|
-
// job; switching providers is power-user territory.
|
|
92
|
-
const COMMON_TABS: Tab[] = ["chat", "dashboard", "agents", "credentials", "tools", "tasks", "profile"];
|
|
93
|
-
const ADVANCED_TABS: Tab[] = ["models", "harness", "logs", "env"];
|
|
89
|
+
const COMMON_TABS: Tab[] = ["chat", "dashboard", "agents", "tools", "tasks", "profile", "settings"];
|
|
90
|
+
const ADVANCED_TABS: Tab[] = [];
|
|
94
91
|
|
|
95
92
|
const ADVANCED_KEY = "jarela.menu.advanced";
|
|
96
93
|
|
|
@@ -118,6 +115,7 @@ const TAB_ACCENT: Partial<Record<Tab, string>> = {
|
|
|
118
115
|
harness: "from-orange-500/20 to-amber-500/5",
|
|
119
116
|
logs: "from-slate-500/20 to-zinc-500/5",
|
|
120
117
|
env: "from-teal-500/20 to-cyan-500/5",
|
|
118
|
+
settings: "from-slate-500/20 to-zinc-500/5",
|
|
121
119
|
};
|
|
122
120
|
|
|
123
121
|
function avatarGradient(id: string): string {
|
|
@@ -231,12 +229,8 @@ export function MenuPanel({
|
|
|
231
229
|
onAgentChange,
|
|
232
230
|
onSetTab,
|
|
233
231
|
}: Props) {
|
|
234
|
-
const { state
|
|
232
|
+
const { state } = useAppContext();
|
|
235
233
|
const isFullMode = state.experienceMode === "full";
|
|
236
|
-
const setMode = (mode: "essential" | "full") => {
|
|
237
|
-
if ((mode === "full") === isFullMode) return;
|
|
238
|
-
dispatch({ type: "SET_EXPERIENCE_MODE", mode });
|
|
239
|
-
};
|
|
240
234
|
// Advanced section starts collapsed once the user has dismissed it
|
|
241
235
|
// once (persisted to localStorage). Defaults to *expanded* on first
|
|
242
236
|
// boot so the engine room is visible to power users out of the box.
|
|
@@ -295,42 +289,11 @@ export function MenuPanel({
|
|
|
295
289
|
style={{ top: "calc(3rem + var(--app-safe-top))" }}
|
|
296
290
|
>
|
|
297
291
|
{/* Common navigation — the day-to-day surface. */}
|
|
298
|
-
<div className="
|
|
299
|
-
<div className="flex items-center justify-between gap-2">
|
|
300
|
-
<span className="text-[10px] uppercase tracking-wide text-fg-faint">Workspace mode</span>
|
|
301
|
-
<div
|
|
302
|
-
role="radiogroup"
|
|
303
|
-
aria-label="Workspace mode"
|
|
304
|
-
className="inline-flex items-center rounded-full border border-border bg-surface-3 p-0.5"
|
|
305
|
-
>
|
|
306
|
-
{(["essential", "full"] as const).map((mode) => {
|
|
307
|
-
const active = (mode === "full") === isFullMode;
|
|
308
|
-
return (
|
|
309
|
-
<button
|
|
310
|
-
key={mode}
|
|
311
|
-
type="button"
|
|
312
|
-
role="radio"
|
|
313
|
-
aria-checked={active}
|
|
314
|
-
onClick={() => setMode(mode)}
|
|
315
|
-
title={active ? `${mode} mode (current)` : `Switch to ${mode} mode`}
|
|
316
|
-
className={`control-tap text-[10px] uppercase tracking-wide px-2 py-0.5 rounded-full transition-colors ${
|
|
317
|
-
active
|
|
318
|
-
? "bg-accent/15 text-fg-subtle"
|
|
319
|
-
: "text-fg-faint hover:text-fg-muted"
|
|
320
|
-
}`}
|
|
321
|
-
>
|
|
322
|
-
{mode}
|
|
323
|
-
</button>
|
|
324
|
-
);
|
|
325
|
-
})}
|
|
326
|
-
</div>
|
|
327
|
-
</div>
|
|
328
|
-
</div>
|
|
329
|
-
<div className="grid grid-cols-4 sm:grid-cols-6 gap-1.5 px-2 py-2 border-b border-border shrink-0">
|
|
292
|
+
<div className="grid grid-cols-4 sm:grid-cols-6 gap-1.5 px-2 py-2 border-b border-border shrink-0 bg-gradient-to-r from-surface-2/50 to-transparent">
|
|
330
293
|
{COMMON_TABS.map(renderTabButton)}
|
|
331
294
|
</div>
|
|
332
295
|
|
|
333
|
-
{isFullMode && (
|
|
296
|
+
{isFullMode && ADVANCED_TABS.length > 0 && (
|
|
334
297
|
<div className="border-b border-border shrink-0">
|
|
335
298
|
<button
|
|
336
299
|
type="button"
|
|
@@ -359,45 +322,6 @@ export function MenuPanel({
|
|
|
359
322
|
onSelect={(id) => { onAgentChange(id); onSetTab("chat"); onClose(); }}
|
|
360
323
|
/>
|
|
361
324
|
</div>
|
|
362
|
-
|
|
363
|
-
{/* Display toggles */}
|
|
364
|
-
<div className="border-t border-border px-3 py-3 shrink-0 bg-surface-1/30">
|
|
365
|
-
<p className="text-[11px] text-fg-faint mb-1.5 font-medium uppercase tracking-wide">Display</p>
|
|
366
|
-
<div className="flex flex-col gap-1.5">
|
|
367
|
-
<ThemePicker />
|
|
368
|
-
</div>
|
|
369
|
-
</div>
|
|
370
|
-
</div>
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function ThemePicker() {
|
|
375
|
-
const { theme, setTheme } = useTheme();
|
|
376
|
-
const options: { value: Theme; label: string; icon: React.ReactNode }[] = [
|
|
377
|
-
{ value: "light", label: "Light", icon: <Sun size={12} /> },
|
|
378
|
-
{ value: "dark", label: "Dark", icon: <Moon size={12} /> },
|
|
379
|
-
{ value: "system", label: "System", icon: <Monitor size={12} /> },
|
|
380
|
-
];
|
|
381
|
-
return (
|
|
382
|
-
<div className="flex items-center gap-2 text-xs text-fg-muted rounded-lg border border-border bg-surface-3/70 px-2.5 py-2">
|
|
383
|
-
<span className="shrink-0">Theme</span>
|
|
384
|
-
<div className="flex flex-1 rounded-lg border border-border overflow-hidden bg-surface">
|
|
385
|
-
{options.map((o) => (
|
|
386
|
-
<button
|
|
387
|
-
key={o.value}
|
|
388
|
-
onClick={() => setTheme(o.value)}
|
|
389
|
-
title={o.label}
|
|
390
|
-
className={`control-tap flex-1 inline-flex items-center justify-center gap-1 py-1 text-[11px] transition-colors ${
|
|
391
|
-
theme === o.value
|
|
392
|
-
? "bg-surface-3 text-fg shadow-sm"
|
|
393
|
-
: "text-fg-faint hover:text-fg-muted hover:bg-surface-3/50"
|
|
394
|
-
}`}
|
|
395
|
-
>
|
|
396
|
-
{o.icon}
|
|
397
|
-
<span>{o.label}</span>
|
|
398
|
-
</button>
|
|
399
|
-
))}
|
|
400
|
-
</div>
|
|
401
325
|
</div>
|
|
402
326
|
);
|
|
403
327
|
}
|
|
@@ -203,7 +203,7 @@ function PresetPicker({
|
|
|
203
203
|
<div className="block">
|
|
204
204
|
<span className="text-xs text-fg-subtle mb-1 block">Persona</span>
|
|
205
205
|
<p className="text-[11px] text-fg-faint mb-2">
|
|
206
|
-
Filters the
|
|
206
|
+
Filters the Credentials list so you only see integrations relevant
|
|
207
207
|
to how you use Jarela. Pick "Everything" to see them all.
|
|
208
208
|
</p>
|
|
209
209
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
@@ -14,11 +14,11 @@ function approvalToastTarget(action: PendingAction): { href: string; hrefLabel:
|
|
|
14
14
|
switch (action.kind) {
|
|
15
15
|
case "enable_integration": {
|
|
16
16
|
const id = str("id");
|
|
17
|
-
return id ? { href: `?tab=credentials&item
|
|
17
|
+
return id ? { href: `?tab=credentials&item=${encodeURIComponent(id)}`, hrefLabel: "Open in Credentials →", title: `${id} enabled` } : null;
|
|
18
18
|
}
|
|
19
19
|
case "start_oauth": {
|
|
20
20
|
const id = str("integration_id");
|
|
21
|
-
return id ? { href: `?tab=credentials&item
|
|
21
|
+
return id ? { href: `?tab=credentials&item=${encodeURIComponent(id)}`, hrefLabel: "Open in Credentials →", title: `${id} authorized` } : null;
|
|
22
22
|
}
|
|
23
23
|
case "set_provider_key": {
|
|
24
24
|
const name = str("name") ?? str("provider");
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Monitor, Moon, Palette, Sun } from "lucide-react";
|
|
3
|
+
import { useTheme, type Theme } from "@/contexts/ThemeContext";
|
|
4
|
+
import { useAppContext, type ExperienceMode } from "@/contexts/AppContext";
|
|
5
|
+
|
|
6
|
+
// Visual + chrome settings that used to live in the MenuPanel footer
|
|
7
|
+
// (theme) and the top of the menu (Workspace mode). Hoisted here so
|
|
8
|
+
// every settings surface has the same shape: a Settings sub-tab with
|
|
9
|
+
// its own header.
|
|
10
|
+
|
|
11
|
+
export function AppearancePanel() {
|
|
12
|
+
const { state, dispatch } = useAppContext();
|
|
13
|
+
const { theme, setTheme } = useTheme();
|
|
14
|
+
const isFullMode = state.experienceMode === "full";
|
|
15
|
+
|
|
16
|
+
const themeOptions: { value: Theme; label: string; icon: React.ReactNode; description: string }[] = [
|
|
17
|
+
{ value: "light", label: "Light", icon: <Sun size={14} />, description: "Bright UI; ignores system preference." },
|
|
18
|
+
{ value: "dark", label: "Dark", icon: <Moon size={14} />, description: "Dimmed UI; ignores system preference." },
|
|
19
|
+
{ value: "system", label: "System", icon: <Monitor size={14} />, description: "Follow the OS-level light/dark preference." },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const modeOptions: { value: ExperienceMode; label: string; description: string }[] = [
|
|
23
|
+
{ value: "essential", label: "Essential", description: "Day-to-day surfaces only. Hides Memory, Bridges, Harness, Logs, Defaults from the menu." },
|
|
24
|
+
{ value: "full", label: "Full", description: "Everything visible: engine-room tabs, power-user settings, all advanced sub-tabs." },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function setMode(mode: ExperienceMode) {
|
|
28
|
+
if (mode === state.experienceMode) return;
|
|
29
|
+
dispatch({ type: "SET_EXPERIENCE_MODE", mode });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex flex-col h-full">
|
|
34
|
+
<div className="border-b border-border px-4 py-3 flex items-center gap-2">
|
|
35
|
+
<Palette size={14} className="text-fg-subtle" />
|
|
36
|
+
<h2 className="text-sm font-semibold text-fg mr-auto">Appearance</h2>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-6">
|
|
40
|
+
<section>
|
|
41
|
+
<h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Theme</h3>
|
|
42
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
43
|
+
{themeOptions.map((o) => {
|
|
44
|
+
const active = theme === o.value;
|
|
45
|
+
return (
|
|
46
|
+
<button
|
|
47
|
+
key={o.value}
|
|
48
|
+
onClick={() => setTheme(o.value)}
|
|
49
|
+
aria-pressed={active}
|
|
50
|
+
className={`text-left rounded-lg border px-3 py-2.5 transition-colors ${
|
|
51
|
+
active
|
|
52
|
+
? "border-accent bg-accent/10 text-fg"
|
|
53
|
+
: "border-border bg-surface-2 text-fg-muted hover:bg-surface-3"
|
|
54
|
+
}`}
|
|
55
|
+
>
|
|
56
|
+
<div className="flex items-center gap-2 mb-1">
|
|
57
|
+
<span className="text-fg-subtle">{o.icon}</span>
|
|
58
|
+
<span className="text-sm font-medium text-fg">{o.label}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<p className="text-[11px] text-fg-faint leading-snug">{o.description}</p>
|
|
61
|
+
</button>
|
|
62
|
+
);
|
|
63
|
+
})}
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
|
|
67
|
+
<section>
|
|
68
|
+
<h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Workspace mode</h3>
|
|
69
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
70
|
+
{modeOptions.map((o) => {
|
|
71
|
+
const active = isFullMode ? o.value === "full" : o.value === "essential";
|
|
72
|
+
return (
|
|
73
|
+
<button
|
|
74
|
+
key={o.value}
|
|
75
|
+
onClick={() => setMode(o.value)}
|
|
76
|
+
aria-pressed={active}
|
|
77
|
+
className={`text-left rounded-lg border px-3 py-2.5 transition-colors ${
|
|
78
|
+
active
|
|
79
|
+
? "border-accent bg-accent/10 text-fg"
|
|
80
|
+
: "border-border bg-surface-2 text-fg-muted hover:bg-surface-3"
|
|
81
|
+
}`}
|
|
82
|
+
>
|
|
83
|
+
<div className="text-sm font-medium text-fg mb-1">{o.label}</div>
|
|
84
|
+
<p className="text-[11px] text-fg-faint leading-snug">{o.description}</p>
|
|
85
|
+
</button>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|