@circuitwall/jarela 2.4.0 → 2.5.0

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 (140) 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 +4 -4
  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/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.html +14 -2
  13. package/.next/standalone/.next/server/app/_not-found.rsc +16 -4
  14. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -4
  15. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +16 -4
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js +1 -1
  20. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +1 -1
  22. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/v1/dashboard/pricing-task/route.js +1 -1
  24. package/.next/standalone/.next/server/app/api/v1/documents/sources/[id]/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/v1/documents/sources/route.js +1 -1
  26. package/.next/standalone/.next/server/app/api/v1/events/route.js +1 -1
  27. package/.next/standalone/.next/server/app/api/v1/extension/agents/route.js +1 -1
  28. package/.next/standalone/.next/server/app/api/v1/extension/fill/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/v1/extension/refine/route.js +1 -1
  30. package/.next/standalone/.next/server/app/api/v1/extension/turn/route.js +1 -1
  31. package/.next/standalone/.next/server/app/api/v1/extensions/route.js +1 -1
  32. package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/config/route.js +1 -1
  33. package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/route.js +1 -1
  34. package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js +1 -1
  35. package/.next/standalone/.next/server/app/api/v1/packages/defaults/[id]/route.js +28 -4
  36. package/.next/standalone/.next/server/app/api/v1/packages/reload/route.js +28 -4
  37. package/.next/standalone/.next/server/app/api/v1/packages/route.js +28 -4
  38. package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +578 -58
  40. package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/run/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +118 -26
  42. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/v1/tools/route.js +1 -1
  44. package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/v1/watchers/[id]/run/route.js +1 -1
  46. package/.next/standalone/.next/server/app/api/v1/watchers/route.js +1 -1
  47. package/.next/standalone/.next/server/app/page.js +31202 -30960
  48. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app-paths-manifest.json +1 -1
  51. package/.next/standalone/.next/server/chunks/2471.js +17 -17
  52. package/.next/standalone/.next/server/chunks/2653.js +80 -1
  53. package/.next/standalone/.next/server/chunks/3399.js +10695 -10695
  54. package/.next/standalone/.next/server/chunks/3566.js +1 -1
  55. package/.next/standalone/.next/server/chunks/4189.js +1 -1
  56. package/.next/standalone/.next/server/chunks/4508.js +2 -2
  57. package/.next/standalone/.next/server/chunks/4596.js +28 -4
  58. package/.next/standalone/.next/server/chunks/46.js +18 -3
  59. package/.next/standalone/.next/server/chunks/466.js +53 -7
  60. package/.next/standalone/.next/server/chunks/4955.js +30 -3
  61. package/.next/standalone/.next/server/chunks/5585.js +28 -4
  62. package/.next/standalone/.next/server/chunks/6649.js +1 -1
  63. package/.next/standalone/.next/server/chunks/9164.js +65 -14
  64. package/.next/standalone/.next/server/chunks/9590.js +18 -3
  65. package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
  66. package/.next/standalone/.next/server/middleware.js +18 -3
  67. package/.next/standalone/.next/server/pages/404.html +14 -2
  68. package/.next/standalone/.next/server/pages/500.html +1 -1
  69. package/.next/standalone/.next/server/preview-props.json +3 -3
  70. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  71. package/.next/standalone/.next/static/chunks/{1395-e4a99912b8f83044.js → 1395-bfe1f3ef760c0f50.js} +54 -8
  72. package/.next/standalone/.next/static/chunks/app/{layout-9bc0b10127cb7d42.js → layout-7b24ad13faa3944b.js} +69 -2
  73. package/.next/standalone/.next/static/chunks/app/{page-04748847c6ff866f.js → page-2ef8d75d40eb4311.js} +10661 -10352
  74. package/.next/standalone/.next/static/css/c160ea862133acd2.css +5 -0
  75. package/.next/standalone/docs/adoption/changesets.json +2756 -0
  76. package/.next/standalone/package.json +1 -1
  77. package/CHANGELOG.md +33 -0
  78. package/api/client.models.test.ts +10 -0
  79. package/api/client.startup.test.ts +129 -0
  80. package/api/client.ts +51 -9
  81. package/api/thread-metadata.test.ts +72 -0
  82. package/api/types.ts +6 -3
  83. package/app/api/v1/providers/[provider]/models/route.test.ts +216 -6
  84. package/app/api/v1/providers/[provider]/models/route.ts +259 -40
  85. package/app/api/v1/threads/[thread_id]/route.ts +26 -23
  86. package/app/globals.css +119 -29
  87. package/app/layout.tsx +12 -0
  88. package/components/agents/AgentsPanel.tsx +5 -13
  89. package/components/chat/ChatView.tsx +9 -18
  90. package/components/chat/InputBar.tsx +1 -1
  91. package/components/chat/MessageBubble.tsx +1 -1
  92. package/components/chat/MessageList.test.tsx +45 -4
  93. package/components/chat/MessageList.tsx +123 -89
  94. package/components/chat/ToolList.tsx +88 -84
  95. package/components/chat/useThreadActivity.test.tsx +2 -2
  96. package/components/chat/useThreadActivity.ts +2 -2
  97. package/components/chat/useThreadData.ts +1 -30
  98. package/components/chat/useThreadMetadataPolling.test.tsx +128 -0
  99. package/components/chat/useThreadMetadataPolling.ts +50 -0
  100. package/components/chat/useUserProfileAndAgent.test.tsx +31 -0
  101. package/components/chat/useUserProfileAndAgent.ts +13 -2
  102. package/components/dashboard/DashboardPanel.tsx +1 -1
  103. package/components/dashboard/Series.tsx +34 -21
  104. package/components/dashboard/SummaryCards.tsx +1 -1
  105. package/components/layout/AppShell.tsx +22 -7
  106. package/components/layout/MenuPanel.tsx +14 -24
  107. package/components/profile/ProfilePanel.tsx +2 -2
  108. package/components/settings/AppearancePanel.tsx +70 -3
  109. package/components/settings/SettingsPanel.tsx +2 -2
  110. package/components/tools/ToolsPanel.tsx +2 -7
  111. package/components/ui/AppearancePreferences.test.tsx +68 -0
  112. package/components/ui/BootScreen.test.tsx +44 -0
  113. package/components/ui/BootScreen.tsx +24 -11
  114. package/components/ui/NotificationStatus.test.tsx +2 -2
  115. package/components/ui/NotificationStatus.tsx +5 -5
  116. package/components/ui/SubTabBar.tsx +2 -3
  117. package/contexts/ThemeContext.tsx +84 -1
  118. package/hooks/useAgentSession.test.ts +12 -1
  119. package/hooks/useAgentSession.ts +6 -4
  120. package/hooks/useUrlSync.test.ts +13 -0
  121. package/hooks/useUrlSync.ts +0 -4
  122. package/lib/db/migrations.test.ts +61 -0
  123. package/lib/db/migrations.ts +13 -0
  124. package/lib/embeddings/performance.test.ts +114 -0
  125. package/lib/stores/dashboard-metrics-cache.test.ts +99 -0
  126. package/lib/stores/dashboard-metrics.ts +30 -4
  127. package/lib/stores/message-embedding-jobs.test.ts +27 -0
  128. package/lib/stores/message-embedding-jobs.ts +5 -3
  129. package/lib/stores/message-usage.test.ts +21 -1
  130. package/lib/stores/pricing-cache.test.ts +31 -0
  131. package/lib/stores/pricing.ts +3 -0
  132. package/lib/tools/packages/default-packages.test.ts +21 -0
  133. package/lib/tools/packages/optional-package.test.ts +72 -0
  134. package/lib/tools/packages/optional-package.ts +33 -4
  135. package/lib/ui/navigate.test.ts +13 -0
  136. package/lib/ui/navigate.ts +10 -2
  137. package/package.json +1 -1
  138. package/.next/standalone/.next/static/css/2669e8744cc8c212.css +0 -5
  139. /package/.next/standalone/.next/static/{hwujW4hR1b-r3k5WdMxZO → 544OFJV-q0RC6Gjgje4LT}/_buildManifest.js +0 -0
  140. /package/.next/standalone/.next/static/{hwujW4hR1b-r3k5WdMxZO → 544OFJV-q0RC6Gjgje4LT}/_ssgManifest.js +0 -0
