@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
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { NextRequest, NextResponse } from "next/server";
10
+ import { z } from "zod";
10
11
  import {
11
12
  deleteThread,
12
13
  getMessagesAfter,
@@ -23,6 +24,7 @@ import { parseStoredTopics } from "@/lib/agents/conversation-summary";
23
24
  import { getThreadActivity } from "@/lib/agents/thread-activity";
24
25
  import { getCheckpointer } from "@/lib/agents/checkpointer";
25
26
  import { errorMessage } from "@/lib/utils/error";
27
+ import { errorResponse, noStoreJson } from "@/lib/api/responses";
26
28
 
27
29
  type Params = { params: Promise<{ thread_id: string }> };
28
30
 
@@ -41,6 +43,9 @@ export async function GET(req: NextRequest, { params }: Params) {
41
43
  if (!thread) return NextResponse.json({ error: "Thread not found" }, { status: 404 });
42
44
 
43
45
  const url = new URL(req.url);
46
+ const view = z.enum(["messages", "metadata"]).default("messages")
47
+ .safeParse(url.searchParams.get("view") ?? undefined);
48
+ if (!view.success) return errorResponse("Invalid thread view", 400);
44
49
  const limit = Math.max(1, Math.min(200, Number(url.searchParams.get("limit")) || DEFAULT_PAGE));
45
50
  // Cursors are the message `seq` (SQLite rowid) — a monotonic order key,
46
51
  // not a timestamp — so pagination can't skip/duplicate rows on a
@@ -48,28 +53,6 @@ export async function GET(req: NextRequest, { params }: Params) {
48
53
  const before = parseSeqParam(url.searchParams.get("before"));
49
54
  const after = parseSeqParam(url.searchParams.get("after"));
50
55
 
51
- // `after` is a forward-fetch shortcut: caller (typically ChatView's
52
- // run-completion handler) already has the recent messages and just wants
53
- // the freshly-persisted ones. has_more is fixed to false because the
54
- // forward window is not paginated — caller already has everything older.
55
- let messages: MessageRow[];
56
- let has_more: boolean;
57
- if (after !== undefined) {
58
- messages = getMessagesAfter(thread_id, after, limit);
59
- has_more = false;
60
- } else {
61
- const page = getMessagesPage(thread_id, limit, before);
62
- messages = page.messages;
63
- has_more = page.has_more;
64
- }
65
-
66
- // Attach per-assistant-turn usage so the chat UI can render a context
67
- // utilisation bar without an extra round-trip per message. User turns
68
- // and legacy assistant rows (pre-message_usage table) simply omit the
69
- // field.
70
- const assistantIds = messages.filter((m) => m.role === "assistant").map((m) => m.msg_id);
71
- const usageById = getMessageUsageByIds(assistantIds);
72
-
73
56
  // Resolve the agent's effective context-window size once for the whole
74
57
  // thread. The bar scales against this cap so the visual matches the
75
58
  // budget the agent applies at run time.
@@ -82,7 +65,7 @@ export async function GET(req: NextRequest, { params }: Params) {
82
65
  DEFAULT_CONTEXT_WINDOW_TOKENS,
83
66
  );
84
67
 
85
- return NextResponse.json({
68
+ const metadata = {
86
69
  ...thread,
87
70
  warm_summary_topics: parseStoredTopics(thread.warm_summary_topics),
88
71
  warm_channel_summaries: currentChannelSummaries(thread_id, thread.hot_since_seq),
@@ -91,6 +74,26 @@ export async function GET(req: NextRequest, { params }: Params) {
91
74
  pending_hot_since_seq: pendingCompactionBoundarySeq(thread_id),
92
75
  manual_compaction_pending: isManualCompactionRunning(thread_id),
93
76
  activity: getThreadActivity(thread_id),
77
+ };
78
+ if (view.data === "metadata") return noStoreJson(metadata);
79
+
80
+ // `after` is a forward-fetch shortcut: caller already has the recent
81
+ // messages and just wants the freshly persisted ones.
82
+ let messages: MessageRow[];
83
+ let has_more: boolean;
84
+ if (after !== undefined) {
85
+ messages = getMessagesAfter(thread_id, after, limit);
86
+ has_more = false;
87
+ } else {
88
+ const page = getMessagesPage(thread_id, limit, before);
89
+ messages = page.messages;
90
+ has_more = page.has_more;
91
+ }
92
+ const assistantIds = messages.filter((m) => m.role === "assistant").map((m) => m.msg_id);
93
+ const usageById = getMessageUsageByIds(assistantIds);
94
+
95
+ return noStoreJson({
96
+ ...metadata,
94
97
  // No server-side filtering: clients receive every message with its
95
98
  // `category` tag and apply the chat-panel filter toolbar on the
96
99
  // render side. Keeping the raw transcript over the wire means audit
@@ -189,7 +189,6 @@ export async function POST(req: NextRequest, { params }: Params) {
189
189
  _pinned_model_config_name: pinnedModelConfigName,
190
190
  _onCompacting: () => broadcast(active, { type: "status", data: { phase: "compacting", label: "Compacting earlier messages…" } }),
191
191
  _user_message_persisted: true,
192
- _message_count_before_user: admittedTurn.messageCountBefore,
193
192
  _idle_ms_before_user: admittedTurn.idleMsBefore,
194
193
  });
195
194
  prepDurationMs = Date.now() - prepStartedAt;
package/app/globals.css CHANGED
@@ -115,6 +115,23 @@
115
115
  }
116
116
  }
117
117
 
118
+ html[data-accent-color="blue"] {
119
+ --color-accent: #2563eb;
120
+ --color-accent-hover: #1d4ed8;
121
+ }
122
+ html[data-accent-color="teal"] {
123
+ --color-accent: #0f766e;
124
+ --color-accent-hover: #115e59;
125
+ }
126
+ html[data-accent-color="violet"] {
127
+ --color-accent: #6d28d9;
128
+ --color-accent-hover: #5b21b6;
129
+ }
130
+ html[data-accent-color="rose"] {
131
+ --color-accent: #be123c;
132
+ --color-accent-hover: #9f1239;
133
+ }
134
+
118
135
  /* Tailwind v4 changed the default border colour from gray-200 to
119
136
  * currentColor. Restore the v3 behaviour so existing `border` shorthand
120
137
  * (without an explicit colour) keeps rendering as the themed divider. */
@@ -195,31 +212,27 @@ body {
195
212
  * pronounced top sheen. *
196
213
  * ---------------------------------------------------------------------- */
197
214
  .glass {
198
- background-color: rgba(24, 24, 27, 0.32);
199
- background-image: linear-gradient(
200
- 180deg,
201
- rgba(255, 255, 255, 0.07) 0%,
202
- transparent 65%
203
- );
215
+ background-color: rgba(24, 24, 27, 0.38);
216
+ background-image:
217
+ linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.035) 34%, transparent 68%),
218
+ linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, transparent 68%);
204
219
  -webkit-backdrop-filter: blur(24px) saturate(2);
205
220
  backdrop-filter: blur(24px) saturate(2);
206
221
  box-shadow:
207
- inset 0 1px 0 0 rgba(255, 255, 255, 0.18),
208
- inset 0 -1px 0 0 rgba(255, 255, 255, 0.04),
209
- inset 0 0 0 1px rgba(255, 255, 255, 0.05);
222
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.25),
223
+ inset 0 -1px 0 0 rgba(255, 255, 255, 0.06),
224
+ inset 0 0 0 1px rgba(255, 255, 255, 0.07);
210
225
  }
211
226
  .glass-elevated {
212
- background-color: rgba(24, 24, 27, 0.42);
213
- background-image: linear-gradient(
214
- 180deg,
215
- rgba(255, 255, 255, 0.09) 0%,
216
- transparent 60%
217
- );
227
+ background-color: rgba(24, 24, 27, 0.5);
228
+ background-image:
229
+ linear-gradient(135deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.04) 34%, transparent 68%),
230
+ linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, transparent 65%);
218
231
  -webkit-backdrop-filter: blur(32px) saturate(2.1);
219
232
  backdrop-filter: blur(32px) saturate(2.1);
220
233
  box-shadow:
221
- inset 0 1px 0 0 rgba(255, 255, 255, 0.22),
222
- inset 0 0 0 1px rgba(255, 255, 255, 0.06),
234
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.3),
235
+ inset 0 0 0 1px rgba(255, 255, 255, 0.08),
223
236
  0 24px 60px -12px rgba(0, 0, 0, 0.5);
