@circuitwall/jarela 2.3.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 (290) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +5 -5
  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/access/[identity]/route.js +7 -0
  19. package/.next/standalone/.next/server/app/api/v1/access/route.js +7 -0
  20. package/.next/standalone/.next/server/app/api/v1/agents/[id]/compact/route.js +7 -0
  21. package/.next/standalone/.next/server/app/api/v1/agents/[id]/display-filters/route.js +17 -5
  22. package/.next/standalone/.next/server/app/api/v1/agents/[id]/route.js +7 -0
  23. package/.next/standalone/.next/server/app/api/v1/agents/[id]/thread/route.js +17 -5
  24. package/.next/standalone/.next/server/app/api/v1/agents/route.js +7 -0
  25. package/.next/standalone/.next/server/app/api/v1/allowed-sites/[hostname]/route.js +7 -0
  26. package/.next/standalone/.next/server/app/api/v1/allowed-sites/route.js +7 -0
  27. package/.next/standalone/.next/server/app/api/v1/artifacts/lifecycle/route.js +7 -0
  28. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js +26 -32
  29. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/[remote_jid]/route.js +18 -7
  31. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/route.js +18 -7
  32. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js +26 -32
  33. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js +27 -33
  35. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +26 -32
  37. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/[route_id]/route.js +28 -12
  39. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/route.js +28 -12
  40. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js +26 -32
  41. package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js +9 -2
  43. package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/v1/bridges/route.js +19 -7
  45. package/.next/standalone/.next/server/app/api/v1/bridges/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/v1/dashboard/metrics/route.js +17 -5
  47. package/.next/standalone/.next/server/app/api/v1/dashboard/pricing/route.js +7 -0
  48. package/.next/standalone/.next/server/app/api/v1/dashboard/pricing-task/route.js +1 -1
  49. package/.next/standalone/.next/server/app/api/v1/documents/search/route.js +7 -0
  50. package/.next/standalone/.next/server/app/api/v1/documents/settings/route.js +7 -0
  51. package/.next/standalone/.next/server/app/api/v1/documents/sources/[id]/route.js +1 -1
  52. package/.next/standalone/.next/server/app/api/v1/documents/sources/route.js +1 -1
  53. package/.next/standalone/.next/server/app/api/v1/env-sync/allowlist/route.js +7 -0
  54. package/.next/standalone/.next/server/app/api/v1/env-sync/route.js +7 -0
  55. package/.next/standalone/.next/server/app/api/v1/events/route.js +26 -32
  56. package/.next/standalone/.next/server/app/api/v1/events/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/v1/extension/agents/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/v1/extension/browser/activate/route.js +7 -0
  59. package/.next/standalone/.next/server/app/api/v1/extension/browser/foreground/route.js +7 -0
  60. package/.next/standalone/.next/server/app/api/v1/extension/browser/history/route.js +7 -0
  61. package/.next/standalone/.next/server/app/api/v1/extension/browser/poll/route.js +7 -0
  62. package/.next/standalone/.next/server/app/api/v1/extension/browser/progress/route.js +7 -0
  63. package/.next/standalone/.next/server/app/api/v1/extension/browser/result/route.js +7 -0
  64. package/.next/standalone/.next/server/app/api/v1/extension/browser/retry/route.js +7 -0
  65. package/.next/standalone/.next/server/app/api/v1/extension/browser/status/route.js +7 -0
  66. package/.next/standalone/.next/server/app/api/v1/extension/browser/tabs/route.js +7 -0
  67. package/.next/standalone/.next/server/app/api/v1/extension/fill/route.js +1 -1
  68. package/.next/standalone/.next/server/app/api/v1/extension/refine/route.js +1 -1
  69. package/.next/standalone/.next/server/app/api/v1/extension/turn/route.js +1 -1
  70. package/.next/standalone/.next/server/app/api/v1/extensions/route.js +1 -1
  71. package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/config/route.js +1 -1
  72. package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/route.js +1 -1
  73. package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js +1 -1
  74. package/.next/standalone/.next/server/app/api/v1/harnesses/[id]/route.js +7 -0
  75. package/.next/standalone/.next/server/app/api/v1/harnesses/default/route.js +7 -0
  76. package/.next/standalone/.next/server/app/api/v1/harnesses/route.js +7 -0
  77. package/.next/standalone/.next/server/app/api/v1/health/route.js +17 -5
  78. package/.next/standalone/.next/server/app/api/v1/integrations/[name]/route.js +7 -0
  79. package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/callback/route.js +7 -0
  80. package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/start/route.js +7 -0
  81. package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/status/route.js +7 -0
  82. package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/callback/route.js +7 -0
  83. package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/start/route.js +7 -0
  84. package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/status/route.js +7 -0
  85. package/.next/standalone/.next/server/app/api/v1/integrations/route.js +7 -0
  86. package/.next/standalone/.next/server/app/api/v1/lifecycle/adoption/route.js +17 -5
  87. package/.next/standalone/.next/server/app/api/v1/maps/embed/route.js +7 -0
  88. package/.next/standalone/.next/server/app/api/v1/mcp-servers/[name]/route.js +17 -5
  89. package/.next/standalone/.next/server/app/api/v1/mcp-servers/route.js +17 -5
  90. package/.next/standalone/.next/server/app/api/v1/memory/[namespace]/[key]/route.js +7 -0
  91. package/.next/standalone/.next/server/app/api/v1/memory/policy/route.js +7 -0
  92. package/.next/standalone/.next/server/app/api/v1/memory/route.js +7 -0
  93. package/.next/standalone/.next/server/app/api/v1/memory/search/route.js +7 -0
  94. package/.next/standalone/.next/server/app/api/v1/models/[name]/compact-threads/route.js +7 -0
  95. package/.next/standalone/.next/server/app/api/v1/models/[name]/route.js +7 -0
  96. package/.next/standalone/.next/server/app/api/v1/models/route.js +7 -0
  97. package/.next/standalone/.next/server/app/api/v1/packages/defaults/[id]/route.js +28 -4
  98. package/.next/standalone/.next/server/app/api/v1/packages/reload/route.js +28 -4
  99. package/.next/standalone/.next/server/app/api/v1/packages/route.js +28 -4
  100. package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +1 -1
  101. package/.next/standalone/.next/server/app/api/v1/pending-actions/[id]/approve/route.js +27 -12
  102. package/.next/standalone/.next/server/app/api/v1/profile/location/route.js +7 -0
  103. package/.next/standalone/.next/server/app/api/v1/profile/route.js +7 -0
  104. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +585 -58
  105. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/probe/route.js +7 -0
  106. package/.next/standalone/.next/server/app/api/v1/providers/github-copilot/auth/route.js +7 -0
  107. package/.next/standalone/.next/server/app/api/v1/providers/route.js +7 -0
  108. package/.next/standalone/.next/server/app/api/v1/proxy-config/route.js +7 -0
  109. package/.next/standalone/.next/server/app/api/v1/redaction/init/route.js +7 -0
  110. package/.next/standalone/.next/server/app/api/v1/redaction/route.js +7 -0
  111. package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/route.js +17 -5
  112. package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/run/route.js +1 -1
  113. package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/route.js +7 -0
  114. package/.next/standalone/.next/server/app/api/v1/security/idle-timeout/route.js +7 -0
  115. package/.next/standalone/.next/server/app/api/v1/security/pin/route.js +7 -0
  116. package/.next/standalone/.next/server/app/api/v1/security/state/route.js +7 -0
  117. package/.next/standalone/.next/server/app/api/v1/security/unlock/route.js +7 -0
  118. package/.next/standalone/.next/server/app/api/v1/security/verify-pin/route.js +7 -0
  119. package/.next/standalone/.next/server/app/api/v1/skills/[id]/route.js +7 -0
  120. package/.next/standalone/.next/server/app/api/v1/skills/repos/[id]/route.js +7 -0
  121. package/.next/standalone/.next/server/app/api/v1/skills/repos/route.js +7 -0
  122. package/.next/standalone/.next/server/app/api/v1/skills/route.js +7 -0
  123. package/.next/standalone/.next/server/app/api/v1/tasks/[agent_id]/route.js +7 -0
  124. package/.next/standalone/.next/server/app/api/v1/tasks/route.js +7 -0
  125. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/activity/route.js +7 -0
  126. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/context-pin/route.js +12 -6
  127. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +125 -26
  128. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +1 -2
  129. package/.next/standalone/.next/server/app/api/v1/threads/route.js +17 -5
  130. package/.next/standalone/.next/server/app/api/v1/tools/route.js +1 -1
  131. package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
  132. package/.next/standalone/.next/server/app/api/v1/voice/transcribe/route.js +17 -5
  133. package/.next/standalone/.next/server/app/api/v1/voice/tts/route.js +17 -5
  134. package/.next/standalone/.next/server/app/api/v1/watchers/[id]/route.js +17 -5
  135. package/.next/standalone/.next/server/app/api/v1/watchers/[id]/run/route.js +1 -1
  136. package/.next/standalone/.next/server/app/api/v1/watchers/route.js +1 -1
  137. package/.next/standalone/.next/server/app/page.js +27323 -27034
  138. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  139. package/.next/standalone/.next/server/app/setup/page.js +8 -0
  140. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  141. package/.next/standalone/.next/server/app-paths-manifest.json +5 -5
  142. package/.next/standalone/.next/server/chunks/1388.js +106 -36
  143. package/.next/standalone/.next/server/chunks/2068.js +47 -10
  144. package/.next/standalone/.next/server/chunks/210.js +6 -6
  145. package/.next/standalone/.next/server/chunks/2447.js +10 -5
  146. package/.next/standalone/.next/server/chunks/2653.js +80 -1
  147. package/.next/standalone/.next/server/chunks/3042.js +10 -8
  148. package/.next/standalone/.next/server/chunks/{4540.js → 309.js} +104 -38
  149. package/.next/standalone/.next/server/chunks/{2350.js → 3272.js} +104 -37
  150. package/.next/standalone/.next/server/chunks/3524.js +150 -67
  151. package/.next/standalone/.next/server/chunks/3566.js +1 -1
  152. package/.next/standalone/.next/server/chunks/4189.js +1 -1
  153. package/.next/standalone/.next/server/chunks/4596.js +28 -4
  154. package/.next/standalone/.next/server/chunks/46.js +91 -31
  155. package/.next/standalone/.next/server/chunks/466.js +53 -7
  156. package/.next/standalone/.next/server/chunks/4955.js +30 -3
  157. package/.next/standalone/.next/server/chunks/5585.js +28 -4
  158. package/.next/standalone/.next/server/chunks/5908.js +63 -42
  159. package/.next/standalone/.next/server/chunks/6142.js +10 -2
  160. package/.next/standalone/.next/server/chunks/8461.js +10 -8
  161. package/.next/standalone/.next/server/chunks/9032.js +5 -5
  162. package/.next/standalone/.next/server/chunks/9164.js +487 -177
  163. package/.next/standalone/.next/server/chunks/9304.js +51 -12
  164. package/.next/standalone/.next/server/chunks/{423.js → 9590.js} +84 -24
  165. package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
  166. package/.next/standalone/.next/server/middleware.js +89 -21
  167. package/.next/standalone/.next/server/pages/404.html +14 -2
  168. package/.next/standalone/.next/server/pages/500.html +1 -1
  169. package/.next/standalone/.next/server/preview-props.json +3 -3
  170. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  171. package/.next/standalone/.next/static/chunks/{1395-e4a99912b8f83044.js → 1395-bfe1f3ef760c0f50.js} +54 -8
  172. package/.next/standalone/.next/static/chunks/app/{layout-9bc0b10127cb7d42.js → layout-7b24ad13faa3944b.js} +69 -2
  173. package/.next/standalone/.next/static/chunks/app/{page-213a623b37226452.js → page-2ef8d75d40eb4311.js} +10643 -10295
  174. package/.next/standalone/.next/static/css/c160ea862133acd2.css +5 -0
  175. package/.next/standalone/docs/adoption/changesets.json +2805 -0
  176. package/.next/standalone/package.json +1 -1
  177. package/CHANGELOG.md +52 -0
  178. package/api/client.models.test.ts +10 -0
  179. package/api/client.startup.test.ts +129 -0
  180. package/api/client.ts +51 -9
  181. package/api/thread-metadata.test.ts +72 -0
  182. package/api/types.ts +6 -3
  183. package/app/api/v1/providers/[provider]/models/route.test.ts +216 -6
  184. package/app/api/v1/providers/[provider]/models/route.ts +259 -40
  185. package/app/api/v1/threads/[thread_id]/route.ts +26 -23
  186. package/app/api/v1/threads/[thread_id]/run/route.ts +0 -1
  187. package/app/globals.css +119 -29
  188. package/app/layout.tsx +12 -0
  189. package/components/agents/AgentsPanel.tsx +5 -13
  190. package/components/chat/ChatView.tsx +9 -18
  191. package/components/chat/InputBar.tsx +1 -1
  192. package/components/chat/MessageBubble.tsx +13 -1
  193. package/components/chat/MessageList.test.tsx +45 -4
  194. package/components/chat/MessageList.tsx +123 -89
  195. package/components/chat/ToolList.tsx +88 -84
  196. package/components/chat/useThreadActivity.test.tsx +2 -2
  197. package/components/chat/useThreadActivity.ts +2 -2
  198. package/components/chat/useThreadData.ts +1 -30
  199. package/components/chat/useThreadMetadataPolling.test.tsx +128 -0
  200. package/components/chat/useThreadMetadataPolling.ts +50 -0
  201. package/components/chat/useUserProfileAndAgent.test.tsx +31 -0
  202. package/components/chat/useUserProfileAndAgent.ts +13 -2
  203. package/components/dashboard/DashboardPanel.tsx +1 -1
  204. package/components/dashboard/Series.tsx +34 -21
  205. package/components/dashboard/SummaryCards.tsx +1 -1
  206. package/components/integrations/NetworkSection.tsx +2 -2
  207. package/components/layout/AppShell.tsx +22 -7
  208. package/components/layout/MenuPanel.tsx +14 -24
  209. package/components/profile/ProfilePanel.tsx +2 -2
  210. package/components/settings/AppearancePanel.tsx +70 -3
  211. package/components/settings/SettingsPanel.tsx +2 -2
  212. package/components/tools/ToolsPanel.tsx +2 -7
  213. package/components/ui/AppearancePreferences.test.tsx +68 -0
  214. package/components/ui/BootScreen.test.tsx +44 -0
  215. package/components/ui/BootScreen.tsx +24 -11
  216. package/components/ui/NotificationStatus.test.tsx +2 -2
  217. package/components/ui/NotificationStatus.tsx +5 -5
  218. package/components/ui/SubTabBar.tsx +2 -3
  219. package/contexts/ThemeContext.tsx +84 -1
  220. package/hooks/useAgentSession.test.ts +12 -1
  221. package/hooks/useAgentSession.ts +6 -4
  222. package/hooks/useUrlSync.test.ts +13 -0
  223. package/hooks/useUrlSync.ts +0 -4
  224. package/lib/agents/citation-checker.test.ts +40 -2
  225. package/lib/agents/citation-checker.ts +56 -11
  226. package/lib/agents/context-boundary.ts +5 -6
  227. package/lib/agents/instruction-edits.test.ts +120 -0
  228. package/lib/agents/instruction-edits.ts +65 -42
  229. package/lib/agents/prepare/request.ts +0 -2
  230. package/lib/agents/prepare/system-prompt.test.ts +10 -0
  231. package/lib/agents/prepare/system-prompt.ts +8 -2
  232. package/lib/agents/proposals.test.ts +76 -0
  233. package/lib/agents/proposals.ts +13 -5
  234. package/lib/agents/run-thread.auto-boundary.test.ts +125 -5
  235. package/lib/agents/run-thread.ts +24 -9
  236. package/lib/agents/sqlite-checkpoint-saver.test.ts +91 -0
  237. package/lib/agents/warm-summary-background.ts +22 -2
  238. package/lib/agents/warm-topics.test.ts +155 -1
  239. package/lib/agents/warm-topics.ts +27 -7
  240. package/lib/bridges/dispatcher.ts +1 -0
  241. package/lib/bridges/message-role.test.ts +81 -0
  242. package/lib/bridges/message-role.ts +23 -10
  243. package/lib/bridges/types.ts +6 -0
  244. package/lib/bridges/whatsapp-link.test.ts +32 -0
  245. package/lib/bridges/whatsapp-link.ts +21 -0
  246. package/lib/bridges/whatsapp.inbound.test.ts +239 -0
  247. package/lib/bridges/whatsapp.sender.test.ts +80 -1
  248. package/lib/bridges/whatsapp.ts +76 -38
  249. package/lib/db/migrations.test.ts +61 -0
  250. package/lib/db/migrations.ts +13 -0
  251. package/lib/embeddings/performance.test.ts +114 -0
  252. package/lib/pricing/snapshot.test.ts +70 -0
  253. package/lib/pricing/snapshot.ts +10 -2
  254. package/lib/proxy/dispatcher.test.ts +119 -1
  255. package/lib/proxy/dispatcher.ts +22 -9
  256. package/lib/stores/agent-configs.ts +10 -5
  257. package/lib/stores/bridges.test.ts +81 -0
  258. package/lib/stores/bridges.ts +9 -8
  259. package/lib/stores/dashboard-metrics-cache.test.ts +99 -0
  260. package/lib/stores/dashboard-metrics.ts +30 -4
  261. package/lib/stores/message-embedding-jobs.test.ts +27 -0
  262. package/lib/stores/message-embedding-jobs.ts +5 -3
  263. package/lib/stores/message-usage.test.ts +21 -1
  264. package/lib/stores/pricing-cache.test.ts +31 -0
  265. package/lib/stores/pricing.ts +3 -0
  266. package/lib/stores/proxy-config.test.ts +120 -0
  267. package/lib/stores/proxy-config.ts +22 -4
  268. package/lib/stores/threads.ts +12 -8
  269. package/lib/tools/communications/calendar.test.ts +43 -2
  270. package/lib/tools/communications/calendar.ts +2 -0
  271. package/lib/tools/communications/gmail.test.ts +51 -1
  272. package/lib/tools/communications/gmail.ts +44 -2
  273. package/lib/tools/communications/outlook-calendar.ts +2 -0
  274. package/lib/tools/communications/outlook.test.ts +33 -2
  275. package/lib/tools/communications/outlook.ts +2 -0
  276. package/lib/tools/filesystem/edit-match.test.ts +133 -0
  277. package/lib/tools/filesystem/edit-match.ts +7 -2
  278. package/lib/tools/packages/default-packages.test.ts +21 -0
  279. package/lib/tools/packages/optional-package.test.ts +72 -0
  280. package/lib/tools/packages/optional-package.ts +33 -4
  281. package/lib/tools/support/result-refs.test.ts +131 -0
  282. package/lib/tools/support/result-refs.ts +41 -9
  283. package/lib/ui/navigate.test.ts +13 -0
  284. package/lib/ui/navigate.ts +10 -2
  285. package/lib/utils/source-link.test.ts +51 -0
  286. package/lib/utils/source-link.ts +45 -0
  287. package/package.json +1 -1
  288. package/.next/standalone/.next/static/css/312542a5725ba0d5.css +0 -5
  289. /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → 544OFJV-q0RC6Gjgje4LT}/_buildManifest.js +0 -0
  290. /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → 544OFJV-q0RC6Gjgje4LT}/_ssgManifest.js +0 -0