@@ -20,6 +20,7 @@ import { BridgesPanel } from "@/components/bridges/BridgesPanel";
20
20
  import { LogsPanel } from "@/components/logs/LogsPanel";
21
21
  import { EnvVarsPanel } from "@/components/env/EnvVarsPanel";
22
22
  import { SettingsPanel } from "@/components/settings/SettingsPanel";
23
+ import { UnifiedMemoryPanel } from "@/components/memory/UnifiedMemoryPanel";
23
24
  import { HeaderActivity } from "@/components/ui/HeaderActivity";
24
25
  import { NotificationStatus } from "@/components/ui/NotificationStatus";
25
26
  import { CryptoFallbackBanner } from "@/components/ui/CryptoFallbackBanner";
@@ -35,7 +36,7 @@ import { getAppName } from "@/lib/env/app-config";
35
36
  import { MenuPanel } from "./MenuPanel";
36
37
  import { DashboardPanel } from "@/components/dashboard/DashboardPanel";
37
38
 
38
- const ADVANCED_TABS = new Set(["bridges", "logs", "env"]);
39
+ const ADVANCED_TABS = new Set(["dashboard", "tasks", "tools", "memory", "bridges", "logs", "env"]);
39
40
 
40
41
  export function AppShell() {
41
42
  const { state, dispatch } = useAppContext();
@@ -304,6 +305,7 @@ export function AppShell() {
304
305
  agents={agents}
305
306
  agentsLoaded={agentsLoaded}
306
307
  activeAgentId={state.activeAgentId}
308
+ preferredThreadId={state.activeThreadId}
307
309
  onPickAgent={(id) => {
308
310
  dispatch({ type: "SET_AGENT", agentId: id });
309
311
  dispatch({ type: "SET_TAB", tab: "chat" });
@@ -468,7 +470,7 @@ export function AppShell() {
468
470
  </button>
469
471
  </header>
470
472
 
471
- <div className="flex-1 relative overflow-hidden">
473
+ <div className="mx-auto flex-1 relative w-full max-w-7xl overflow-hidden">
472
474
  {/*
473
475
  Each tab is wrapped in <Activity> on its first visit, then kept
474
476
  mounted so scroll position, expanded rows, and local form/filter
@@ -504,14 +506,23 @@ export function AppShell() {
504
506
  <AgentsPanel />
505
507
  </Activity>
506
508
  )}
509
+ {isFullMode && mountedTabs.has("memory") && (
510
+ <Activity mode={state.activeTab === "memory" ? "visible" : "hidden"}>
511
+ <UnifiedMemoryPanel />
512
+ </Activity>
513
+ )}
507
514
  {mountedTabs.has("models") && (
508
515
  <Activity mode={state.activeTab === "models" ? "visible" : "hidden"}>
509
- <ModelsPanel />
516
+ <div className="mx-auto h-full w-full max-w-5xl min-w-0">
517
+ <ModelsPanel />
518
+ </div>
510
519
  </Activity>
511
520
  )}
512
521
  {mountedTabs.has("credentials") && (
513
522
  <Activity mode={state.activeTab === "credentials" ? "visible" : "hidden"}>
514
- <CredentialsPanel />
523
+ <div className="mx-auto h-full w-full max-w-5xl min-w-0">
524
+ <CredentialsPanel />
525
+ </div>
515
526
  </Activity>
516
527
  )}
517
528
  {mountedTabs.has("mcp") && (
@@ -541,12 +552,16 @@ export function AppShell() {
541
552
  )}
542
553
  {isFullMode && mountedTabs.has("logs") && (
543
554
  <Activity mode={state.activeTab === "logs" ? "visible" : "hidden"}>
544
- <LogsPanel />
555
+ <div className="mx-auto h-full w-full max-w-5xl min-w-0">
556
+ <LogsPanel />
557
+ </div>
545
558
  </Activity>
546
559
  )}
547
560
  {isFullMode && mountedTabs.has("env") && (
548
561
  <Activity mode={state.activeTab === "env" ? "visible" : "hidden"}>
549
- <EnvVarsPanel />
562
+ <div className="mx-auto h-full w-full max-w-5xl min-w-0">
563
+ <EnvVarsPanel />
564
+ </div>
550
565
  </Activity>
551
566
  )}
552
567
  {mountedTabs.has("settings") && (
@@ -557,7 +572,7 @@ export function AppShell() {
557
572
 
558
573
  {showMenu && (
559
574
  <div
560
- className="fixed left-0 right-0 bottom-0 bg-black/30 backdrop-blur-sm z-30"
575
+ className="fixed left-0 right-0 bottom-0 bg-black/20 z-30"
561
576
  style={{ top: "calc(3rem + var(--app-safe-top))" }}
562
577
  onClick={() => setShowMenu(false)}
563
578
  />
@@ -38,22 +38,22 @@ const TAB_ICONS: Record<Tab, React.ReactNode> = {
38
38
 
39
39
  const TAB_TITLES: Record<Tab, string> = {
40
40
  chat: "Chat",
41
- dashboard: "Dashboard",
41
+ dashboard: "Usage",
42
42
  agents: "Agents",
43
- memory: "Memory",
43
+ memory: "Knowledge",
44
44
  documents: "Documents",
45
45
  models: "Models",
46
46
  credentials: "Credentials",
47
47
  mcp: "MCP",
48
48
  extensions: "Extensions",
49
- tools: "Tools",
50
- tasks: "Tasks",
49
+ tools: "Capabilities",
50
+ tasks: "Automations",
51
51
  bridges: "Bridges",
52
52
  profile: "Profile",
53
53
  harness: "Harness",
54
54
  logs: "Logs",
55
55
  env: "Defaults",
56
- settings: "Advanced settings",
56
+ settings: "Settings",
57
57
  };
58
58
 
59
59
  // Tiny-screen labels (<=380px). Only abbreviate where the full name is
@@ -61,38 +61,28 @@ const TAB_TITLES: Record<Tab, string> = {
61
61
  // "Me", "Setup") that overloads or contradicts the real meaning.
62
62
  const TAB_SHORT: Record<Tab, string> = {
63
63
  chat: "Chat",
64
- dashboard: "Dash",
64
+ dashboard: "Usage",
65
65
  agents: "Agents",
66
- memory: "Memory",
66
+ memory: "Knowledge",
67
67
  documents: "Docs",
68
68
  models: "Models",
69
69
  credentials: "Keys",
70
70
  mcp: "MCP",
71
71
  extensions: "Ext",
72
- tools: "Tools",
73
- tasks: "Tasks",
72
+ tools: "Capabilities",
73
+ tasks: "Automations",
74
74
  bridges: "Bridges",
75
75
  profile: "Profile",
76
76
  harness: "Tests",
77
77
  logs: "Logs",
78
78
  env: "Env",
79
- settings: "Advanced",
79
+ settings: "Settings",
80
80
  };
81
81
 
82
- // Two-tier menu. "Common" surfaces the day-to-day verbs. Settings is
83
- // the single home for previously top-level chrome (credentials, models,
84
- // harness, logs, defaults, appearance, networking) — those legacy Tab
85
- // values still exist for deep-link compat but no longer appear in this
86
- // menu grid.
87
- //
88
- // Capability-presence surfaces (documents, memory, MCP, extensions,
89
- // bridges, built-in tool categories) all live under the "Tools" tab now —
90
- // they were scattered across the top-level menu before but they're all
91
- // answering the same question: "what can the agent see / do?". The legacy
92
- // top-level Tab entries remain wired so deep-links (?tab=documents&item=…)
93
- // still resolve, but they're hidden from the menu grid.
94
- const COMMON_TABS: Tab[] = ["chat", "dashboard", "agents", "tools", "tasks", "profile", "settings"];
95
- const ADVANCED_TABS: Tab[] = [];
82
+ // Keep everyday destinations visible; move the operational and capability
83
+ // panels behind Full mode's Advanced section.
84
+ const COMMON_TABS: Tab[] = ["chat", "agents", "settings", "profile"];
85
+ const ADVANCED_TABS: Tab[] = ["dashboard", "tasks", "tools", "memory"];
96
86
 
97
87
  const ADVANCED_KEY = "jarela.menu.advanced";
98
88
 
@@ -42,7 +42,7 @@ export function ProfilePanel() {
42
42
  }
43
43
 
44
44
  return (
45
- <div className="flex flex-col h-full">
45
+ <div className="mx-auto flex h-full w-full max-w-5xl flex-col">
46
46
  <PanelHeader icon={<User size={14} />} title="Profile">
47
47
  {isEssential && (
48
48
  <button
@@ -56,7 +56,7 @@ export function ProfilePanel() {
56
56
  </button>
57
57
  )}
58
58
  </PanelHeader>
59
- <div ref={containerRef} className="flex-1 overflow-y-auto no-scrollbar max-w-lg mx-auto w-full">
59
+ <div ref={containerRef} className="min-h-0 flex-1 overflow-y-auto no-scrollbar">
60
60
  <ProfileEditor />
61
61
  </div>
62
62
  </div>
@@ -1,8 +1,9 @@
1
1
  "use client";
2
- import { Monitor, Moon, Palette, Sun } from "lucide-react";
2
+ import { Check, Monitor, Moon, Palette, Sun } from "lucide-react";
3
3
  import { PanelHeader } from "@/components/ui/PanelHeader";
4
- import { useTheme, type Theme } from "@/contexts/ThemeContext";
4
+ import { useTheme, type AccentColor, type SurfaceStyle, type Theme } from "@/contexts/ThemeContext";
5
5
  import { useAppContext, type ExperienceMode } from "@/contexts/AppContext";
6
+ import { getAppAccentColor } from "@/lib/env/app-config";
6
7
 
7
8
  // Visual + chrome settings that used to live in the MenuPanel footer
8
9
  // (theme) and the top of the menu (Workspace mode). Hoisted here so
@@ -11,8 +12,9 @@ import { useAppContext, type ExperienceMode } from "@/contexts/AppContext";
11
12
 
12
13
  export function AppearancePanel() {
13
14
  const { state, dispatch } = useAppContext();
14
- const { theme, setTheme } = useTheme();
15
+ const { theme, setTheme, accentColor, setAccentColor, surfaceStyle, setSurfaceStyle } = useTheme();
15
16
  const isFullMode = state.experienceMode === "full";
17
+ const defaultAccent = getAppAccentColor() ?? "#2563eb";
16
18
 
17
19
  const themeOptions: { value: Theme; label: string; icon: React.ReactNode; description: string }[] = [
18
20
  { value: "light", label: "Light", icon: <Sun size={14} />, description: "Bright UI; ignores system preference." },
@@ -25,6 +27,21 @@ export function AppearancePanel() {
25
27
  { value: "full", label: "Full", description: "Everything visible: engine-room tabs, power-user settings, all advanced sub-tabs." },
26
28
  ];
27
29
 
30
+ const hasBrandAccent = getAppAccentColor() !== null;
31
+ const accentOptions: { value: AccentColor; label: string; swatch: string }[] = [
32
+ { value: "default", label: hasBrandAccent ? "Brand" : "Blue", swatch: defaultAccent },
33
+ ...(hasBrandAccent ? [{ value: "blue" as const, label: "Blue", swatch: "#2563eb" }] : []),
34
+ { value: "teal", label: "Teal", swatch: "#0f766e" },
35
+ { value: "violet", label: "Violet", swatch: "#6d28d9" },
36
+ { value: "rose", label: "Rose", swatch: "#be123c" },
37
+ ];
38
+
39
+ const surfaceOptions: { value: SurfaceStyle; label: string; description: string }[] = [
40
+ { value: "ambient", label: "Atmospheric", description: "Keep the background glow and current glass surfaces." },
41
+ { value: "calm", label: "Calm", description: "Keep navigation glass subtle and the background quiet." },
42
+ { value: "liquid", label: "Liquid Glass", description: "Let background colors show through with brighter edge reflections." },
43
+ ];
44
+
28
45
  function setMode(mode: ExperienceMode) {
29
46
  if (mode === state.experienceMode) return;
30
47
  dispatch({ type: "SET_EXPERIENCE_MODE", mode });
@@ -35,6 +52,33 @@ export function AppearancePanel() {
35
52
  <PanelHeader icon={<Palette size={14} />} title="Appearance" />
36
53
 
37
54
  <div className="flex-1 overflow-y-auto no-scrollbar px-4 py-4 space-y-6">
55
+ <section>
56
+ <h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Accent color</h3>
57
+ <div className="flex flex-wrap gap-2">
58
+ {accentOptions.map((option) => {
59
+ const active = accentColor === option.value;
60
+ return (
61
+ <button
62
+ key={option.value}
63
+ type="button"
64
+ onClick={() => setAccentColor(option.value)}
65
+ aria-label={`${option.label} accent color`}
66
+ aria-pressed={active}
67
+ title={option.label}
68
+ className={`control-tap inline-flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-xs transition-colors ${
69
+ active ? "border-accent bg-accent/10 text-fg" : "border-border bg-surface-2 text-fg-muted hover:bg-surface-3"
70
+ }`}
71
+ >
72
+ <span className="inline-flex h-4 w-4 items-center justify-center rounded-full" style={{ backgroundColor: option.swatch }}>
73
+ {active && <Check size={11} className="text-white" />}
74
+ </span>
75
+ {option.label}
76
+ </button>
77
+ );
78
+ })}
79
+ </div>
80
+ </section>
81
+
38
82
  <section>
39
83
  <h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Theme</h3>
40
84
  <div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
@@ -62,6 +106,29 @@ export function AppearancePanel() {
62
106
  </div>
63
107
  </section>
64
108
 
109
+ <section>
110
+ <h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Surface style</h3>
111
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
112
+ {surfaceOptions.map((option) => {
113
+ const active = surfaceStyle === option.value;
114
+ return (
115
+ <button
116
+ key={option.value}
117
+ type="button"
118
+ onClick={() => setSurfaceStyle(option.value)}
119
+ aria-pressed={active}
120
+ className={`text-left rounded-lg border px-3 py-2.5 transition-colors ${
121
+ active ? "border-accent bg-accent/10 text-fg" : "border-border bg-surface-2 text-fg-muted hover:bg-surface-3"
122
+ }`}
123
+ >
124
+ <div className="text-sm font-medium text-fg mb-1">{option.label}</div>
125
+ <p className="text-[11px] text-fg-faint leading-snug">{option.description}</p>
126
+ </button>
127
+ );
128
+ })}
129
+ </div>
130
+ </section>
131
+
65
132
  <section>
66
133
  <h3 className="text-[11px] uppercase tracking-wide text-fg-faint mb-2 px-1">Workspace mode</h3>
67
134
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
@@ -92,7 +92,7 @@ export function SettingsPanel() {
92
92
  }));
93
93
 
94
94
  return (
95
- <div className="flex flex-col h-full min-h-0">
95
+ <div className="mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col">
96
96
  <SubTabBar
97
97
  ariaLabel="Settings sub-section"
98
98
  tabs={tabItems}
@@ -120,7 +120,7 @@ export function SettingsPanel() {
120
120
  // future change to either panel without touching this file.
121
121
  function PrivacySecurityPanel() {
122
122
  return (
123
- <div className="h-full overflow-y-auto no-scrollbar max-w-lg mx-auto w-full px-4 py-3">
123
+ <div className="h-full w-full overflow-y-auto px-4 py-3 no-scrollbar">
124
124
  <SettingsGroup defaultOpenId="pin">
125
125
  <SecurityPanel />
126
126
  <RedactionPanel />
@@ -2,7 +2,6 @@
2
2
  import { useAppContext } from "@/contexts/AppContext";
3
3
  import { MCPPanel } from "@/components/mcp/MCPPanel";
4
4
  import { SkillsPanel } from "@/components/skills/SkillsPanel";
5
- import { UnifiedMemoryPanel } from "@/components/memory/UnifiedMemoryPanel";
6
5
  import { BridgesPanel } from "@/components/bridges/BridgesPanel";
7
6
  import { BrowserPanel } from "@/components/tools/BrowserPanel";
8
7
  import { PackagesPanel } from "./PackagesPanel";
@@ -13,7 +12,7 @@ import { SubTabBar, type SubTabItem } from "@/components/ui/SubTabBar";
13
12
  // manifests, and drop-in `.cjs` files. One home for
14
13
  // everything that turns into a LangChain tool.
15
14
  // - "Skills" — markdown playbooks (repos) the agent reads/writes on demand.
16
- // - "Memory" — searchable chats and documents, plus saved facts.
15
+ // - Knowledge now has its own Advanced-menu destination.
17
16
  // - "MCP" — external Model Context Protocol servers.
18
17
  // - "Bridges" — mobile / messaging bridge pairings.
19
18
  // - "Browser" — the local Chromium extension as an agent runtime surface.
@@ -23,17 +22,15 @@ type Sub =
23
22
  | "packages"
24
23
  | "browser"
25
24
  | "skills"
26
- | "memory"
27
25
  | "mcp"
28
26
  | "bridges";
29
27
 
30
- const SUBS: Sub[] = ["packages", "browser", "skills", "memory", "mcp", "bridges"];
28
+ const SUBS: Sub[] = ["packages", "browser", "skills", "mcp", "bridges"];
31
29
 
32
30
  const SUB_TITLES: Record<Sub, string> = {
33
31
  packages: "Packages",
34
32
  browser: "Browser",
35
33
  skills: "Skills",
36
- memory: "Memory",
37
34
  mcp: "MCP servers",
38
35
  bridges: "Bridges",
39
36
  };
@@ -43,7 +40,6 @@ const SUB_TITLES: Record<Sub, string> = {
43
40
  const LEGACY_SUBS: Record<string, Sub> = {
44
41
  builtin: "packages",
45
42
  extensions: "packages",
46
- documents: "memory",
47
43
  };
48
44
 
49
45
  export function ToolsPanel() {
@@ -70,7 +66,6 @@ export function ToolsPanel() {
70
66
  {active === "packages" && <PackagesPanel />}
71
67
  {active === "browser" && <BrowserPanel />}
72
68
  {active === "skills" && <SkillsPanel />}
73
- {active === "memory" && <UnifiedMemoryPanel />}
74
69
  {active === "mcp" && <MCPPanel />}
75
70
  {active === "bridges" && <BridgesPanel />}
76
71
  </div>
@@ -0,0 +1,68 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
4
+ import { ThemeProvider, useTheme } from "@/contexts/ThemeContext";
5
+
6
+ function PreferenceProbe() {
7
+ const { accentColor, setAccentColor, surfaceStyle, setSurfaceStyle } = useTheme();
8
+ return (
9
+ <div>
10
+ <span data-testid="accent">{accentColor}</span>
11
+ <span data-testid="surface">{surfaceStyle}</span>
12
+ <button onClick={() => setAccentColor("teal")}>Choose teal</button>
13
+ <button onClick={() => setAccentColor("default")}>Choose default</button>
14
+ <button onClick={() => setSurfaceStyle("ambient")}>Choose atmospheric</button>
15
+ <button onClick={() => setSurfaceStyle("liquid")}>Choose Liquid Glass</button>
16
+ </div>
17
+ );
18
+ }
19
+
20
+ describe("ThemeContext appearance preferences", () => {
21
+ beforeEach(() => {
22
+ localStorage.clear();
23
+ vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({
24
+ matches: false,
25
+ addEventListener: vi.fn(),
26
+ removeEventListener: vi.fn(),
27
+ }));
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.unstubAllGlobals();
32
+ document.documentElement.removeAttribute("data-accent-color");
33
+ document.documentElement.removeAttribute("data-surface-style");
34
+ });
35
+
36
+ it("restores, applies, and resets appearance choices", async () => {
37
+ localStorage.setItem("jarela-accent-color", "rose");
38
+ localStorage.setItem("jarela-surface-style", "calm");
39
+ render(
40
+ <ThemeProvider>
41
+ <PreferenceProbe />
42
+ </ThemeProvider>,
43
+ );
44
+
45
+ await waitFor(() => {
46
+ expect(screen.getByTestId("accent").textContent).toBe("rose");
47
+ expect(screen.getByTestId("surface").textContent).toBe("calm");
48
+ expect(document.documentElement.dataset.accentColor).toBe("rose");
49
+ expect(document.documentElement.dataset.surfaceStyle).toBe("calm");
50
+ });
51
+
52
+ fireEvent.click(screen.getByRole("button", { name: "Choose teal" }));
53
+ expect(document.documentElement.dataset.accentColor).toBe("teal");
54
+ expect(localStorage.getItem("jarela-accent-color")).toBe("teal");
55
+
56
+ fireEvent.click(screen.getByRole("button", { name: "Choose default" }));
57
+ expect(document.documentElement.hasAttribute("data-accent-color")).toBe(false);
58
+ expect(localStorage.getItem("jarela-accent-color")).toBeNull();
59
+
60
+ fireEvent.click(screen.getByRole("button", { name: "Choose atmospheric" }));
61
+ expect(document.documentElement.dataset.surfaceStyle).toBe("ambient");
62
+ expect(localStorage.getItem("jarela-surface-style")).toBe("ambient");
63
+
64
+ fireEvent.click(screen.getByRole("button", { name: "Choose Liquid Glass" }));
65
+ expect(document.documentElement.dataset.surfaceStyle).toBe("liquid");
66
+ expect(localStorage.getItem("jarela-surface-style")).toBe("liquid");
67
+ });
68
+ });
@@ -0,0 +1,44 @@
1
+ // @vitest-environment jsdom
2
+ import { act, render } from "@testing-library/react";
3
+ import { afterEach, expect, it, vi } from "vitest";
4
+ import { BootScreen } from "./BootScreen";
5
+
6
+ const reads = vi.hoisted(() => ({
7
+ profile: vi.fn(),
8
+ agent: vi.fn(),
9
+ thread: vi.fn(),
10
+ messages: vi.fn(),
11
+ }));
12
+ vi.mock("@/api/client", () => ({
13
+ api: {
14
+ profile: { get: reads.profile },
15
+ agents: { get: reads.agent, getThread: reads.thread },
16
+ threads: { get: reads.messages },
17
+ },
18
+ }));
19
+ vi.mock("@/components/ui/Logo", () => ({ Logo: () => null }));
20
+ vi.mock("@/components/ui/VersionTag", () => ({ VersionTag: () => null }));
21
+ vi.mock("@/components/ui/PoweredBy", () => ({ PoweredBy: () => null }));
22
+ vi.mock("@/components/ui/WorkflowChecklist", () => ({ WorkflowChecklist: () => null }));
23
+
24
+ afterEach(() => { vi.useRealTimers(); });
25
+
26
+ it("does not restart boot prefetch after dismissal when the agent changes", async () => {
27
+ vi.useFakeTimers();
28
+ reads.profile.mockResolvedValue({});
29
+ reads.agent.mockResolvedValue({});
30
+ reads.thread.mockResolvedValue({ thread_id: "thread" });
31
+ reads.messages.mockResolvedValue({});
32
+ const onPickAgent = vi.fn();
33
+ const { rerender, container } = render(
34
+ <BootScreen agents={[]} agentsLoaded activeAgentId="first" onPickAgent={onPickAgent} />,
35
+ );
36
+ await act(async () => {});
37
+ await act(async () => { await vi.advanceTimersByTimeAsync(750); });
38
+ expect(container.innerHTML).toBe("");
39
+ rerender(<BootScreen agents={[]} agentsLoaded activeAgentId="second" onPickAgent={onPickAgent} />);
40
+ await act(async () => {});
41
+ expect(reads.agent).toHaveBeenCalledTimes(1);
42
+ expect(reads.agent).toHaveBeenCalledWith("first", { startup: true });
43
+ expect(reads.thread).toHaveBeenCalledTimes(1);
44
+ });
@@ -61,6 +61,7 @@ interface Props {
61
61
  agents: AgentConfig[];
62
62
  agentsLoaded: boolean;
63
63
  activeAgentId: string | null;
64
+ preferredThreadId?: string | null;
64
65
  onPickAgent: (agentId: string) => void;
65
66
  // Boot picker is a chat-tab gate; suppress on non-chat tabs so it doesn't overlay them.
66
67
  suppressed?: boolean;
@@ -78,7 +79,7 @@ interface Props {
78
79
  // Replaces the previous splash + ChatView empty-state agent picker. The
79
80
  // header (z-40) is hidden while this is visible so the user lands on a
80
81
  // focused, one-purpose screen.
81
- export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, suppressed }: Props) {
82
+ export function BootScreen({ agents, agentsLoaded, activeAgentId, preferredThreadId, onPickAgent, suppressed }: Props) {
82
83
  // Capture which agent we transitioned through so the icon can keep
83
84
  // showing during the fade-out, even if a parent state shift would
84
85
  // otherwise clear it.
@@ -96,7 +97,6 @@ export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, s
96
97
  const [adoptionBusy, setAdoptionBusy] = useState<VersionAdoptionAction | null>(null);
97
98
  const [adoptionLiveLabel, setAdoptionLiveLabel] = useState<string | null>(null);
98
99
  const adoptionRef = useRef<VersionAdoptionState | null>(null);
99
- const prefetchStartedRef = useRef(false);
100
100
  const adoptionRunStartedRef = useRef<string | null>(null);
101
101
  const adoptionAbortRef = useRef<AbortController | null>(null);
102
102
  adoptionRef.current = adoption;
@@ -244,9 +244,8 @@ export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, s
244
244
  // half-rendered chat that has to fetch the same data anyway.
245
245
  useEffect(() => {
246
246
  const id = activeAgentId ?? pickedId;
247
- if (!id || !agentsLoaded) return;
248
- if (prefetchStartedRef.current) return;
249
- prefetchStartedRef.current = true;
247
+ if (!id || !agentsLoaded || done) return;
248
+ setDismissing(false);
250
249
 
251
250
  setSteps([
252
251
  { key: "profile", label: "loading your profile", status: "active" },
@@ -259,22 +258,36 @@ export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, s
259
258
 
260
259
  void (async () => {
261
260
  await Promise.allSettled([
262
- api.profile.get().finally(() => {
261
+ api.profile.get({ startup: true }).catch((error: unknown) => {
262
+ console.error("startup profile warmup failed", error);
263
+ }).finally(() => {
263
264
  if (!cancelled) markStep("profile", "done");
264
265
  }),
265
266
  api.agents
266
- .get(id)
267
+ .get(id, { startup: true })
268
+ .catch((error: unknown) => {
269
+ console.error("startup agent warmup failed", error);
270
+ })
267
271
  .finally(() => {
268
272
  if (!cancelled) markStep("agent", "done");
269
273
  }),
270
274
  (async () => {
271
275
  try {
272
- const t = await api.agents.getThread(id);
276
+ if (preferredThreadId) {
277
+ try {
278
+ const preferred = await api.threads.get(preferredThreadId, { startup: true });
279
+ if (preferred.agent_id === id) return;
280
+ } catch (error) {
281
+ console.error("startup selected conversation warmup failed", error);
282
+ }
283
+ }
284
+ const t = await api.agents.getThread(id, { startup: true });
273
285
  if (cancelled) return;
274
286
  markStep("thread", "done");
275
287
  markStep("messages", "active");
276
- await api.threads.get(t.thread_id);
277
- } catch {
288
+ await api.threads.get(t.thread_id, { startup: true });
289
+ } catch (error) {
290
+ console.error("startup conversation warmup failed", error);
278
291
  // Errors here are non-fatal for boot — ChatView will surface
279
292
  // them with its own retry path. Just mark the steps done so
280
293
  // we don't block the user behind the splash forever.
@@ -301,7 +314,7 @@ export function BootScreen({ agents, agentsLoaded, activeAgentId, onPickAgent, s
301
314
  cancelled = true;
302
315
  if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
303
316
  };
304
- }, [activeAgentId, pickedId, agentsLoaded, markStep]);
317
+ }, [activeAgentId, pickedId, preferredThreadId, agentsLoaded, markStep, done]);
305
318
 
306
319
  if (done) return null;
307
320
  if (suppressed) return null;
@@ -21,12 +21,12 @@ describe("NotificationStatus", () => {
21
21
  it("renders a theme-safe denied state with an accessible dismiss action", async () => {
22
22
  render(<NotificationStatus />);
23
23
 
24
- const copy = await screen.findByText(/OS notifications are blocked/);
24
+ const copy = await screen.findByText(/System notifications are blocked/);
25
25
  const banner = copy.closest(".rounded-lg");
26
26
  expect(banner?.className).toContain("bg-rose-500/10");
27
27
  expect(screen.getByRole("button", { name: "Dismiss notification prompt" })).toBeTruthy();
28
28
 
29
29
  fireEvent.click(screen.getByRole("button", { name: "Dismiss notification prompt" }));
30
- expect(screen.queryByText(/OS notifications are blocked/)).toBeNull();
30
+ expect(screen.queryByText(/System notifications are blocked/)).toBeNull();
31
31
  });
32
32
  });
@@ -85,15 +85,15 @@ export function NotificationStatus() {
85
85
  : "border-amber-500/30 bg-amber-500/10";
86
86
 
87
87
  return (
88
- <div className="absolute top-9 left-0 right-0 z-30 mx-auto max-w-2xl px-4 mt-2 pointer-events-none">
89
- <div role="status" className={`pointer-events-auto rounded-lg border ${bannerTone} backdrop-blur px-3 py-2 flex items-center gap-2 shadow-lg`}>
88
+ <div className="absolute top-9 left-0 right-0 z-30 mx-auto max-w-xl px-4 mt-2 pointer-events-none">
89
+ <div role="status" className={`pointer-events-auto rounded-lg border ${bannerTone} backdrop-blur px-3 py-2 flex flex-wrap items-center gap-x-2 gap-y-1 shadow-lg`}>
90
90
  {status === "denied"
91
91
  ? <BellOff size={14} className="text-rose-700 dark:text-rose-400 shrink-0" />
92
92
  : <Bell size={14} className="text-amber-700 dark:text-amber-400 shrink-0" />}
93
- <p className="text-xs text-fg mr-auto">
93
+ <p className="min-w-0 flex-1 basis-full text-xs text-fg sm:basis-auto">
94
94
  {status === "denied"
95
- ? "OS notifications are blocked. In-app pop-ups still work; for system-level alerts when Jarela isn't focused, allow Notifications via the lock icon in the URL bar."
96
- : "Enable OS notifications to also get pinged when Jarela isn't focused (in-app pop-ups already work)."}
95
+ ? "System notifications are blocked. In-app alerts still work; change this in browser site settings."
96
+ : "Enable system alerts for when Jarela isn't focused."}
97
97
  </p>
98
98
  {status === "default" && (
99
99
  <button
@@ -159,11 +159,10 @@ function EdgeArrow({ side, onClick }: { side: "left" | "right"; onClick: () => v
159
159
  return (
160
160
  <button
161
161
  type="button"
162
- tabIndex={-1}
163
- aria-hidden="true"
162
+ aria-label={`Scroll tabs ${side}`}
164
163
  onClick={onClick}
165
164
  className={
166
- "absolute inset-y-0 w-8 flex items-center pointer-coarse:hidden text-fg-muted hover:text-fg from-[var(--bg-secondary)] via-[var(--bg-secondary)] to-transparent " +
165
+ "absolute inset-y-0 w-8 flex items-center text-fg-muted hover:text-fg from-[var(--bg-secondary)] via-[var(--bg-secondary)] to-transparent " +
167
166
  (side === "left"
168
167
  ? "bg-gradient-to-r left-0 justify-start pl-1"
169
168
  : "bg-gradient-to-l right-0 justify-end pr-1")