224
237
  }
225
238
  .glass-bubble {
@@ -253,15 +266,19 @@ body {
253
266
  * inner edge flips to a hairline of slate so the glass reads as raised
254
267
  * rather than dissolved into the page. */
255
268
  [data-theme="light"] .glass {
256
- background-color: rgba(255, 255, 255, 0.55);
257
- background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.4) 0%, transparent 65%);
269
+ background-color: rgba(255, 255, 255, 0.48);
270
+ background-image:
271
+ linear-gradient(135deg, rgba(255, 255, 255, 0.62) 0%, rgba(255, 255, 255, 0.18) 34%, transparent 68%),
272
+ linear-gradient(180deg, rgba(255, 255, 255, 0.28) 0%, transparent 65%);
258
273
  box-shadow:
259
- inset 0 1px 0 0 rgba(255, 255, 255, 0.85),
260
- inset 0 0 0 1px rgba(15, 23, 42, 0.07);
274
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.98),
275
+ inset 0 0 0 1px rgba(15, 23, 42, 0.09);
261
276
  }
262
277
  [data-theme="light"] .glass-elevated {
263
- background-color: rgba(255, 255, 255, 0.65);
264
- background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, transparent 60%);
278
+ background-color: rgba(255, 255, 255, 0.58);
279
+ background-image:
280
+ linear-gradient(135deg, rgba(255, 255, 255, 0.7) 0%, rgba(255, 255, 255, 0.22) 34%, transparent 68%),
281
+ linear-gradient(180deg, rgba(255, 255, 255, 0.36) 0%, transparent 60%);
265
282
  box-shadow:
266
283
  inset 0 1px 0 0 rgba(255, 255, 255, 0.9),
267
284
  inset 0 0 0 1px rgba(15, 23, 42, 0.07),
@@ -276,15 +293,19 @@ body {
276
293
  }
277
294
  @media (prefers-color-scheme: light) {
278
295
  [data-theme="system"] .glass {
279
- background-color: rgba(255, 255, 255, 0.55);
280
- background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.4) 0%, transparent 65%);
296
+ background-color: rgba(255, 255, 255, 0.48);
297
+ background-image:
298
+ linear-gradient(135deg, rgba(255, 255, 255, 0.62) 0%, rgba(255, 255, 255, 0.18) 34%, transparent 68%),
299
+ linear-gradient(180deg, rgba(255, 255, 255, 0.28) 0%, transparent 65%);
281
300
  box-shadow:
282
- inset 0 1px 0 0 rgba(255, 255, 255, 0.85),
283
- inset 0 0 0 1px rgba(15, 23, 42, 0.07);
301
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.98),
302
+ inset 0 0 0 1px rgba(15, 23, 42, 0.09);
284
303
  }
285
304
  [data-theme="system"] .glass-elevated {
286
- background-color: rgba(255, 255, 255, 0.65);
287
- background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, transparent 60%);
305
+ background-color: rgba(255, 255, 255, 0.58);
306
+ background-image:
307
+ linear-gradient(135deg, rgba(255, 255, 255, 0.7) 0%, rgba(255, 255, 255, 0.22) 34%, transparent 68%),
308
+ linear-gradient(180deg, rgba(255, 255, 255, 0.36) 0%, transparent 60%);
288
309
  box-shadow:
289
310
  inset 0 1px 0 0 rgba(255, 255, 255, 0.9),
290
311
  inset 0 0 0 1px rgba(15, 23, 42, 0.07),
@@ -310,6 +331,75 @@ body {
310
331
  }
311
332
  }
312
333
 