@@ -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")
@@ -2,13 +2,25 @@
2
2
  import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react";
3
3
 
4
4
  export type Theme = "light" | "dark" | "system";
5
+ export type AccentColor = "default" | "blue" | "teal" | "violet" | "rose";
6
+ export type SurfaceStyle = "ambient" | "calm" | "liquid";
5
7
 
6
8
  const STORAGE_KEY = "jarela-theme";
9
+ const ACCENT_STORAGE_KEY = "jarela-accent-color";
10
+ const SURFACE_STORAGE_KEY = "jarela-surface-style";
7
11
 
8
12
  function isTheme(v: unknown): v is Theme {
9
13
  return v === "light" || v === "dark" || v === "system";
10
14
  }
11
15
 
16
+ function isAccentColor(v: unknown): v is AccentColor {
17
+ return v === "default" || v === "blue" || v === "teal" || v === "violet" || v === "rose";
18
+ }
19
+
20
+ function isSurfaceStyle(v: unknown): v is SurfaceStyle {
21
+ return v === "ambient" || v === "calm" || v === "liquid";
22
+ }
23
+
12
24
  function readStored(): Theme {
13
25
  if (typeof window === "undefined") return "system";
14
26
  try {
@@ -19,6 +31,26 @@ function readStored(): Theme {
19
31
  }
20
32
  }
21
33
 
34
+ function readStoredAccent(): AccentColor {
35
+ if (typeof window === "undefined") return "blue";
36
+ try {
37
+ const value = window.localStorage.getItem(ACCENT_STORAGE_KEY);
38
+ return isAccentColor(value) ? value : "default";
39
+ } catch {
40
+ return "default";
41
+ }
42
+ }
43
+
44
+ function readStoredSurface(): SurfaceStyle {
45
+ if (typeof window === "undefined") return "ambient";
46
+ try {
47
+ const value = window.localStorage.getItem(SURFACE_STORAGE_KEY);
48
+ return isSurfaceStyle(value) ? value : "ambient";
49
+ } catch {
50
+ return "ambient";
51
+ }
52
+ }
53
+
22
54
  const LIGHT_CHROME = "#ffffff";
23
55
  const DARK_CHROME = "#09090b";
24
56
 
@@ -44,9 +76,27 @@ function apply(theme: Theme) {
44
76
  syncChrome(theme);
45
77
  }
46
78
 
79
+ function applyAccentColor(accentColor: AccentColor) {
80
+ if (typeof document === "undefined") return;
81
+ if (accentColor === "default") {
82
+ document.documentElement.removeAttribute("data-accent-color");
83
+ return;
84
+ }
85
+ document.documentElement.setAttribute("data-accent-color", accentColor);
86
+ }
87
+
88
+ function applySurfaceStyle(surfaceStyle: SurfaceStyle) {
89
+ if (typeof document === "undefined") return;
90
+ document.documentElement.setAttribute("data-surface-style", surfaceStyle);
91
+ }
92
+
47
93
  interface Ctx {
48
94
  theme: Theme;
49
95
  setTheme: (t: Theme) => void;
96
+ accentColor: AccentColor;
97
+ setAccentColor: (color: AccentColor) => void;
98
+ surfaceStyle: SurfaceStyle;
99
+ setSurfaceStyle: (style: SurfaceStyle) => void;
50
100
  }
51
101
 
52
102
  const ThemeContext = createContext<Ctx | null>(null);
@@ -56,10 +106,18 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
56
106
  // which writes data-theme before React hydrates. The effect below syncs the
57
107
  // React state to whatever the script (or localStorage) decided.
58
108
  const [theme, setThemeState] = useState<Theme>("system");
109
+ const [accentColor, setAccentColorState] = useState<AccentColor>("blue");
110
+ const [surfaceStyle, setSurfaceStyleState] = useState<SurfaceStyle>("ambient");
59
111
 
60
112
  useEffect(() => {
61
113
  const stored = readStored();
114
+ const storedAccent = readStoredAccent();
115
+ const storedSurface = readStoredSurface();
62
116
  setThemeState(stored);
117
+ setAccentColorState(storedAccent);
118
+ setSurfaceStyleState(storedSurface);
119
+ applyAccentColor(storedAccent);
120
+ applySurfaceStyle(storedSurface);
63
121
  // When in "system" mode, mirror OS-level changes into the PWA chrome.
64
122
  if (typeof window === "undefined") return;
65
123
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
@@ -80,7 +138,32 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
80
138
  }
81
139
  }, []);
