@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.
Files changed (98) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -1
  3. package/.next/standalone/.next/build-manifest.json +2 -2
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js +1 -537
  15. package/.next/standalone/.next/server/app/_not-found/page.js.map +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +23 -2
  19. package/.next/standalone/.next/server/app/_not-found.rsc +47 -18
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +47 -18
  21. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +36 -7
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  25. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  26. package/.next/standalone/.next/server/app/index.html +23 -2
  27. package/.next/standalone/.next/server/app/index.rsc +46 -17
  28. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  29. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +46 -17
  30. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  31. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +36 -7
  32. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  33. package/.next/standalone/.next/server/app/page.js +79690 -79580
  34. package/.next/standalone/.next/server/app/page.js.map +1 -1
  35. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/setup/page.js +1 -1
  38. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/setup.html +1 -1
  41. package/.next/standalone/.next/server/app/setup.rsc +48 -19
  42. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +48 -19
  43. package/.next/standalone/.next/server/app/setup.segments/_head.segment.rsc +1 -1
  44. package/.next/standalone/.next/server/app/setup.segments/_index.segment.rsc +36 -7
  45. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  46. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +1 -1
  47. package/.next/standalone/.next/server/app/setup.segments/setup.segment.rsc +1 -1
  48. package/.next/standalone/.next/server/app-paths-manifest.json +1 -1
  49. package/.next/standalone/.next/server/chunks/4045.js +611 -0
  50. package/.next/standalone/.next/server/chunks/4045.js.map +1 -0
  51. package/.next/standalone/.next/server/chunks/5432.js +5 -2
  52. package/.next/standalone/.next/server/chunks/5432.js.map +1 -1
  53. package/.next/standalone/.next/server/chunks/{5745.js → 6765.js} +9 -551
  54. package/.next/standalone/.next/server/chunks/6765.js.map +1 -0
  55. package/.next/standalone/.next/server/chunks/7885.js +5 -2
  56. package/.next/standalone/.next/server/chunks/7885.js.map +1 -1
  57. package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
  58. package/.next/standalone/.next/server/pages/404.html +23 -2
  59. package/.next/standalone/.next/server/pages/500.html +1 -1
  60. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  61. package/.next/standalone/.next/static/chunks/{3741-81560b0aef166f49.js → 3741-b70e2d1a48a087d6.js} +16 -9
  62. package/.next/standalone/.next/static/chunks/3741-b70e2d1a48a087d6.js.map +1 -0
  63. package/.next/standalone/.next/static/chunks/app/{layout-31009df0f341ccf5.js → layout-84c6f211a7a1ca36.js} +38 -5
  64. package/.next/standalone/.next/static/chunks/app/layout-84c6f211a7a1ca36.js.map +1 -0
  65. package/.next/standalone/.next/static/chunks/app/{page-108f526cb6e48b7b.js → page-31e4d0ad258be21a.js} +258 -122
  66. package/.next/standalone/.next/static/chunks/app/page-31e4d0ad258be21a.js.map +1 -0
  67. package/.next/standalone/.next/static/css/af02b3acc7d8f08d.css +5 -0
  68. package/.next/standalone/.next/static/css/af02b3acc7d8f08d.css.map +1 -0
  69. package/.next/standalone/package.json +1 -1
  70. package/.next/standalone/public/manifest.json +2 -2
  71. package/CHANGELOG.md +24 -0
  72. package/api/types.ts +3 -1
  73. package/app/layout.tsx +30 -1
  74. package/components/agents/AgentEditor.tsx +5 -5
  75. package/components/layout/AppShell.tsx +36 -18
  76. package/components/layout/MenuPanel.tsx +7 -7
  77. package/components/models/ModelEditor.tsx +3 -3
  78. package/components/profile/ProfileEditor.tsx +9 -9
  79. package/components/profile/ProfilePanel.tsx +3 -3
  80. package/components/setup/OnboardingWizard.tsx +4 -4
  81. package/components/ui/HeaderActivity.tsx +24 -0
  82. package/contexts/AppContext.tsx +12 -4
  83. package/contexts/ThemeContext.tsx +30 -1
  84. package/hooks/useSSE.ts +42 -5
  85. package/lib/agents/base.ts +3 -1
  86. package/lib/agents/run-thread.ts +6 -3
  87. package/lib/ui/loading.ts +59 -0
  88. package/package.json +1 -1
  89. package/public/manifest.json +2 -2
  90. package/.next/standalone/.next/server/chunks/5745.js.map +0 -1
  91. package/.next/standalone/.next/static/chunks/3741-81560b0aef166f49.js.map +0 -1
  92. package/.next/standalone/.next/static/chunks/app/layout-31009df0f341ccf5.js.map +0 -1
  93. package/.next/standalone/.next/static/chunks/app/page-108f526cb6e48b7b.js.map +0 -1
  94. package/.next/standalone/.next/static/css/a23baa1c79db0435.css +0 -5
  95. package/.next/standalone/.next/static/css/a23baa1c79db0435.css.map +0 -1
  96. package/components/ui/TopProgressBar.tsx +0 -32
  97. /package/.next/standalone/.next/static/{0ImXz-nn9QbojXpTvB3i0 → yk5tOLkAUA4hfQKDjGbzK}/_buildManifest.js +0 -0
  98. /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 { TopProgressBar } from "@/components/ui/TopProgressBar";
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 isAdvanced = state.experienceMode === "advanced";
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 (!isAdvanced && ADVANCED_TABS.has(state.activeTab)) {
63
+ if (!isFullMode && ADVANCED_TABS.has(state.activeTab)) {
64
64
  dispatch({ type: "SET_TAB", tab: "profile" });
65
65
  }
66
- }, [dispatch, isAdvanced, state.activeTab]);
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
- {/* eslint-disable-next-line @next/next/no-img-element */}
238
- <img src="/logo-mark-transparent.png" alt="" className="h-6 w-auto dark:brightness-0 dark:invert" />
239
- <span className="text-fg font-semibold tracking-tight">{getAppName()}</span>
240
- <span className="text-xs text-fg-faint max-w-[11rem] truncate hidden sm:inline">
241
- {activeAgent?.name ?? "select agent"}
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
- {isAdvanced && mountedTabs.has("memory") && (
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
- {isAdvanced && mountedTabs.has("bridges") && (
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
- {isAdvanced && mountedTabs.has("harness") && (
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 isAdvanced = state.experienceMode === "advanced";
231
+ const isFullMode = state.experienceMode === "full";
232
232
  const toggleMode = () => {
233
- dispatch({ type: "SET_EXPERIENCE_MODE", mode: isAdvanced ? "normal" : "advanced" });
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 ${isAdvanced ? "normal" : "advanced"} mode`}
300
- aria-label={`Switch to ${isAdvanced ? "normal" : "advanced"} mode`}
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
- isAdvanced
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
- {isAdvanced ? "advanced" : "normal"}
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
- {isAdvanced && (
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 isAdvanced = state.experienceMode === "advanced";
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 = isAdvanced || showExpert;
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
- {!isAdvanced && (
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
- Normal hides technical panels and advanced model controls.
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: "normal" })}
131
- aria-pressed={mode === "normal"}
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 === "normal"
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">Normal</div>
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: "advanced" })}
144
- aria-pressed={mode === "advanced"}
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 === "advanced"
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">Advanced</div>
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 isNormal = state.experienceMode === "normal";
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 (isNormal && showWizard) {
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
- {isNormal && (
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 === "normal" ? "Guided setup" : "Profile"}
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
- {(["normal", "advanced"] as const).map((mode) => (
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 === "normal" ? "Guided" : "Full controls"}</div>
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 === "normal"
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
+ }
@@ -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 = "normal" | "advanced";
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: "normal",
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 === "normal" || stored === "advanced") {
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
- setThemeState(readStored());
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.
@@ -18,7 +18,9 @@ export interface StreamOptions {
18
18
  filters?: StreamFilters;
19
19
  tool_policy?: ToolPolicy;
20
20
  agent_run_config?: AgentRunConfig;
21
- ui_experience_mode?: "normal" | "advanced";
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 {
@@ -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
- const experienceMode = options?.ui_experience_mode === "normal" ? "normal" : "advanced";
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 === "normal"
225
+ experienceMode === "essential"
223
226
  ? "Prefer concise, plain-language explanations and avoid exposing low-level configuration details unless asked."
224
- : "User opted into advanced controls; detailed technical explanations are welcome.",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@circuitwall/jarela",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Jarela — local chat interface for LangGraph agents (multi-provider, single-process, SQLite-backed).",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Andrew Ge Wu",
@@ -6,8 +6,8 @@
6
6
  "scope": "/",
7
7
  "display": "standalone",
8
8
  "display_override": ["minimal-ui", "standalone"],
9
- "background_color": "#0f172a",
10
- "theme_color": "#2563eb",
9
+ "background_color": "#09090b",
10
+ "theme_color": "#09090b",
11
11
  "edge_side_panel": {
12
12
  "preferred_width": 480
13
13
  },