334
+ [data-surface-style="calm"] body {
335
+ background-image: none;
336
+ }
337
+ [data-surface-style="calm"] .dashboard-atmosphere {
338
+ display: none;
339
+ }
340
+ [data-surface-style="calm"] .glass,
341
+ [data-surface-style="calm"] .glass-elevated {
342
+ -webkit-backdrop-filter: blur(14px) saturate(1.2);
343
+ backdrop-filter: blur(14px) saturate(1.2);
344
+ background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.07) 0%, transparent 65%);
345
+ }
346
+ [data-surface-style="calm"] .glass-bubble,
347
+ [data-surface-style="calm"] .glass-bubble-accent {
348
+ -webkit-backdrop-filter: none;
349
+ backdrop-filter: none;
350
+ background-image: none;
351
+ }
352
+ [data-surface-style="calm"] .glass,
353
+ [data-surface-style="calm"] .glass-elevated {
354
+ background-color: color-mix(in srgb, var(--color-surface-2) 78%, transparent);
355
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-border) 70%, transparent);
356
+ }
357
+ [data-surface-style="calm"] .glass-elevated {
358
+ box-shadow:
359
+ inset 0 0 0 1px color-mix(in srgb, var(--color-border) 70%, transparent),
360
+ 0 12px 32px -16px rgba(0, 0, 0, 0.35);
361
+ }
362
+ [data-surface-style="calm"] .glass-bubble {
363
+ background-color: var(--color-surface-3);
364
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-border) 50%, transparent);
365
+ }
366
+ [data-surface-style="calm"] .glass-bubble-accent {
367
+ background-color: color-mix(in srgb, var(--color-accent) 82%, var(--color-surface-2));
368
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-accent) 45%, transparent);
369
+ }
370
+
371
+ [data-surface-style="liquid"] .glass,
372
+ [data-surface-style="liquid"] .glass-elevated {
373
+ background-color: color-mix(in srgb, var(--color-surface-2) 30%, transparent);
374
+ background-image:
375
+ radial-gradient(ellipse 120% 150% at 0% 0%, rgba(255, 255, 255, 0.2), transparent 54%),
376
+ linear-gradient(115deg, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0.035) 24%, transparent 50%, color-mix(in srgb, var(--color-accent) 8%, transparent) 100%),
377
+ linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, transparent 68%);
378
+ -webkit-backdrop-filter: blur(22px) saturate(1.65) contrast(1.06);
379
+ backdrop-filter: blur(22px) saturate(1.65) contrast(1.06);
380
+ box-shadow:
381
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.48),
382
+ inset 0 -1px 0 0 rgba(0, 0, 0, 0.08),
383
+ inset 0 0 0 1px rgba(255, 255, 255, 0.16),
384
+ 0 8px 24px -18px rgba(0, 0, 0, 0.45);
385
+ }
386
+ [data-surface-style="liquid"] .glass-elevated {
387
+ background-color: color-mix(in srgb, var(--color-surface-2) 38%, transparent);
388
+ box-shadow:
389
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.56),
390
+ inset 0 0 0 1px rgba(255, 255, 255, 0.2),
391
+ 0 24px 60px -12px rgba(0, 0, 0, 0.42),
392
+ 0 0 30px -22px color-mix(in srgb, var(--color-accent) 40%, transparent);
393
+ }
394
+ @supports not (backdrop-filter: blur(1px)) {
395
+ [data-surface-style="liquid"] .glass,
396
+ [data-surface-style="liquid"] .glass-elevated {
397
+ background-color: var(--color-surface-2);
398
+ background-image: none;
399
+ box-shadow: inset 0 0 0 1px var(--color-border);
400
+ }
401
+ }
402
+
313
403
  /* Disable the blue tap flash on iOS / Android; we use explicit :active styles. */
314
404
  * { -webkit-tap-highlight-color: transparent; }
315
405
 