82
140
 
83
- return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;
141
+ const setAccentColor = useCallback((color: AccentColor) => {
142
+ setAccentColorState(color);
143
+ applyAccentColor(color);
144
+ try {
145
+ if (color === "default") window.localStorage.removeItem(ACCENT_STORAGE_KEY);
146
+ else window.localStorage.setItem(ACCENT_STORAGE_KEY, color);
147
+ } catch {
148
+ /* ignore quota / private-mode errors */
149
+ }
150
+ }, []);
151
+
152
+ const setSurfaceStyle = useCallback((style: SurfaceStyle) => {
153
+ setSurfaceStyleState(style);
154
+ applySurfaceStyle(style);
155
+ try {
156
+ window.localStorage.setItem(SURFACE_STORAGE_KEY, style);
157
+ } catch {
158
+ /* ignore quota / private-mode errors */
159
+ }
160
+ }, []);
161
+
162
+ return (
163
+ <ThemeContext.Provider value={{ theme, setTheme, accentColor, setAccentColor, surfaceStyle, setSurfaceStyle }}>
164
+ {children}
165
+ </ThemeContext.Provider>
166
+ );
84
167
  }
85
168
 
86
169
  export function useTheme() {
@@ -1,6 +1,6 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
- import { renderHook, waitFor } from "@testing-library/react";
3
+ import { act, renderHook, waitFor } from "@testing-library/react";
4
4
  import { beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import { useAgentSession } from "@/hooks/useAgentSession";
6
6
 
@@ -11,6 +11,7 @@ vi.mock("@/api/client", () => ({
11
11
  api: {
12
12
  threads: {
13
13
  get: (...args: unknown[]) => threadGetMock(...args),
14
+ metadata: (...args: unknown[]) => threadGetMock(...args),
14
15
  },
15
16
  agents: {
16
17
  getThread: (...args: unknown[]) => agentGetThreadMock(...args),
@@ -35,5 +36,15 @@ describe("useAgentSession contract", () => {
35
36
  expect(result.current.loading).toBe(result.current.state.loading);
36
37
  expect(result.current.refresh).toBe(result.current.commands.refresh);
37
38
  expect(agentGetThreadMock).not.toHaveBeenCalled();
39
+ expect(threadGetMock).toHaveBeenCalledWith("preferred-thread", { startup: true });
40
+ });
41
+
42
+ it("uses the startup handoff initially but keeps explicit refreshes fresh", async () => {
43
+ agentGetThreadMock.mockResolvedValue({ thread_id: "thread" });
44
+ const { result } = renderHook(() => useAgentSession("agent"));
45
+ await waitFor(() => expect(result.current.threadId).toBe("thread"));
46
+ expect(agentGetThreadMock).toHaveBeenLastCalledWith("agent", { startup: true });
47
+ await act(async () => { await result.current.refresh(); });
48
+ expect(agentGetThreadMock).toHaveBeenLastCalledWith("agent", undefined);
38
49
  });
39
50
  });
@@ -15,7 +15,7 @@ export function useAgentSession(
15
15
  const [error, setError] = useState<string | null>(null);
16
16
  const requestSeqRef = useRef(0);
17
17
 
18
- const refresh = useCallback(async () => {
18
+ const refresh = useCallback(async (opts?: { startup?: boolean }) => {
19
19
  if (!agentId) {
20
20
  setThreadId(null);
21
21
  setError(null);
@@ -32,7 +32,9 @@ export function useAgentSession(
32
32
  // prefer it as long as it still belongs to this agent.
33
33
  if (preferredThreadId) {
34
34
  try {
35
- const d = await api.threads.get(preferredThreadId, { limit: 1 });
35
+ const d = opts?.startup
36
+ ? await api.threads.get(preferredThreadId, { startup: true })
37
+ : await api.threads.metadata(preferredThreadId);
36
38
  if (mySeq !== requestSeqRef.current) return;
37
39
  if (d.agent_id === agentId) {
38
40
  setThreadId(preferredThreadId);
@@ -43,7 +45,7 @@ export function useAgentSession(
43
45
  }
44
46
  }
45
47
 
46
- const t = await api.agents.getThread(agentId);
48
+ const t = await api.agents.getThread(agentId, opts);
47
49
  if (mySeq !== requestSeqRef.current) return;
48
50
  setThreadId(t.thread_id);
49
51
  } catch (err) {
@@ -70,7 +72,7 @@ export function useAgentSession(
70
72
  return;
71
73
  }
72
74
 
73
- void refresh();
75
+ void refresh({ startup: true });
74
76
 
75
77
  }, [agentId, preferredThreadId, refresh]);
76
78
 
@@ -30,4 +30,17 @@ describe("useUrlSync contract", () => {
30
30
  replaceStateSpy.mockRestore();
31
31
  });
32
32
  });
33
+
34
+ it.each([
35
+ "/?tab=documents",
36
+ "/?tab=tools&item=documents",
37
+ "/?tab=tools&item=memory",
38
+ ])("routes legacy Knowledge URL %s to the Advanced Knowledge panel", async (url) => {
39
+ window.history.replaceState(null, "", url);
40
+ renderHook(() => useUrlSync(), { wrapper });
41
+
42
+ await waitFor(() => {
43
+ expect(window.location.search).toBe("?tab=memory");
44
+ });
45
+ });
33
46
  });
@@ -38,10 +38,6 @@ export function useUrlSync(): UnifiedHookResult<
38
38
  tab = "tools";
39
39
  item = "packages";
40
40
  }
41
- if (tab === "documents" || tab === "memory") {
42
- tab = "tools";
43
- item = "memory";
44
- }
45
41
  // Browser-extension "Open Jarela" passes ?agent=<id> (and optionally
46
42
  // ?thread=<id>) so the app lands on the user's currently-picked
47
43
  // target agent instead of whichever chat was last open. The agent
@@ -27,11 +27,27 @@ afterAll(() => {
27
27
  });
28
28
 
29
29
  describe("normalizeSource", () => {
30
- it("lowercases + strips query/fragment + trailing slash on URLs", () => {
31
- expect(normalizeSource("HTTPS://Example.com/Docs/?x=1#a")).toBe("https://example.com/docs");
30
+ it("lowercases + strips tracking params, page anchors and trailing slash on URLs", () => {
31
+ expect(normalizeSource("HTTPS://Example.com/Docs/?utm_source=x&fbclid=y#a")).toBe("https://example.com/docs");
32
32
  expect(normalizeSource("https://a.io/p/")).toBe("https://a.io/p");
33
33
  });
34
34
 
35
+ it("keeps the query and hash route that identify a message permalink", () => {
36
+ const a = normalizeSource("https://outlook.office365.com/owa/?ItemID=AAMk%2BA%3D&exvsurl=1&viewmodel=ReadMessageItem");
37
+ const b = normalizeSource("https://outlook.office365.com/owa/?ItemID=AAMk%2BB%3D&exvsurl=1&viewmodel=ReadMessageItem");
38
+ expect(a).not.toBe(b);
39
+ // Base64 item ids are case-sensitive; folding case would merge distinct messages.
40
+ expect(a).toContain("ItemID=AAMk%2BA%3D");
41
+ expect(normalizeSource("https://mail.google.com/mail/u/0/#all/18c2f0a1b2c3d4e5"))
42
+ .toBe("https://mail.google.com/mail/u/0#all/18c2f0a1b2c3d4e5");
43
+ expect(normalizeSource("https://www.youtube.com/watch?v=dQw4w9WgXcQ&utm_medium=share"))
44
+ .toBe("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
45
+ });
46
+
47
+ it("treats the same permalink with reordered params as one source", () => {
48
+ expect(normalizeSource("https://x.test/r?b=2&a=1")).toBe(normalizeSource("https://x.test/r?a=1&b=2&utm_campaign=z"));
49
+ });
50
+
35
51
  it("normalises backslashes and trailing slashes on paths", () => {
36
52
  expect(normalizeSource("docs\\Adr\\README.md")).toBe("docs/adr/readme.md");
37
53
  expect(normalizeSource("lib/agents/")).toBe("lib/agents");
@@ -137,6 +153,18 @@ describe("parseCitationVerdict", () => {
137
153
  expect(out).toEqual([{ text: "a", marker: null, link: null, verified: false, reason: "no link", impact: "med" }]);
138
154
  });
139
155
 
156
+ it("finds the verdict when the model wraps it in prose that also contains braces", () => {
157
+ const verdict = '{"claims":[{"text":"rate is 5%","marker":1,"verified":true,"reason":"in [1]","impact":"high"}]}';
158
+ const expected = [{ text: "rate is 5%", marker: 1, link: null, verified: true, reason: "in [1]", impact: "high" }];
159
+ expect(parseCitationVerdict(`Checked the {draft} reply. ${verdict}`)).toEqual(expected);
160
+ expect(parseCitationVerdict(`${verdict}\nNote: ignored the {code} block.`)).toEqual(expected);
161
+ });
162
+
163
+ it("keeps braces that belong to a claim string", () => {
164
+ const out = parseCitationVerdict('{"claims":[{"text":"set {a: 1} in config","verified":false,"reason":"} unmatched"}]}');
165
+ expect(out?.[0].text).toBe("set {a: 1} in config");
166
+ expect(out?.[0].reason).toBe("} unmatched");
167
+ });
140
168
  it("returns null on malformed JSON", () => {
141
169
  expect(parseCitationVerdict("{not json")).toBeNull();
142
170
  });
@@ -299,6 +327,16 @@ describe("mergeDeclaredReferences", () => {
299
327
  ]);
300
328
  });
301
329
 
330
+ it("keeps two declared emails that differ only by their permalink id", () => {
331
+ const out = mergeDeclaredReferences([], [
332
+ { label: "Invoice mail", href: "https://mail.google.com/mail/u/0/#all/18c2f0a1b2c3d4e5" },
333
+ { label: "Contract mail", href: "https://mail.google.com/mail/u/0/#all/18c2f0a1b2c3ffff" },
334
+ { label: "Board invite", href: "https://outlook.office365.com/owa/?ItemID=AAMk%2BA%3D&exvsurl=1" },
335
+ { label: "Offsite invite", href: "https://outlook.office365.com/owa/?ItemID=AAMk%2BB%3D&exvsurl=1" },
336
+ ]);
337
+ expect(out.map((e) => e.label)).toEqual(["Invoice mail", "Contract mail", "Board invite", "Offsite invite"]);
338
+ });
339
+
302
340
  it("returns a copy of the manifest unchanged when there are no declared refs", () => {
303
341
  const manifest = [{ n: 1, label: "x", href: "x" }];
304
342
  expect(mergeDeclaredReferences(manifest, [])).toEqual(manifest);