@circuitwall/jarela 0.8.0 → 0.8.1
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 +1 -1
- 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.js +1 -537
- package/.next/standalone/.next/server/app/_not-found/page.js.map +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +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 +23 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +47 -18
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +47 -18
- 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 +36 -7
- 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/index.html +23 -2
- package/.next/standalone/.next/server/app/index.rsc +46 -17
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +46 -17
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +36 -7
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page.js +79690 -79580
- package/.next/standalone/.next/server/app/page.js.map +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup/page.js +1 -1
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +48 -19
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +48 -19
- package/.next/standalone/.next/server/app/setup.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/_index.segment.rsc +36 -7
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup.segment.rsc +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -1
- package/.next/standalone/.next/server/chunks/4045.js +611 -0
- package/.next/standalone/.next/server/chunks/4045.js.map +1 -0
- package/.next/standalone/.next/server/chunks/5432.js +5 -2
- package/.next/standalone/.next/server/chunks/5432.js.map +1 -1
- package/.next/standalone/.next/server/chunks/{5745.js → 6765.js} +9 -551
- package/.next/standalone/.next/server/chunks/6765.js.map +1 -0
- package/.next/standalone/.next/server/chunks/7885.js +5 -2
- package/.next/standalone/.next/server/chunks/7885.js.map +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/.next/standalone/.next/server/pages/404.html +23 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{3741-81560b0aef166f49.js → 3741-b70e2d1a48a087d6.js} +16 -9
- package/.next/standalone/.next/static/chunks/3741-b70e2d1a48a087d6.js.map +1 -0
- package/.next/standalone/.next/static/chunks/app/{layout-31009df0f341ccf5.js → layout-84c6f211a7a1ca36.js} +38 -5
- package/.next/standalone/.next/static/chunks/app/layout-84c6f211a7a1ca36.js.map +1 -0
- package/.next/standalone/.next/static/chunks/app/{page-108f526cb6e48b7b.js → page-31e4d0ad258be21a.js} +258 -122
- package/.next/standalone/.next/static/chunks/app/page-31e4d0ad258be21a.js.map +1 -0
- package/.next/standalone/.next/static/css/af02b3acc7d8f08d.css +5 -0
- package/.next/standalone/.next/static/css/af02b3acc7d8f08d.css.map +1 -0
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/public/manifest.json +2 -2
- package/CHANGELOG.md +24 -0
- package/api/types.ts +3 -1
- package/app/layout.tsx +30 -1
- package/components/agents/AgentEditor.tsx +5 -5
- package/components/layout/AppShell.tsx +36 -18
- package/components/layout/MenuPanel.tsx +7 -7
- package/components/models/ModelEditor.tsx +3 -3
- package/components/profile/ProfileEditor.tsx +9 -9
- package/components/profile/ProfilePanel.tsx +3 -3
- package/components/setup/OnboardingWizard.tsx +4 -4
- package/components/ui/HeaderActivity.tsx +24 -0
- package/contexts/AppContext.tsx +12 -4
- package/contexts/ThemeContext.tsx +30 -1
- package/hooks/useSSE.ts +42 -5
- package/lib/agents/base.ts +3 -1
- package/lib/agents/run-thread.ts +6 -3
- package/lib/ui/loading.ts +59 -0
- package/package.json +1 -1
- package/public/manifest.json +2 -2
- package/.next/standalone/.next/server/chunks/5745.js.map +0 -1
- package/.next/standalone/.next/static/chunks/3741-81560b0aef166f49.js.map +0 -1
- package/.next/standalone/.next/static/chunks/app/layout-31009df0f341ccf5.js.map +0 -1
- package/.next/standalone/.next/static/chunks/app/page-108f526cb6e48b7b.js.map +0 -1
- package/.next/standalone/.next/static/css/a23baa1c79db0435.css +0 -5
- package/.next/standalone/.next/static/css/a23baa1c79db0435.css.map +0 -1
- package/components/ui/TopProgressBar.tsx +0 -32
- /package/.next/standalone/.next/static/{0ImXz-nn9QbojXpTvB3i0 → yk5tOLkAUA4hfQKDjGbzK}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{0ImXz-nn9QbojXpTvB3i0 → yk5tOLkAUA4hfQKDjGbzK}/_ssgManifest.js +0 -0
|
@@ -19,7 +19,7 @@ import { ConnectionsPanel } from "@/components/connections/ConnectionsPanel";
|
|
|
19
19
|
import { ScheduledTasksPanel } from "@/components/scheduled-tasks/ScheduledTasksPanel";
|
|
20
20
|
import { BridgesPanel } from "@/components/bridges/BridgesPanel";
|
|
21
21
|
import { HarnessPanel } from "@/components/harness/HarnessPanel";
|
|
22
|
-
import {
|
|
22
|
+
import { HeaderActivity } from "@/components/ui/HeaderActivity";
|
|
23
23
|
import { NotificationStatus } from "@/components/ui/NotificationStatus";
|
|
24
24
|
import { CryptoFallbackBanner } from "@/components/ui/CryptoFallbackBanner";
|
|
25
25
|
import { UpdateAvailableBanner } from "@/components/ui/UpdateAvailableBanner";
|
|
@@ -34,7 +34,7 @@ const ADVANCED_TABS = new Set(["memory", "bridges", "harness"]);
|
|
|
34
34
|
|
|
35
35
|
export function AppShell() {
|
|
36
36
|
const { state, dispatch } = useAppContext();
|
|
37
|
-
const
|
|
37
|
+
const isFullMode = state.experienceMode === "full";
|
|
38
38
|
useUrlSync();
|
|
39
39
|
const { threadId, loading: sessionLoading, error: sessionError } = useAgentSession(
|
|
40
40
|
state.activeAgentId,
|
|
@@ -60,10 +60,10 @@ export function AppShell() {
|
|
|
60
60
|
}, [state.activeTab]);
|
|
61
61
|
|
|
62
62
|
useEffect(() => {
|
|
63
|
-
if (!
|
|
63
|
+
if (!isFullMode && ADVANCED_TABS.has(state.activeTab)) {
|
|
64
64
|
dispatch({ type: "SET_TAB", tab: "profile" });
|
|
65
65
|
}
|
|
66
|
-
}, [dispatch,
|
|
66
|
+
}, [dispatch, isFullMode, state.activeTab]);
|
|
67
67
|
|
|
68
68
|
const unreadCount = useUnreadCount();
|
|
69
69
|
|
|
@@ -193,7 +193,6 @@ export function AppShell() {
|
|
|
193
193
|
|
|
194
194
|
return (
|
|
195
195
|
<div className="h-screen h-[100dvh] flex flex-col text-fg overflow-hidden px-safe">
|
|
196
|
-
<TopProgressBar />
|
|
197
196
|
<NotificationStatus />
|
|
198
197
|
<Toaster />
|
|
199
198
|
<ServerStatus />
|
|
@@ -222,23 +221,41 @@ export function AppShell() {
|
|
|
222
221
|
}}
|
|
223
222
|
>
|
|
224
223
|
<div className="relative flex items-center gap-2 select-none" ref={agentPickerRef}>
|
|
225
|
-
{/* Logo is blue-on-transparent. In dark mode the blue gets lost
|
|
226
|
-
against the dark glass, so we drop the color and lift the
|
|
227
|
-
alpha to white — `brightness-0` flattens to black, `invert`
|
|
228
|
-
flips it to white, alpha channel is preserved by both. */}
|
|
229
224
|
<button
|
|
230
225
|
type="button"
|
|
231
226
|
onClick={() => setShowAgentPicker((v) => !v)}
|
|
232
227
|
className="control-tap inline-flex items-center gap-2 rounded-lg px-1.5 py-1 hover:bg-surface-3/60 transition-colors"
|
|
233
|
-
title="Select active agent"
|
|
228
|
+
title={activeAgent ? `Active agent: ${activeAgent.name} — click to switch` : "Select active agent"}
|
|
234
229
|
aria-haspopup="menu"
|
|
235
230
|
aria-expanded={showAgentPicker}
|
|
236
231
|
>
|
|
237
|
-
{
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
232
|
+
{activeAgent?.icon ? (
|
|
233
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
234
|
+
<img
|
|
235
|
+
src={activeAgent.icon}
|
|
236
|
+
alt=""
|
|
237
|
+
className="h-6 w-6 rounded-md object-cover shrink-0"
|
|
238
|
+
/>
|
|
239
|
+
) : activeAgent ? (
|
|
240
|
+
<span
|
|
241
|
+
aria-hidden
|
|
242
|
+
className="h-6 w-6 rounded-md bg-surface-3 text-[11px] font-semibold text-fg-subtle inline-flex items-center justify-center shrink-0"
|
|
243
|
+
>
|
|
244
|
+
{activeAgent.name.charAt(0).toUpperCase()}
|
|
245
|
+
</span>
|
|
246
|
+
) : (
|
|
247
|
+
// No active agent yet — fall back to the app mark. Blue-on-
|
|
248
|
+
// transparent loses contrast against the dark glass, so we
|
|
249
|
+
// flatten + invert it in dark mode (alpha is preserved).
|
|
250
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
251
|
+
<img
|
|
252
|
+
src="/logo-mark-transparent.png"
|
|
253
|
+
alt=""
|
|
254
|
+
className="h-6 w-auto dark:brightness-0 dark:invert"
|
|
255
|
+
/>
|
|
256
|
+
)}
|
|
257
|
+
<span className="text-fg font-semibold tracking-tight truncate max-w-[12rem] sm:max-w-[16rem]">
|
|
258
|
+
{activeAgent?.name ?? getAppName()}
|
|
242
259
|
</span>
|
|
243
260
|
<ChevronDown size={14} className={`text-fg-faint transition-transform ${showAgentPicker ? "rotate-180" : ""}`} />
|
|
244
261
|
</button>
|
|
@@ -288,6 +305,7 @@ export function AppShell() {
|
|
|
288
305
|
</div>
|
|
289
306
|
)}
|
|
290
307
|
</div>
|
|
308
|
+
<HeaderActivity />
|
|
291
309
|
<button
|
|
292
310
|
onClick={() => { setShowMenu((v) => !v); }}
|
|
293
311
|
className={`control-tap ml-auto relative p-2.5 rounded transition-colors ${showMenu ? "text-fg bg-surface-3" : "text-fg-faint hover:text-fg-muted hover:bg-surface-3/50"}`}
|
|
@@ -341,7 +359,7 @@ export function AppShell() {
|
|
|
341
359
|
<AgentsPanel />
|
|
342
360
|
</Activity>
|
|
343
361
|
)}
|
|
344
|
-
{
|
|
362
|
+
{isFullMode && mountedTabs.has("memory") && (
|
|
345
363
|
<Activity mode={state.activeTab === "memory" ? "visible" : "hidden"}>
|
|
346
364
|
<MemoryPanel />
|
|
347
365
|
</Activity>
|
|
@@ -381,7 +399,7 @@ export function AppShell() {
|
|
|
381
399
|
<ScheduledTasksPanel />
|
|
382
400
|
</Activity>
|
|
383
401
|
)}
|
|
384
|
-
{
|
|
402
|
+
{isFullMode && mountedTabs.has("bridges") && (
|
|
385
403
|
<Activity mode={state.activeTab === "bridges" ? "visible" : "hidden"}>
|
|
386
404
|
<BridgesPanel />
|
|
387
405
|
</Activity>
|
|
@@ -391,7 +409,7 @@ export function AppShell() {
|
|
|
391
409
|
<ProfilePanel />
|
|
392
410
|
</Activity>
|
|
393
411
|
)}
|
|
394
|
-
{
|
|
412
|
+
{isFullMode && mountedTabs.has("harness") && (
|
|
395
413
|
<Activity mode={state.activeTab === "harness" ? "visible" : "hidden"}>
|
|
396
414
|
<HarnessPanel />
|
|
397
415
|
</Activity>
|
|
@@ -228,9 +228,9 @@ export function MenuPanel({
|
|
|
228
228
|
onShowThinkingChange,
|
|
229
229
|
}: Props) {
|
|
230
230
|
const { state, dispatch } = useAppContext();
|
|
231
|
-
const
|
|
231
|
+
const isFullMode = state.experienceMode === "full";
|
|
232
232
|
const toggleMode = () => {
|
|
233
|
-
dispatch({ type: "SET_EXPERIENCE_MODE", mode:
|
|
233
|
+
dispatch({ type: "SET_EXPERIENCE_MODE", mode: isFullMode ? "essential" : "full" });
|
|
234
234
|
};
|
|
235
235
|
// Advanced section starts collapsed once the user has dismissed it
|
|
236
236
|
// once (persisted to localStorage). Defaults to *expanded* on first
|
|
@@ -296,15 +296,15 @@ export function MenuPanel({
|
|
|
296
296
|
<button
|
|
297
297
|
type="button"
|
|
298
298
|
onClick={toggleMode}
|
|
299
|
-
title={`Switch to ${
|
|
300
|
-
aria-label={`Switch to ${
|
|
299
|
+
title={`Switch to ${isFullMode ? "essential" : "full"} mode`}
|
|
300
|
+
aria-label={`Switch to ${isFullMode ? "essential" : "full"} mode`}
|
|
301
301
|
className={`control-tap text-[10px] uppercase tracking-wide px-2 py-0.5 rounded-full border transition-colors ${
|
|
302
|
-
|
|
302
|
+
isFullMode
|
|
303
303
|
? "border-accent/40 bg-accent/10 text-fg-subtle hover:bg-accent/20"
|
|
304
304
|
: "border-border bg-surface-3 text-fg-faint hover:text-fg-muted hover:border-border-strong"
|
|
305
305
|
}`}
|
|
306
306
|
>
|
|
307
|
-
{
|
|
307
|
+
{isFullMode ? "full" : "essential"}
|
|
308
308
|
</button>
|
|
309
309
|
</div>
|
|
310
310
|
</div>
|
|
@@ -312,7 +312,7 @@ export function MenuPanel({
|
|
|
312
312
|
{COMMON_TABS.map(renderTabButton)}
|
|
313
313
|
</div>
|
|
314
314
|
|
|
315
|
-
{
|
|
315
|
+
{isFullMode && (
|
|
316
316
|
<div className="border-b border-border shrink-0">
|
|
317
317
|
<button
|
|
318
318
|
type="button"
|
|
@@ -49,11 +49,11 @@ function fmtCtx(n: number | null) {
|
|
|
49
49
|
|
|
50
50
|
export function ModelEditor({ model, onSave, onClose }: Props) {
|
|
51
51
|
const { state } = useAppContext();
|
|
52
|
-
const
|
|
52
|
+
const isFullMode = state.experienceMode === "full";
|
|
53
53
|
// Per-editor opt-in so a normal-mode user can reveal the engine-room
|
|
54
54
|
// fields for one model without flipping the global workspace mode.
|
|
55
55
|
const [showExpert, setShowExpert] = useState(false);
|
|
56
|
-
const expertVisible =
|
|
56
|
+
const expertVisible = isFullMode || showExpert;
|
|
57
57
|
const isEdit = !!model;
|
|
58
58
|
const [name, setName] = useState(model?.name ?? "");
|
|
59
59
|
const [provider, setProvider] = useState(model?.provider ?? "anthropic");
|
|
@@ -240,7 +240,7 @@ export function ModelEditor({ model, onSave, onClose }: Props) {
|
|
|
240
240
|
<button onClick={onClose} className="text-fg-subtle hover:text-fg transition-colors"><X size={16} /></button>
|
|
241
241
|
</div>
|
|
242
242
|
<div className="p-4 space-y-3.5">
|
|
243
|
-
{!
|
|
243
|
+
{!isFullMode && (
|
|
244
244
|
<button
|
|
245
245
|
type="button"
|
|
246
246
|
onClick={() => setShowExpert((v) => !v)}
|
|
@@ -122,33 +122,33 @@ export function ProfileEditor() {
|
|
|
122
122
|
</div>
|
|
123
123
|
<p className="text-[11px] text-fg-faint leading-snug">
|
|
124
124
|
Choose how much configuration detail is shown in the app.
|
|
125
|
-
|
|
125
|
+
Essential hides technical panels and advanced model controls.
|
|
126
126
|
</p>
|
|
127
127
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
128
128
|
<button
|
|
129
129
|
type="button"
|
|
130
|
-
onClick={() => dispatch({ type: "SET_EXPERIENCE_MODE", mode: "
|
|
131
|
-
aria-pressed={mode === "
|
|
130
|
+
onClick={() => dispatch({ type: "SET_EXPERIENCE_MODE", mode: "essential" })}
|
|
131
|
+
aria-pressed={mode === "essential"}
|
|
132
132
|
className={`text-left px-3 py-2.5 rounded-xl border transition-colors ${
|
|
133
|
-
mode === "
|
|
133
|
+
mode === "essential"
|
|
134
134
|
? "border-accent/60 bg-accent/15 text-fg shadow-sm"
|
|
135
135
|
: "border-border bg-surface-3 text-fg-muted hover:text-fg hover:border-border-strong"
|
|
136
136
|
}`}
|
|
137
137
|
>
|
|
138
|
-
<div className="text-xs font-medium">
|
|
138
|
+
<div className="text-xs font-medium">Essential</div>
|
|
139
139
|
<div className="text-[10px] text-fg-faint leading-tight mt-0.5">Cleaner layout, fewer technical controls</div>
|
|
140
140
|
</button>
|
|
141
141
|
<button
|
|
142
142
|
type="button"
|
|
143
|
-
onClick={() => dispatch({ type: "SET_EXPERIENCE_MODE", mode: "
|
|
144
|
-
aria-pressed={mode === "
|
|
143
|
+
onClick={() => dispatch({ type: "SET_EXPERIENCE_MODE", mode: "full" })}
|
|
144
|
+
aria-pressed={mode === "full"}
|
|
145
145
|
className={`text-left px-3 py-2.5 rounded-xl border transition-colors ${
|
|
146
|
-
mode === "
|
|
146
|
+
mode === "full"
|
|
147
147
|
? "border-accent/60 bg-accent/15 text-fg shadow-sm"
|
|
148
148
|
: "border-border bg-surface-3 text-fg-muted hover:text-fg hover:border-border-strong"
|
|
149
149
|
}`}
|
|
150
150
|
>
|
|
151
|
-
<div className="text-xs font-medium">
|
|
151
|
+
<div className="text-xs font-medium">Full</div>
|
|
152
152
|
<div className="text-[10px] text-fg-faint leading-tight mt-0.5">Per-function controls and full tuning</div>
|
|
153
153
|
</button>
|
|
154
154
|
</div>
|
|
@@ -8,12 +8,12 @@ import { ProfileEditor } from "./ProfileEditor";
|
|
|
8
8
|
|
|
9
9
|
export function ProfilePanel() {
|
|
10
10
|
const { state } = useAppContext();
|
|
11
|
-
const
|
|
11
|
+
const isEssential = state.experienceMode === "essential";
|
|
12
12
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
13
13
|
const [showWizard, setShowWizard] = useState(false);
|
|
14
14
|
useDeepLinkScroll("profile", "profile", containerRef);
|
|
15
15
|
|
|
16
|
-
if (
|
|
16
|
+
if (isEssential && showWizard) {
|
|
17
17
|
return (
|
|
18
18
|
<div className="h-full overflow-y-auto profile-scrollbar panel-scrollbar">
|
|
19
19
|
<div className="max-w-3xl mx-auto w-full px-4 pt-4">
|
|
@@ -45,7 +45,7 @@ export function ProfilePanel() {
|
|
|
45
45
|
<div className="border-b border-border px-4 py-3 flex items-center gap-2">
|
|
46
46
|
<User size={14} className="text-fg-subtle" />
|
|
47
47
|
<h2 className="text-sm font-semibold text-fg">User Profile</h2>
|
|
48
|
-
{
|
|
48
|
+
{isEssential && (
|
|
49
49
|
<button
|
|
50
50
|
type="button"
|
|
51
51
|
onClick={() => {
|
|
@@ -353,7 +353,7 @@ export function OnboardingWizard({ context }: Props) {
|
|
|
353
353
|
)}
|
|
354
354
|
<div className="min-w-0 flex-1">
|
|
355
355
|
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-fg-faint">
|
|
356
|
-
{fullScreen ? "First launch setup" : state.experienceMode === "
|
|
356
|
+
{fullScreen ? "First launch setup" : state.experienceMode === "essential" ? "Guided setup" : "Profile"}
|
|
357
357
|
</p>
|
|
358
358
|
<h1 className="mt-1 text-2xl font-semibold tracking-tight">
|
|
359
359
|
{fullScreen ? `Set up ${getAppName()} once` : "Set up your assistant"}
|
|
@@ -362,7 +362,7 @@ export function OnboardingWizard({ context }: Props) {
|
|
|
362
362
|
Configure your profile, choose a model, and create a first agent from one screen. As you change the model, the feature icons below light up to show what the provider and model actually ship.
|
|
363
363
|
</p>
|
|
364
364
|
<div className="mt-4 flex flex-col sm:flex-row gap-2 max-w-xl">
|
|
365
|
-
{(["
|
|
365
|
+
{(["essential", "full"] as const).map((mode) => (
|
|
366
366
|
<button
|
|
367
367
|
key={mode}
|
|
368
368
|
type="button"
|
|
@@ -374,9 +374,9 @@ export function OnboardingWizard({ context }: Props) {
|
|
|
374
374
|
: "border-border bg-surface-2 hover:border-border-strong"
|
|
375
375
|
}`}
|
|
376
376
|
>
|
|
377
|
-
<div className="text-sm font-medium capitalize">{mode === "
|
|
377
|
+
<div className="text-sm font-medium capitalize">{mode === "essential" ? "Guided" : "Full controls"}</div>
|
|
378
378
|
<div className="mt-0.5 text-[11px] text-fg-faint leading-snug">
|
|
379
|
-
{mode === "
|
|
379
|
+
{mode === "essential"
|
|
380
380
|
? "Curated panels and a simpler model editor."
|
|
381
381
|
: "Every panel, context tuning, and engine-room toggles."}
|
|
382
382
|
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useActivityLabel } from "@/lib/ui/loading";
|
|
3
|
+
|
|
4
|
+
// Inline "what is happening right now" text rendered next to the agent
|
|
5
|
+
// dropdown. Replaces the old TopProgressBar: a single short label that
|
|
6
|
+
// updates live as the run progresses (Sending… / Thinking… / Using <tool>…).
|
|
7
|
+
// Returns null when nothing is in flight so the header stays calm.
|
|
8
|
+
export function HeaderActivity() {
|
|
9
|
+
const label = useActivityLabel();
|
|
10
|
+
if (!label) return null;
|
|
11
|
+
return (
|
|
12
|
+
<span
|
|
13
|
+
role="status"
|
|
14
|
+
aria-live="polite"
|
|
15
|
+
className="ml-1 inline-flex items-center gap-1.5 text-xs text-fg-faint truncate max-w-[14rem]"
|
|
16
|
+
>
|
|
17
|
+
<span className="relative inline-flex h-1.5 w-1.5 shrink-0">
|
|
18
|
+
<span className="absolute inset-0 rounded-full bg-accent/70 animate-ping" />
|
|
19
|
+
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-accent" />
|
|
20
|
+
</span>
|
|
21
|
+
<span className="truncate">{label}</span>
|
|
22
|
+
</span>
|
|
23
|
+
);
|
|
24
|
+
}
|
package/contexts/AppContext.tsx
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { createContext, useContext, useEffect, useReducer, type ReactNode } from "react";
|
|
3
3
|
|
|
4
|
-
export type ExperienceMode = "
|
|
4
|
+
export type ExperienceMode = "essential" | "full";
|
|
5
5
|
|
|
6
6
|
const EXPERIENCE_MODE_KEY = "jarela.experience.mode";
|
|
7
7
|
|
|
8
|
+
// Back-compat: pre-rename builds stored "normal" / "advanced". Read those
|
|
9
|
+
// values silently so an upgrade does not reset the user's choice.
|
|
10
|
+
function parseStoredMode(raw: string | null): ExperienceMode | null {
|
|
11
|
+
if (raw === "essential" || raw === "normal") return "essential";
|
|
12
|
+
if (raw === "full" || raw === "advanced") return "full";
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
export type Tab = "chat" | "dashboard" | "agents" | "memory" | "documents" | "models" | "mcp" | "extensions" | "tools" | "connections" | "tasks" | "bridges" | "profile" | "harness";
|
|
9
17
|
|
|
10
18
|
interface AppState {
|
|
@@ -54,14 +62,14 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
54
62
|
activeThreadId: null,
|
|
55
63
|
activeAgentId: null,
|
|
56
64
|
activeTab: "chat",
|
|
57
|
-
experienceMode: "
|
|
65
|
+
experienceMode: "essential",
|
|
58
66
|
selectedItem: {},
|
|
59
67
|
});
|
|
60
68
|
|
|
61
69
|
useEffect(() => {
|
|
62
70
|
try {
|
|
63
|
-
const stored = window.localStorage.getItem(EXPERIENCE_MODE_KEY);
|
|
64
|
-
if (stored
|
|
71
|
+
const stored = parseStoredMode(window.localStorage.getItem(EXPERIENCE_MODE_KEY));
|
|
72
|
+
if (stored) {
|
|
65
73
|
dispatch({ type: "SET_EXPERIENCE_MODE", mode: stored });
|
|
66
74
|
}
|
|
67
75
|
} catch {
|
|
@@ -19,9 +19,29 @@ function readStored(): Theme {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const LIGHT_CHROME = "#ffffff";
|
|
23
|
+
const DARK_CHROME = "#09090b";
|
|
24
|
+
|
|
25
|
+
function resolveChrome(theme: Theme): string {
|
|
26
|
+
if (theme === "light") return LIGHT_CHROME;
|
|
27
|
+
if (theme === "dark") return DARK_CHROME;
|
|
28
|
+
if (typeof window === "undefined") return DARK_CHROME;
|
|
29
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? DARK_CHROME : LIGHT_CHROME;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Keep the single <meta name="theme-color"> tag (installed by the pre-paint
|
|
33
|
+
// script in app/layout.tsx) aligned with the active surface so the PWA's
|
|
34
|
+
// desktop title bar and mobile address bar match the theme.
|
|
35
|
+
function syncChrome(theme: Theme) {
|
|
36
|
+
if (typeof document === "undefined") return;
|
|
37
|
+
const meta = document.querySelector('meta[name="theme-color"]');
|
|
38
|
+
if (meta) meta.setAttribute("content", resolveChrome(theme));
|
|
39
|
+
}
|
|
40
|
+
|
|
22
41
|
function apply(theme: Theme) {
|
|
23
42
|
if (typeof document === "undefined") return;
|
|
24
43
|
document.documentElement.setAttribute("data-theme", theme);
|
|
44
|
+
syncChrome(theme);
|
|
25
45
|
}
|
|
26
46
|
|
|
27
47
|
interface Ctx {
|
|
@@ -38,7 +58,16 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
|
38
58
|
const [theme, setThemeState] = useState<Theme>("system");
|
|
39
59
|
|
|
40
60
|
useEffect(() => {
|
|
41
|
-
|
|
61
|
+
const stored = readStored();
|
|
62
|
+
setThemeState(stored);
|
|
63
|
+
// When in "system" mode, mirror OS-level changes into the PWA chrome.
|
|
64
|
+
if (typeof window === "undefined") return;
|
|
65
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
66
|
+
const onChange = () => {
|
|
67
|
+
if (readStored() === "system") syncChrome("system");
|
|
68
|
+
};
|
|
69
|
+
mq.addEventListener?.("change", onChange);
|
|
70
|
+
return () => mq.removeEventListener?.("change", onChange);
|
|
42
71
|
}, []);
|
|
43
72
|
|
|
44
73
|
const setTheme = useCallback((t: Theme) => {
|
package/hooks/useSSE.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { api, submitRun, subscribeRun } from "@/api/client";
|
|
4
4
|
import type { ContentPart, SSEEventType, StreamOptions } from "@/api/types";
|
|
5
5
|
import type { ToolEvent } from "@/components/chat/ToolList";
|
|
6
|
+
import { pushActivity } from "@/lib/ui/loading";
|
|
6
7
|
|
|
7
8
|
export type { ToolEvent };
|
|
8
9
|
|
|
@@ -19,6 +20,28 @@ export function useSSE(onDone?: () => void) {
|
|
|
19
20
|
const [error, setError] = useState<string | null>(null);
|
|
20
21
|
const abortRef = useRef<AbortController | null>(null);
|
|
21
22
|
const threadIdRef = useRef<string | null>(null);
|
|
23
|
+
// Live "what is the agent doing" label, surfaced in the app header. The
|
|
24
|
+
// slot stays open for the duration of one run; we mutate its label as
|
|
25
|
+
// tool calls come and go so the header text updates in place without
|
|
26
|
+
// pushing/popping (which would flicker stacked activities).
|
|
27
|
+
const activityRef = useRef<ReturnType<typeof pushActivity> | null>(null);
|
|
28
|
+
const activeToolsRef = useRef<Map<string, string>>(new Map());
|
|
29
|
+
|
|
30
|
+
const openActivity = useCallback((initial: string) => {
|
|
31
|
+
activityRef.current?.clear();
|
|
32
|
+
activeToolsRef.current.clear();
|
|
33
|
+
activityRef.current = pushActivity(initial);
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
const closeActivity = useCallback(() => {
|
|
37
|
+
activityRef.current?.clear();
|
|
38
|
+
activityRef.current = null;
|
|
39
|
+
activeToolsRef.current.clear();
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
// Always release the activity label if the hook unmounts mid-run, so a
|
|
43
|
+
// dangling "thinking…" can't outlive its session.
|
|
44
|
+
useEffect(() => closeActivity, [closeActivity]);
|
|
22
45
|
|
|
23
46
|
const consume = useCallback(async (
|
|
24
47
|
iterable: AsyncIterable<string>,
|
|
@@ -27,20 +50,28 @@ export function useSSE(onDone?: () => void) {
|
|
|
27
50
|
const event = JSON.parse(raw) as SSEEventType;
|
|
28
51
|
if (event.type === "text_delta") {
|
|
29
52
|
setStreamingContent((p) => p + event.delta);
|
|
53
|
+
activityRef.current?.set("Responding…");
|
|
30
54
|
} else if (event.type === "thinking_delta") {
|
|
31
55
|
setThinkingContent((p) => p + event.delta);
|
|
56
|
+
if (activeToolsRef.current.size === 0) activityRef.current?.set("Thinking…");
|
|
32
57
|
} else if (event.type === "tool_call") {
|
|
33
58
|
setToolEvents((prev) => [
|
|
34
59
|
...prev,
|
|
35
60
|
{ id: event.id, phase: "call", name: event.name, payload: event.arguments },
|
|
36
61
|
]);
|
|
62
|
+
activeToolsRef.current.set(event.id, event.name);
|
|
63
|
+
activityRef.current?.set(`Using ${event.name}…`);
|
|
37
64
|
} else if (event.type === "tool_result") {
|
|
38
65
|
setToolEvents((prev) => [
|
|
39
66
|
...prev,
|
|
40
67
|
{ id: event.id, phase: "result", name: event.name, payload: event.result },
|
|
41
68
|
]);
|
|
69
|
+
activeToolsRef.current.delete(event.id);
|
|
70
|
+
const remaining = activeToolsRef.current.values().next().value as string | undefined;
|
|
71
|
+
activityRef.current?.set(remaining ? `Using ${remaining}…` : "Thinking…");
|
|
42
72
|
} else if (event.type === "done") {
|
|
43
73
|
setStreaming(false);
|
|
74
|
+
closeActivity();
|
|
44
75
|
// Don't clear streamingContent here — it would cause a visual gap
|
|
45
76
|
// between "stream done" and "refetched messages arrived" where the
|
|
46
77
|
// assistant bubble disappears for ~100ms. The consumer (ChatView)
|
|
@@ -55,11 +86,12 @@ export function useSSE(onDone?: () => void) {
|
|
|
55
86
|
setStreaming(false);
|
|
56
87
|
setStreamingContent("");
|
|
57
88
|
setThinkingContent("");
|
|
89
|
+
closeActivity();
|
|
58
90
|
setError(event.message);
|
|
59
91
|
break;
|
|
60
92
|
}
|
|
61
93
|
}
|
|
62
|
-
}, [onDone]);
|
|
94
|
+
}, [onDone, closeActivity]);
|
|
63
95
|
|
|
64
96
|
const start = useCallback(async (
|
|
65
97
|
threadId: string,
|
|
@@ -76,6 +108,7 @@ export function useSSE(onDone?: () => void) {
|
|
|
76
108
|
setThinkingContent("");
|
|
77
109
|
setToolEvents([]);
|
|
78
110
|
setError(null);
|
|
111
|
+
openActivity("Sending…");
|
|
79
112
|
|
|
80
113
|
try {
|
|
81
114
|
// Command: register the run server-side. 202 = we own this turn; 409
|
|
@@ -95,9 +128,10 @@ export function useSSE(onDone?: () => void) {
|
|
|
95
128
|
setStreaming(false);
|
|
96
129
|
setStreamingContent("");
|
|
97
130
|
setThinkingContent("");
|
|
131
|
+
closeActivity();
|
|
98
132
|
return { accepted: false };
|
|
99
133
|
}
|
|
100
|
-
}, [consume]);
|
|
134
|
+
}, [consume, openActivity, closeActivity]);
|
|
101
135
|
|
|
102
136
|
// Stop the active run. Three-part: (1) tell the server to abort the
|
|
103
137
|
// agent stream so the LangGraph loop unwinds; (2) tear down local
|
|
@@ -117,9 +151,10 @@ export function useSSE(onDone?: () => void) {
|
|
|
117
151
|
setStreaming(false);
|
|
118
152
|
// Keep streamingContent and thinkingContent visible until the next
|
|
119
153
|
// start()/attach() — same pattern as the `done` branch in consume().
|
|
154
|
+
closeActivity();
|
|
120
155
|
abortRef.current?.abort();
|
|
121
156
|
onDone?.();
|
|
122
|
-
}, [onDone]);
|
|
157
|
+
}, [onDone, closeActivity]);
|
|
123
158
|
|
|
124
159
|
// Attach to an in-flight run for the given thread (server-side run kept
|
|
125
160
|
// going because the user switched away, or because this is a fresh
|
|
@@ -138,6 +173,7 @@ export function useSSE(onDone?: () => void) {
|
|
|
138
173
|
setThinkingContent("");
|
|
139
174
|
setToolEvents([]);
|
|
140
175
|
setError(null);
|
|
176
|
+
openActivity("Reconnecting…");
|
|
141
177
|
|
|
142
178
|
try {
|
|
143
179
|
await consume(subscribeRun(threadId, ctrl.signal));
|
|
@@ -147,11 +183,12 @@ export function useSSE(onDone?: () => void) {
|
|
|
147
183
|
// "no run to attach to" (server returns 404, EventSource fails to
|
|
148
184
|
// open) — completely normal when navigating into an idle session.
|
|
149
185
|
setStreaming(false);
|
|
186
|
+
closeActivity();
|
|
150
187
|
if ((err as Error).name !== "AbortError") {
|
|
151
188
|
onDone?.();
|
|
152
189
|
}
|
|
153
190
|
}
|
|
154
|
-
}, [consume, onDone]);
|
|
191
|
+
}, [consume, onDone, openActivity, closeActivity]);
|
|
155
192
|
|
|
156
193
|
// Called by the consumer after a refetch lands, so the streaming bubble
|
|
157
194
|
// gets swapped for the persisted assistant message in a single render.
|
package/lib/agents/base.ts
CHANGED
|
@@ -18,7 +18,9 @@ export interface StreamOptions {
|
|
|
18
18
|
filters?: StreamFilters;
|
|
19
19
|
tool_policy?: ToolPolicy;
|
|
20
20
|
agent_run_config?: AgentRunConfig;
|
|
21
|
-
|
|
21
|
+
// Back-compat: pre-rename clients may still send "normal" / "advanced".
|
|
22
|
+
// Server-side normalization lives in lib/agents/run-thread.ts.
|
|
23
|
+
ui_experience_mode?: "essential" | "full" | "normal" | "advanced";
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export interface StreamChunk {
|
package/lib/agents/run-thread.ts
CHANGED
|
@@ -215,13 +215,16 @@ export async function prepareThreadRun(
|
|
|
215
215
|
: null;
|
|
216
216
|
|
|
217
217
|
const timeCtx = `Current time: ${new Date().toISOString()} (UTC). Use this when computing scheduled task timestamps.`;
|
|
218
|
-
|
|
218
|
+
// Accept both the new ("essential"/"full") and legacy ("normal"/"advanced")
|
|
219
|
+
// labels so an older client speaking to a newer server still works.
|
|
220
|
+
const rawMode = options?.ui_experience_mode;
|
|
221
|
+
const experienceMode = rawMode === "essential" || rawMode === "normal" ? "essential" : "full";
|
|
219
222
|
const experienceCtx = [
|
|
220
223
|
"--- UX mode ---",
|
|
221
224
|
`User interface mode: ${experienceMode}.`,
|
|
222
|
-
experienceMode === "
|
|
225
|
+
experienceMode === "essential"
|
|
223
226
|
? "Prefer concise, plain-language explanations and avoid exposing low-level configuration details unless asked."
|
|
224
|
-
: "User opted into advanced
|
|
227
|
+
: "User opted into the full / advanced UI; detailed technical explanations are welcome.",
|
|
225
228
|
].join("\n");
|
|
226
229
|
|
|
227
230
|
// Host environment hint so the agent doesn't have to guess platform-specific
|
package/lib/ui/loading.ts
CHANGED
|
@@ -45,3 +45,62 @@ export function useTrackLoading(active: boolean): void {
|
|
|
45
45
|
return startLoading();
|
|
46
46
|
}, [active]);
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Activity label channel. Independent of the loading count: callers that
|
|
51
|
+
// want to surface a human-readable "what's happening right now" string
|
|
52
|
+
// (e.g. "thinking…", "using web_search") push onto a small stack. The
|
|
53
|
+
// header reads the top of the stack and renders it inline.
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
let activitySeq = 0;
|
|
57
|
+
const activityStack: Array<{ id: number; label: string }> = [];
|
|
58
|
+
const activityListeners = new Set<(label: string | null) => void>();
|
|
59
|
+
|
|
60
|
+
function currentActivity(): string | null {
|
|
61
|
+
return activityStack.length > 0 ? activityStack[activityStack.length - 1].label : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function notifyActivity() {
|
|
65
|
+
const top = currentActivity();
|
|
66
|
+
for (const l of activityListeners) l(top);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Push a label and return a setter+clearer for the same slot. The setter
|
|
70
|
+
// keeps the same stack id so updating a label (e.g. tool name changing
|
|
71
|
+
// mid-run) doesn't reorder layered activities. `clear` removes the slot.
|
|
72
|
+
export function pushActivity(label: string): {
|
|
73
|
+
set: (next: string) => void;
|
|
74
|
+
clear: () => void;
|
|
75
|
+
} {
|
|
76
|
+
const id = ++activitySeq;
|
|
77
|
+
activityStack.push({ id, label });
|
|
78
|
+
notifyActivity();
|
|
79
|
+
return {
|
|
80
|
+
set(next: string) {
|
|
81
|
+
const slot = activityStack.find((s) => s.id === id);
|
|
82
|
+
if (slot && slot.label !== next) {
|
|
83
|
+
slot.label = next;
|
|
84
|
+
notifyActivity();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
clear() {
|
|
88
|
+
const idx = activityStack.findIndex((s) => s.id === id);
|
|
89
|
+
if (idx >= 0) {
|
|
90
|
+
activityStack.splice(idx, 1);
|
|
91
|
+
notifyActivity();
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function useActivityLabel(): string | null {
|
|
98
|
+
const [label, setLabel] = useState<string | null>(currentActivity());
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const fn = (v: string | null) => setLabel(v);
|
|
101
|
+
activityListeners.add(fn);
|
|
102
|
+
setLabel(currentActivity());
|
|
103
|
+
return () => { activityListeners.delete(fn); };
|
|
104
|
+
}, []);
|
|
105
|
+
return label;
|
|
106
|
+
}
|
package/package.json
CHANGED
package/public/manifest.json
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"scope": "/",
|
|
7
7
|
"display": "standalone",
|
|
8
8
|
"display_override": ["minimal-ui", "standalone"],
|
|
9
|
-
"background_color": "#
|
|
10
|
-
"theme_color": "#
|
|
9
|
+
"background_color": "#09090b",
|
|
10
|
+
"theme_color": "#09090b",
|
|
11
11
|
"edge_side_panel": {
|
|
12
12
|
"preferred_width": 480
|
|
13
13
|
},
|