package/app/layout.tsx CHANGED
@@ -90,6 +90,18 @@ const themeBootstrap = `(() => {
90
90
  var t = localStorage.getItem("jarela-theme");
91
91
  if (t !== "light" && t !== "dark" && t !== "system") t = "system";
92
92
  document.documentElement.setAttribute("data-theme", t);
93
+ var accent = localStorage.getItem("jarela-accent-color");
94
+ if (["blue", "teal", "violet", "rose"].includes(accent)) {
95
+ document.documentElement.setAttribute("data-accent-color", accent);
96
+ } else {
97
+ document.documentElement.removeAttribute("data-accent-color");
98
+ }
99
+ var surface = localStorage.getItem("jarela-surface-style");
100
+ if (surface === "calm" || surface === "ambient" || surface === "liquid") {
101
+ document.documentElement.setAttribute("data-surface-style", surface);
102
+ } else {
103
+ document.documentElement.removeAttribute("data-surface-style");
104
+ }
93
105
  // Collapse any media-scoped theme-color metas Next emits to a single tag
94
106
  // so the PWA window chrome matches the user's explicit choice (not just
95
107
  // prefers-color-scheme). ThemeContext keeps this in sync on change.
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
  import { useEffect, useRef, useState } from "react";
3
- import { Plus, Trash2 } from "lucide-react";
3
+ import { Bot, Plus, Trash2 } from "lucide-react";
4
4
  import type { AgentConfig, AgentConfigIn } from "@/api/types";
5
5
  import { api } from "@/api/client";
6
6
  import { useAgents } from "@/hooks/useAgents";
7
7
  import { useModels } from "@/hooks/useModels";
8
8
  import { useDeepLinkScroll } from "@/hooks/useDeepLinkScroll";
9
- import { Button } from "@/components/ui/Button";
9
+ import { HeaderAction, PanelHeader } from "@/components/ui/PanelHeader";
10
10
  import { AgentEditor } from "./AgentEditor";
11
11
  import { useAppContext } from "@/contexts/AppContext";
12
12
 
@@ -100,17 +100,9 @@ export function AgentsPanel() {
100
100
  return (
101
101
  <div className="flex flex-col h-full min-h-0">
102
102
  <>
103
- {/* Header */}
104
- <div className="flex items-center justify-between px-3 pt-3 pb-2 shrink-0">
105
- <span className="text-[11px] text-fg-faint font-medium uppercase tracking-wide">Agents</span>
106
- <Button
107
- onClick={() => setEditing("new")}
108
- size="sm"
109
- icon={<Plus size={11} />}
110
- >
111
- New
112
- </Button>
113
- </div>
103
+ <PanelHeader icon={<Bot size={14} />} title="Agents">
104
+ <HeaderAction onClick={() => setEditing("new")} icon={<Plus size={14} />} label="New" />
105
+ </PanelHeader>
114
106
 
115
107
  {/* List */}
116
108
  <div ref={containerRef} className="flex-1 overflow-y-auto px-2 pb-2">
@@ -17,6 +17,7 @@ import { useChatErrorReporting } from "./useChatErrorReporting";
17
17
  import { useChatQueue, type ChatQueueApi } from "./useChatQueue";
18
18
  import { useThreadCrossDeviceSync } from "./useThreadCrossDeviceSync";
19
19
  import { useThreadData } from "./useThreadData";
20
+ import { useThreadMetadataPolling } from "./useThreadMetadataPolling";
20
21
  import { useThreadActivity } from "./useThreadActivity";
21
22
  import { useUserProfileAndAgent } from "./useUserProfileAndAgent";
22
23
  import { composerPlaceholder, finalizeRunFromServer } from "./chat-run-utils";
@@ -109,21 +110,11 @@ export function ChatView({ threadId, agentId, sessionLoading, sessionError, onMe
109
110
  const currentThreadIdRef = useRef(threadId);
110
111
  currentThreadIdRef.current = threadId;
111
112
 
112
- // Auto compaction starts server-side during a run; poll so the banner shows live.
113
- useEffect(() => {
114
- if (!threadId || !sse.streaming) return;
115
- let cancelled = false;
116
- const timer = window.setInterval(() => {
117
- void api.threads.get(threadId, { limit: 1 }).then((d) => {
118
- if (!cancelled) applyThreadMeta(thread.metaApplier, d);
119
- }).catch(() => { /* best-effort */ });
120
- }, 2000);
121
- return () => {
122
- cancelled = true;
123
- window.clearInterval(timer);
124
- };
125
- // eslint-disable-next-line react-hooks/exhaustive-deps
126
- }, [threadId, sse.streaming]);
113
+ useThreadMetadataPolling(
114
+ threadId,
115
+ sse.streaming || thread.warmSummaryPending || thread.compactionPending || thread.manualCompactionPending,
116
+ thread.metaApplier,
117
+ );
127
118
 
128
119
  useThreadCrossDeviceSync({
129
120
  threadId,
@@ -262,9 +253,9 @@ export function ChatView({ threadId, agentId, sessionLoading, sessionError, onMe
262
253
  thread.metaApplier.setWarmSummaryTopics?.(parseWarmSummaryTopics(result.warm_summary_topics));
263
254
  // The compact response carries only the chat recap; channel recaps
264
255
  // (bridge, watcher, scheduled task) come with the thread metadata.
265
- void api.threads.get(compactedThreadId, { limit: 1 })
256
+ void api.threads.metadata(compactedThreadId)
266
257
  .then((d) => { if (stillCurrent()) applyThreadMeta(thread.metaApplier, d); })
267
- .catch(() => { /* best-effort */ });
258
+ .catch((err) => console.error("compaction metadata refresh failed", err));
268
259
  }
269
260
  notifyCompaction(result);
270
261
  } catch (err) {
@@ -296,7 +287,7 @@ export function ChatView({ threadId, agentId, sessionLoading, sessionError, onMe
296
287
  });
297
288
 
298
289
  return (
299
- <div className="flex flex-col h-full">
290
+ <div className="flex h-full min-h-0 w-full min-w-0 flex-col">
300
291
  <MessageList
301
292
  threadId={threadId}
302
293
  messages={thread.messages}
@@ -366,7 +366,7 @@ export function InputBar({ attachments, onAttachmentsChange, onSubmit, onQueue,
366
366
  )}
367
367
 
368
368
  <textarea
369
- className="flex-1 resize-none bg-surface-3/60 text-fg text-sm rounded-xl px-3 py-2 border border-border/60 focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent/40 placeholder:text-fg-faint max-h-[84px] min-h-[44px] transition-colors"
369
+ className="flex-1 min-w-0 resize-none bg-surface-3/60 text-fg text-sm rounded-xl px-3 py-2 border border-border/60 focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent/40 placeholder:text-fg-faint max-h-[84px] min-h-[44px] transition-colors"
370
370
  ref={textareaRef}
371
371
  placeholder={placeholder ?? "Message…"}
372
372
  rows={1}
@@ -849,6 +849,18 @@ function BridgeMessageCard({ ctx, activity }: {
849
849
  {activity.occurrence_count > 1 ? ` · ${activity.occurrence_count} checks` : ""}
850
850
  </span>
851
851
  ) : null}
852
+ {ctx.chatLink ? (
853
+ <a
854
+ href={ctx.chatLink}
855
+ target="_blank"
856
+ rel="noopener noreferrer"
857
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-white/15 hover:bg-white/25 text-[9.5px] shrink-0"
858
+ title={`Open this chat (${ctx.chatLink})`}
859
+ >
860
+ <ExternalLink size={9} />
861
+ open chat
862
+ </a>
863
+ ) : null}
852
864
  </>
853
865
  );
854
866
  const sections: Array<{ label: string; content: ReactNode; defaultOpen?: boolean; hint?: string }> = [
@@ -2028,7 +2040,7 @@ export const MessageBubble = memo(function MessageBubble({ message, attachedActi
2028
2040
  )}
2029
2041
  </div>
2030
2042
 
2031
- <div className={`flex flex-col min-w-0 ${containsImageAttachment ? "max-w-[calc(100%-2.25rem)] sm:max-w-[92%] md:max-w-[86%]" : "max-w-[88%] sm:max-w-[75%]"} ${isUser ? "items-end" : "items-start"}`}>
2043
+ <div className={`flex flex-col min-w-0 ${!isUser ? "flex-1 items-start" : containsImageAttachment ? "max-w-[calc(100%-2.25rem)] sm:max-w-[92%] md:max-w-[86%] items-end" : "max-w-[calc(100%-2.25rem)] sm:max-w-[75%] items-end"}`}>
2032
2044
  {(timeLabel || messageId) && (
2033
2045
  <div className={`flex items-center gap-1 mb-0.5 px-1 opacity-0 group-hover:opacity-100 pointer-coarse:opacity-100 transition-opacity ${isUser ? "flex-row-reverse" : ""}`}>
2034
2046
  {timeLabel && (
@@ -4,14 +4,16 @@ import { render, screen, fireEvent, within, waitFor } from "@testing-library/rea
4
4
  import { MessageList } from "./MessageList";
5
5
  import type { Message } from "@/api/types";
6
6
 
7
+ const renderCounts = vi.hoisted(() => new Map<string, number>());
7
8
  vi.mock("./MessageBubble", () => ({
8
9
  MessageBubble: ({ message, attachedActivity, showAvatar }: {
9
- message: Pick<Message, "content">;
10
+ message: Pick<Message, "id" | "content">;
10
11
  attachedActivity?: { source_kind: string };
11
12
  showAvatar?: boolean;
12
- }) => (
13
- <div data-activity-kind={attachedActivity?.source_kind} data-show-avatar={String(showAvatar)}>{message.content}</div>
14
- ),
13
+ }) => {
14
+ renderCounts.set(message.id, (renderCounts.get(message.id) ?? 0) + 1);
15
+ return <div data-activity-kind={attachedActivity?.source_kind} data-show-avatar={String(showAvatar)}>{message.content}</div>;
16
+ },
15
17
  }));
16
18
 
17
19
  vi.mock("./ToolList", () => ({
@@ -65,6 +67,45 @@ function installPointerCapture(button: HTMLButtonElement) {
65
67
  }
66
68
 
67
69
  describe("MessageList conversation focus", () => {
70
+ it("does not re-render historical rows as streaming tokens or thinking arrive", () => {
71
+ renderCounts.clear();
72
+ const messages = [
73
+ mkMessage("history1", "user", "earlier prompt", "2026-10-18T08:00:00.000Z"),
74
+ mkMessage("history2", "assistant", "earlier answer", "2026-10-18T08:00:01.000Z"),
75
+ ];
76
+ const { rerender } = render(
77
+ <MessageList messages={messages} streaming streamingContent="A" thinkingContent="thinking" />,
78
+ );
79
+ const firstCounts = messages.map((m) => renderCounts.get(m.id));
80
+ rerender(<MessageList messages={messages} streaming streamingContent="A longer reply" thinkingContent="more thinking" />);
81
+ expect(messages.map((m) => renderCounts.get(m.id))).toEqual(firstCounts);
82
+ expect(screen.getByText("A longer reply")).toBeTruthy();
83
+ });
84
+
85
+ it("updates history when a persisted message changes", () => {
86
+ const messages = [mkMessage("history3", "assistant", "before edit", "2026-10-18T08:00:00.000Z")];
87
+ const { rerender } = render(<MessageList messages={messages} />);
88
+ rerender(<MessageList messages={[{ ...messages[0], content: "after edit" }]} />);
89
+ expect(screen.getByText("after edit")).toBeTruthy();
90
+ expect(screen.queryByText("before edit")).toBeNull();
91
+ });
92
+
93
+ it("preserves historical message nodes when the context boundary moves", () => {
94
+ const messages = [
95
+ mkMessage("history4", "user", "first message", "2026-10-18T08:00:00.000Z"),
96
+ mkMessage("history5", "assistant", "second message", "2026-10-18T08:00:01.000Z"),
97
+ ];
98
+ const { container, rerender } = render(<MessageList messages={messages} hotSinceSeq={4} />);
99
+ const first = container.querySelector('[data-message-id="history4"]');
100
+ const second = container.querySelector('[data-message-id="history5"]');
101
+ rerender(<MessageList messages={messages} hotSinceSeq={5} />);
102
+ expect(container.querySelector('[data-message-id="history4"]')).toBe(first);
103
+ expect(container.querySelector('[data-message-id="history5"]')).toBe(second);
104
+ const boundary = container.querySelector("[data-focus-boundary='1']");
105
+ expect(first!.compareDocumentPosition(boundary!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
106
+ expect(boundary!.compareDocumentPosition(second!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
107
+ });
108
+
68
109
  it.each([
69
110
  ["bridge", "bridge-1:chat-1", "user", "bridge", { bridge_conversation: { key: "bridge-1:chat-1" } }],
70
111
  ["scheduled_task", "task-1", "assistant", "scheduled_task", undefined],