@swarmclawai/swarmclaw 0.8.4 → 0.8.7

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 (394) hide show
  1. package/README.md +9 -9
  2. package/bin/swarmclaw.js +5 -1
  3. package/bin/worker-cmd.js +73 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +17 -7
  6. package/src/app/api/agents/route.ts +21 -8
  7. package/src/app/api/approvals/route.test.ts +6 -6
  8. package/src/app/api/approvals/route.ts +2 -1
  9. package/src/app/api/auth/route.ts +2 -3
  10. package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
  11. package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
  12. package/src/app/api/chatrooms/[id]/route.ts +7 -6
  13. package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
  14. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  15. package/src/app/api/chats/[id]/clear/route.ts +9 -9
  16. package/src/app/api/chats/[id]/devserver/route.ts +2 -1
  17. package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
  18. package/src/app/api/chats/[id]/fork/route.ts +3 -5
  19. package/src/app/api/chats/[id]/restore/route.ts +6 -7
  20. package/src/app/api/chats/[id]/retry/route.ts +3 -4
  21. package/src/app/api/chats/[id]/route.ts +61 -62
  22. package/src/app/api/chats/route.ts +7 -1
  23. package/src/app/api/connectors/[id]/route.ts +7 -8
  24. package/src/app/api/connectors/route.ts +5 -4
  25. package/src/app/api/eval/run/route.ts +2 -1
  26. package/src/app/api/eval/suite/route.ts +2 -1
  27. package/src/app/api/external-agents/route.test.ts +1 -1
  28. package/src/app/api/external-agents/route.ts +2 -2
  29. package/src/app/api/files/serve/route.ts +1 -1
  30. package/src/app/api/gateways/[id]/route.ts +7 -5
  31. package/src/app/api/gateways/route.ts +1 -1
  32. package/src/app/api/knowledge/upload/route.ts +1 -1
  33. package/src/app/api/logs/route.ts +5 -7
  34. package/src/app/api/memory-images/[filename]/route.ts +2 -3
  35. package/src/app/api/openclaw/agent-files/route.ts +4 -3
  36. package/src/app/api/openclaw/approvals/route.ts +3 -4
  37. package/src/app/api/openclaw/config-sync/route.ts +3 -2
  38. package/src/app/api/openclaw/cron/route.ts +3 -2
  39. package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
  40. package/src/app/api/openclaw/exec-config/route.ts +3 -2
  41. package/src/app/api/openclaw/gateway/route.ts +5 -4
  42. package/src/app/api/openclaw/history/route.ts +3 -2
  43. package/src/app/api/openclaw/media/route.ts +2 -1
  44. package/src/app/api/openclaw/permissions/route.ts +3 -2
  45. package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
  46. package/src/app/api/openclaw/skills/install/route.ts +2 -1
  47. package/src/app/api/openclaw/skills/remove/route.ts +2 -1
  48. package/src/app/api/openclaw/skills/route.ts +3 -2
  49. package/src/app/api/orchestrator/run/route.ts +5 -14
  50. package/src/app/api/perf/route.ts +43 -0
  51. package/src/app/api/plugins/dependencies/route.ts +2 -1
  52. package/src/app/api/plugins/install/route.ts +2 -1
  53. package/src/app/api/plugins/marketplace/route.ts +3 -2
  54. package/src/app/api/plugins/settings/route.ts +2 -1
  55. package/src/app/api/preview-server/route.ts +11 -10
  56. package/src/app/api/projects/[id]/route.ts +1 -1
  57. package/src/app/api/schedules/[id]/route.test.ts +128 -0
  58. package/src/app/api/schedules/[id]/route.ts +43 -43
  59. package/src/app/api/schedules/[id]/run/route.ts +11 -62
  60. package/src/app/api/schedules/route.ts +21 -87
  61. package/src/app/api/settings/route.ts +2 -0
  62. package/src/app/api/setup/doctor/route.ts +9 -8
  63. package/src/app/api/tasks/[id]/approve/route.ts +33 -30
  64. package/src/app/api/tasks/[id]/route.ts +12 -35
  65. package/src/app/api/tasks/import/github/route.ts +2 -1
  66. package/src/app/api/tasks/route.ts +79 -91
  67. package/src/app/api/wallets/[id]/approve/route.ts +2 -1
  68. package/src/app/api/wallets/[id]/route.ts +13 -19
  69. package/src/app/api/wallets/[id]/send/route.ts +2 -1
  70. package/src/app/api/wallets/route.ts +2 -1
  71. package/src/app/api/webhooks/[id]/route.ts +2 -1
  72. package/src/app/api/webhooks/route.test.ts +3 -1
  73. package/src/app/page.tsx +23 -331
  74. package/src/cli/index.js +19 -0
  75. package/src/cli/index.ts +38 -7
  76. package/src/cli/spec.js +9 -0
  77. package/src/components/activity/activity-feed.tsx +7 -4
  78. package/src/components/agents/agent-card.tsx +32 -6
  79. package/src/components/agents/agent-chat-list.tsx +55 -22
  80. package/src/components/agents/agent-files-editor.tsx +3 -2
  81. package/src/components/agents/agent-sheet.tsx +123 -22
  82. package/src/components/agents/inspector-panel.tsx +1 -1
  83. package/src/components/agents/openclaw-skills-panel.tsx +2 -1
  84. package/src/components/agents/trash-list.tsx +1 -1
  85. package/src/components/auth/access-key-gate.tsx +8 -2
  86. package/src/components/auth/setup-wizard.tsx +10 -9
  87. package/src/components/auth/user-picker.tsx +3 -2
  88. package/src/components/chat/chat-area.tsx +20 -1
  89. package/src/components/chat/chat-card.tsx +18 -3
  90. package/src/components/chat/chat-header.tsx +24 -4
  91. package/src/components/chat/chat-list.tsx +2 -11
  92. package/src/components/chat/heartbeat-history-panel.tsx +2 -1
  93. package/src/components/chat/message-bubble.tsx +45 -6
  94. package/src/components/chat/message-list.tsx +280 -145
  95. package/src/components/chat/streaming-bubble.tsx +217 -60
  96. package/src/components/chat/swarm-panel.test.ts +274 -0
  97. package/src/components/chat/swarm-panel.tsx +410 -0
  98. package/src/components/chat/swarm-status-card.tsx +346 -0
  99. package/src/components/chat/tool-call-bubble.tsx +48 -23
  100. package/src/components/chatrooms/chatroom-list.tsx +8 -5
  101. package/src/components/chatrooms/chatroom-message.tsx +10 -7
  102. package/src/components/chatrooms/chatroom-view.tsx +12 -9
  103. package/src/components/connectors/connector-health.tsx +6 -4
  104. package/src/components/connectors/connector-list.tsx +16 -11
  105. package/src/components/connectors/connector-sheet.tsx +12 -6
  106. package/src/components/home/home-view.tsx +38 -24
  107. package/src/components/input/chat-input.tsx +10 -1
  108. package/src/components/layout/app-layout.tsx +2 -38
  109. package/src/components/layout/sheet-layer.tsx +50 -0
  110. package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
  111. package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
  112. package/src/components/plugins/plugin-list.tsx +8 -4
  113. package/src/components/plugins/plugin-sheet.tsx +2 -1
  114. package/src/components/providers/provider-list.tsx +3 -2
  115. package/src/components/providers/provider-sheet.tsx +2 -1
  116. package/src/components/runs/run-list.tsx +11 -7
  117. package/src/components/schedules/schedule-card.tsx +5 -3
  118. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  119. package/src/components/shared/attachment-chip.tsx +19 -3
  120. package/src/components/shared/notification-center.tsx +6 -3
  121. package/src/components/shared/settings/plugin-manager.tsx +3 -2
  122. package/src/components/shared/settings/section-embedding.tsx +2 -1
  123. package/src/components/shared/settings/section-orchestrator.tsx +2 -1
  124. package/src/components/shared/settings/section-user-preferences.tsx +107 -0
  125. package/src/components/shared/settings/settings-page.tsx +13 -9
  126. package/src/components/skills/clawhub-browser.tsx +15 -4
  127. package/src/components/skills/skill-list.tsx +15 -4
  128. package/src/components/tasks/approvals-panel.tsx +2 -1
  129. package/src/components/tasks/task-board.tsx +35 -37
  130. package/src/components/tasks/task-sheet.tsx +4 -3
  131. package/src/components/ui/full-screen-loader.tsx +164 -0
  132. package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
  133. package/src/components/wallets/wallet-panel.tsx +6 -5
  134. package/src/components/wallets/wallet-section.tsx +3 -2
  135. package/src/components/webhooks/webhook-list.tsx +4 -5
  136. package/src/components/webhooks/webhook-sheet.tsx +6 -6
  137. package/src/hooks/use-app-bootstrap.ts +202 -0
  138. package/src/hooks/use-mounted-ref.ts +14 -0
  139. package/src/hooks/use-now.ts +31 -0
  140. package/src/hooks/use-openclaw-gateway.ts +2 -1
  141. package/src/instrumentation.ts +20 -8
  142. package/src/lib/agent-default-tools.test.ts +52 -0
  143. package/src/lib/agent-default-tools.ts +40 -0
  144. package/src/lib/api-client.test.ts +21 -0
  145. package/src/lib/api-client.ts +6 -11
  146. package/src/lib/canvas-content.test.ts +360 -0
  147. package/src/lib/chat-streaming-state.test.ts +49 -2
  148. package/src/lib/chat-streaming-state.ts +26 -10
  149. package/src/lib/fetch-timeout.test.ts +54 -0
  150. package/src/lib/fetch-timeout.ts +60 -3
  151. package/src/lib/live-tool-events.test.ts +77 -0
  152. package/src/lib/live-tool-events.ts +73 -0
  153. package/src/lib/local-observability.test.ts +2 -2
  154. package/src/lib/openclaw-endpoint.test.ts +1 -1
  155. package/src/lib/providers/anthropic.ts +12 -16
  156. package/src/lib/providers/index.ts +4 -2
  157. package/src/lib/providers/ollama.ts +9 -6
  158. package/src/lib/providers/openai.ts +11 -14
  159. package/src/lib/runtime-env.test.ts +8 -8
  160. package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
  161. package/src/lib/schedule-dedupe.test.ts +1 -1
  162. package/src/lib/schedule-dedupe.ts +3 -2
  163. package/src/lib/server/agent-thread-session.test.ts +6 -6
  164. package/src/lib/server/agent-thread-session.ts +6 -9
  165. package/src/lib/server/alert-dispatch.ts +2 -1
  166. package/src/lib/server/api-routes.test.ts +6 -6
  167. package/src/lib/server/approval-connector-notify.test.ts +4 -4
  168. package/src/lib/server/approvals-auto-approve.test.ts +29 -29
  169. package/src/lib/server/approvals.test.ts +317 -0
  170. package/src/lib/server/approvals.ts +5 -4
  171. package/src/lib/server/autonomy-runtime.test.ts +11 -11
  172. package/src/lib/server/browser-state.ts +2 -2
  173. package/src/lib/server/capability-router.test.ts +1 -1
  174. package/src/lib/server/capability-router.ts +3 -2
  175. package/src/lib/server/chat-execution-advanced.test.ts +15 -2
  176. package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
  177. package/src/lib/server/chat-execution-disabled.test.ts +3 -3
  178. package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
  179. package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
  180. package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
  181. package/src/lib/server/chat-execution-tool-events.ts +116 -0
  182. package/src/lib/server/chat-execution-utils.test.ts +479 -0
  183. package/src/lib/server/chat-execution-utils.ts +533 -0
  184. package/src/lib/server/chat-execution.ts +153 -748
  185. package/src/lib/server/chat-streaming-utils.ts +174 -0
  186. package/src/lib/server/chat-turn-tool-routing.ts +310 -0
  187. package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
  188. package/src/lib/server/clawhub-client.ts +2 -1
  189. package/src/lib/server/collection-helpers.test.ts +92 -0
  190. package/src/lib/server/collection-helpers.ts +25 -3
  191. package/src/lib/server/connectors/access.ts +146 -0
  192. package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
  193. package/src/lib/server/connectors/bluebubbles.ts +4 -4
  194. package/src/lib/server/connectors/commands.ts +367 -0
  195. package/src/lib/server/connectors/connector-routing.test.ts +4 -4
  196. package/src/lib/server/connectors/delivery.ts +142 -0
  197. package/src/lib/server/connectors/discord.ts +37 -40
  198. package/src/lib/server/connectors/email.ts +11 -10
  199. package/src/lib/server/connectors/googlechat.ts +4 -4
  200. package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
  201. package/src/lib/server/connectors/ingress-delivery.ts +23 -0
  202. package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
  203. package/src/lib/server/connectors/manager.test.ts +352 -77
  204. package/src/lib/server/connectors/manager.ts +134 -673
  205. package/src/lib/server/connectors/matrix.ts +4 -4
  206. package/src/lib/server/connectors/message-sentinel.ts +7 -0
  207. package/src/lib/server/connectors/openclaw.test.ts +1 -1
  208. package/src/lib/server/connectors/openclaw.ts +8 -10
  209. package/src/lib/server/connectors/outbox.test.ts +192 -0
  210. package/src/lib/server/connectors/outbox.ts +369 -0
  211. package/src/lib/server/connectors/pairing.test.ts +18 -1
  212. package/src/lib/server/connectors/pairing.ts +49 -4
  213. package/src/lib/server/connectors/policy.ts +9 -3
  214. package/src/lib/server/connectors/reconnect-state.ts +71 -0
  215. package/src/lib/server/connectors/response-media.ts +256 -0
  216. package/src/lib/server/connectors/runtime-state.ts +67 -0
  217. package/src/lib/server/connectors/session.test.ts +357 -0
  218. package/src/lib/server/connectors/session.ts +422 -0
  219. package/src/lib/server/connectors/signal.ts +7 -7
  220. package/src/lib/server/connectors/slack.ts +43 -43
  221. package/src/lib/server/connectors/teams.ts +4 -4
  222. package/src/lib/server/connectors/telegram.ts +37 -43
  223. package/src/lib/server/connectors/types.ts +31 -1
  224. package/src/lib/server/connectors/whatsapp.test.ts +108 -0
  225. package/src/lib/server/connectors/whatsapp.ts +106 -34
  226. package/src/lib/server/context-manager.test.ts +409 -0
  227. package/src/lib/server/cost.test.ts +1 -1
  228. package/src/lib/server/daemon-policy.ts +78 -0
  229. package/src/lib/server/daemon-state-connectors.test.ts +167 -0
  230. package/src/lib/server/daemon-state.test.ts +283 -55
  231. package/src/lib/server/daemon-state.ts +106 -109
  232. package/src/lib/server/data-dir.test.ts +5 -5
  233. package/src/lib/server/data-dir.ts +4 -0
  234. package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
  235. package/src/lib/server/delegation-jobs.test.ts +87 -0
  236. package/src/lib/server/delegation-jobs.ts +42 -48
  237. package/src/lib/server/devserver-launch.ts +1 -1
  238. package/src/lib/server/document-utils.ts +7 -9
  239. package/src/lib/server/elevenlabs.ts +2 -1
  240. package/src/lib/server/embeddings.test.ts +105 -0
  241. package/src/lib/server/ethereum.ts +3 -2
  242. package/src/lib/server/eval/agent-regression.ts +3 -2
  243. package/src/lib/server/eval/runner.ts +2 -1
  244. package/src/lib/server/eval/scorer.ts +2 -1
  245. package/src/lib/server/evm-swap.ts +2 -1
  246. package/src/lib/server/gateway/protocol.test.ts +1 -1
  247. package/src/lib/server/guardian.ts +2 -1
  248. package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
  249. package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
  250. package/src/lib/server/heartbeat-service.test.ts +406 -0
  251. package/src/lib/server/heartbeat-service.ts +54 -7
  252. package/src/lib/server/heartbeat-wake.test.ts +19 -0
  253. package/src/lib/server/heartbeat-wake.ts +17 -16
  254. package/src/lib/server/integrity-monitor.test.ts +149 -0
  255. package/src/lib/server/json-utils.ts +22 -0
  256. package/src/lib/server/knowledge-db.test.ts +13 -13
  257. package/src/lib/server/link-understanding.ts +2 -1
  258. package/src/lib/server/llm-response-cache.test.ts +1 -1
  259. package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
  260. package/src/lib/server/main-agent-loop.test.ts +6 -6
  261. package/src/lib/server/main-agent-loop.ts +21 -7
  262. package/src/lib/server/mcp-client.test.ts +1 -1
  263. package/src/lib/server/mcp-conformance.test.ts +1 -1
  264. package/src/lib/server/mcp-conformance.ts +3 -2
  265. package/src/lib/server/memory-consolidation.ts +2 -1
  266. package/src/lib/server/memory-db.test.ts +485 -0
  267. package/src/lib/server/memory-db.ts +39 -26
  268. package/src/lib/server/memory-graph.test.ts +2 -2
  269. package/src/lib/server/memory-policy.test.ts +7 -7
  270. package/src/lib/server/memory-retrieval.test.ts +1 -1
  271. package/src/lib/server/openclaw-config-sync.ts +2 -1
  272. package/src/lib/server/openclaw-deploy.test.ts +1 -1
  273. package/src/lib/server/openclaw-deploy.ts +8 -12
  274. package/src/lib/server/openclaw-exec-config.ts +2 -1
  275. package/src/lib/server/openclaw-gateway.ts +6 -7
  276. package/src/lib/server/openclaw-skills-normalize.ts +2 -1
  277. package/src/lib/server/openclaw-sync.ts +7 -5
  278. package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
  279. package/src/lib/server/orchestrator-lg.ts +199 -327
  280. package/src/lib/server/path-utils.ts +31 -0
  281. package/src/lib/server/perf.ts +161 -0
  282. package/src/lib/server/plugins-approval-guidance.ts +115 -0
  283. package/src/lib/server/plugins.test.ts +1 -1
  284. package/src/lib/server/plugins.ts +22 -132
  285. package/src/lib/server/process-manager.ts +5 -8
  286. package/src/lib/server/provider-health.test.ts +137 -0
  287. package/src/lib/server/provider-health.ts +3 -3
  288. package/src/lib/server/provider-model-discovery.ts +3 -12
  289. package/src/lib/server/queue-followups.test.ts +9 -9
  290. package/src/lib/server/queue-reconcile.test.ts +2 -2
  291. package/src/lib/server/queue-recovery.test.ts +269 -0
  292. package/src/lib/server/queue.test.ts +570 -0
  293. package/src/lib/server/queue.ts +62 -455
  294. package/src/lib/server/resolve-image.ts +30 -0
  295. package/src/lib/server/runtime-settings.test.ts +4 -4
  296. package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
  297. package/src/lib/server/schedule-normalization.test.ts +279 -0
  298. package/src/lib/server/schedule-service.ts +263 -0
  299. package/src/lib/server/scheduler.ts +17 -74
  300. package/src/lib/server/session-mailbox.test.ts +191 -0
  301. package/src/lib/server/session-run-manager.test.ts +640 -0
  302. package/src/lib/server/session-run-manager.ts +59 -15
  303. package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
  304. package/src/lib/server/session-tools/calendar.ts +2 -1
  305. package/src/lib/server/session-tools/canvas.ts +2 -1
  306. package/src/lib/server/session-tools/chatroom.ts +2 -1
  307. package/src/lib/server/session-tools/connector.ts +26 -28
  308. package/src/lib/server/session-tools/context-mgmt.ts +3 -2
  309. package/src/lib/server/session-tools/crawl.ts +4 -3
  310. package/src/lib/server/session-tools/crud.ts +105 -324
  311. package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
  312. package/src/lib/server/session-tools/delegate.ts +6 -8
  313. package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
  314. package/src/lib/server/session-tools/discovery.ts +4 -3
  315. package/src/lib/server/session-tools/document.ts +2 -1
  316. package/src/lib/server/session-tools/email.ts +2 -1
  317. package/src/lib/server/session-tools/extract.ts +2 -1
  318. package/src/lib/server/session-tools/file.ts +4 -3
  319. package/src/lib/server/session-tools/http.ts +2 -1
  320. package/src/lib/server/session-tools/human-loop.ts +2 -1
  321. package/src/lib/server/session-tools/image-gen.ts +4 -3
  322. package/src/lib/server/session-tools/index.ts +26 -30
  323. package/src/lib/server/session-tools/mailbox.ts +2 -1
  324. package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
  325. package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
  326. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
  327. package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
  328. package/src/lib/server/session-tools/monitor.ts +2 -1
  329. package/src/lib/server/session-tools/platform.ts +2 -1
  330. package/src/lib/server/session-tools/plugin-creator.ts +2 -1
  331. package/src/lib/server/session-tools/replicate.ts +3 -2
  332. package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
  333. package/src/lib/server/session-tools/shell.ts +4 -9
  334. package/src/lib/server/session-tools/subagent.ts +322 -170
  335. package/src/lib/server/session-tools/table.ts +6 -5
  336. package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
  337. package/src/lib/server/session-tools/wallet.ts +7 -6
  338. package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
  339. package/src/lib/server/session-tools/web-utils.ts +317 -0
  340. package/src/lib/server/session-tools/web.ts +62 -328
  341. package/src/lib/server/skill-prompt-budget.test.ts +1 -1
  342. package/src/lib/server/skills-normalize.ts +2 -1
  343. package/src/lib/server/storage-item-access.test.ts +302 -0
  344. package/src/lib/server/storage.ts +366 -314
  345. package/src/lib/server/stream-agent-chat.test.ts +82 -3
  346. package/src/lib/server/stream-agent-chat.ts +146 -510
  347. package/src/lib/server/stream-continuation.ts +412 -0
  348. package/src/lib/server/subagent-lineage.test.ts +647 -0
  349. package/src/lib/server/subagent-lineage.ts +435 -0
  350. package/src/lib/server/subagent-runtime.test.ts +484 -0
  351. package/src/lib/server/subagent-runtime.ts +419 -0
  352. package/src/lib/server/subagent-swarm.test.ts +391 -0
  353. package/src/lib/server/subagent-swarm.ts +564 -0
  354. package/src/lib/server/system-events.ts +3 -3
  355. package/src/lib/server/task-followups.test.ts +491 -0
  356. package/src/lib/server/task-followups.ts +391 -0
  357. package/src/lib/server/task-lifecycle.test.ts +205 -0
  358. package/src/lib/server/task-lifecycle.ts +200 -0
  359. package/src/lib/server/task-quality-gate.test.ts +1 -1
  360. package/src/lib/server/task-resume.ts +208 -0
  361. package/src/lib/server/task-service.test.ts +108 -0
  362. package/src/lib/server/task-service.ts +264 -0
  363. package/src/lib/server/task-validation.test.ts +1 -1
  364. package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
  365. package/src/lib/server/tool-capability-policy.test.ts +2 -2
  366. package/src/lib/server/tool-capability-policy.ts +3 -2
  367. package/src/lib/server/tool-planning.ts +2 -1
  368. package/src/lib/server/tool-retry.ts +2 -3
  369. package/src/lib/server/wake-dispatcher.test.ts +303 -0
  370. package/src/lib/server/wake-dispatcher.ts +318 -0
  371. package/src/lib/server/wake-mode.test.ts +161 -0
  372. package/src/lib/server/wake-mode.ts +174 -0
  373. package/src/lib/server/wallet-service.ts +8 -9
  374. package/src/lib/server/watch-jobs.ts +2 -1
  375. package/src/lib/server/workspace-context.ts +2 -2
  376. package/src/lib/shared-utils.test.ts +142 -0
  377. package/src/lib/shared-utils.ts +62 -0
  378. package/src/lib/tool-event-summary.ts +2 -1
  379. package/src/lib/view-routes.test.ts +100 -0
  380. package/src/lib/wallet.test.ts +322 -6
  381. package/src/proxy.test.ts +4 -4
  382. package/src/proxy.ts +2 -3
  383. package/src/stores/set-if-changed.ts +40 -0
  384. package/src/stores/slices/agent-slice.ts +111 -0
  385. package/src/stores/slices/auth-slice.ts +25 -0
  386. package/src/stores/slices/data-slice.ts +301 -0
  387. package/src/stores/slices/index.ts +7 -0
  388. package/src/stores/slices/session-slice.ts +112 -0
  389. package/src/stores/slices/task-slice.ts +63 -0
  390. package/src/stores/slices/ui-slice.ts +192 -0
  391. package/src/stores/use-app-store.ts +17 -822
  392. package/src/stores/use-approval-store.ts +2 -1
  393. package/src/stores/use-chat-store.ts +8 -1
  394. package/src/types/index.ts +10 -0
@@ -0,0 +1,52 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { ALL_TOOLS } from '@/lib/tool-definitions'
5
+ import { getDefaultAgentPluginIds, resolveAgentPluginSelection } from './agent-default-tools'
6
+
7
+ describe('agent default tools', () => {
8
+ it('only includes known tool ids', () => {
9
+ const knownToolIds = new Set(ALL_TOOLS.map((tool) => tool.id))
10
+ const defaults = getDefaultAgentPluginIds()
11
+
12
+ assert.ok(defaults.length > 0)
13
+ assert.deepEqual(defaults, Array.from(new Set(defaults)))
14
+ assert.equal(defaults.every((toolId) => knownToolIds.has(toolId)), true)
15
+ })
16
+
17
+ it('uses the shared defaults when a request never chose tools', () => {
18
+ assert.deepEqual(
19
+ resolveAgentPluginSelection({
20
+ hasExplicitPlugins: false,
21
+ hasExplicitTools: false,
22
+ plugins: [],
23
+ tools: undefined,
24
+ }),
25
+ getDefaultAgentPluginIds(),
26
+ )
27
+ })
28
+
29
+ it('preserves an explicit empty plugins selection', () => {
30
+ assert.deepEqual(
31
+ resolveAgentPluginSelection({
32
+ hasExplicitPlugins: true,
33
+ hasExplicitTools: false,
34
+ plugins: [],
35
+ tools: ['web'],
36
+ }),
37
+ [],
38
+ )
39
+ })
40
+
41
+ it('accepts explicit legacy tools selections', () => {
42
+ assert.deepEqual(
43
+ resolveAgentPluginSelection({
44
+ hasExplicitPlugins: false,
45
+ hasExplicitTools: true,
46
+ plugins: [],
47
+ tools: ['web', 'browser'],
48
+ }),
49
+ ['web', 'browser'],
50
+ )
51
+ })
52
+ })
@@ -0,0 +1,40 @@
1
+ import { ALL_TOOLS } from '@/lib/tool-definitions'
2
+
3
+ const DEFAULT_AGENT_PLUGIN_IDS = [
4
+ 'shell',
5
+ 'files',
6
+ 'edit_file',
7
+ 'web',
8
+ 'browser',
9
+ 'memory',
10
+ 'delegate',
11
+ 'sandbox',
12
+ 'create_document',
13
+ 'create_spreadsheet',
14
+ 'http_request',
15
+ 'git',
16
+ 'monitor',
17
+ ] as const
18
+
19
+ const KNOWN_TOOL_IDS = new Set(ALL_TOOLS.map((tool) => tool.id))
20
+
21
+ export function getDefaultAgentPluginIds(): string[] {
22
+ return DEFAULT_AGENT_PLUGIN_IDS.filter((toolId) => KNOWN_TOOL_IDS.has(toolId))
23
+ }
24
+
25
+ export function resolveAgentPluginSelection(options: {
26
+ hasExplicitPlugins: boolean
27
+ hasExplicitTools: boolean
28
+ plugins?: string[] | null
29
+ tools?: string[] | null
30
+ }): string[] {
31
+ const { hasExplicitPlugins, hasExplicitTools, plugins, tools } = options
32
+
33
+ if (hasExplicitPlugins) return Array.isArray(plugins) ? plugins : []
34
+ if (hasExplicitTools) return Array.isArray(tools) ? tools : []
35
+
36
+ if (Array.isArray(plugins) && plugins.length) return plugins
37
+ if (Array.isArray(tools) && tools.length) return tools
38
+
39
+ return getDefaultAgentPluginIds()
40
+ }
@@ -47,3 +47,24 @@ test('does not dedupe non-GET requests', async () => {
47
47
 
48
48
  assert.equal(calls, 2)
49
49
  })
50
+
51
+ test('retries GET requests that fail with TimeoutError', async () => {
52
+ let calls = 0
53
+ global.fetch = (async () => {
54
+ calls += 1
55
+ if (calls === 1) {
56
+ const error = new Error('Request timed out after 12000ms')
57
+ error.name = 'TimeoutError'
58
+ throw error
59
+ }
60
+ return new Response(JSON.stringify({ ok: true }), {
61
+ headers: { 'content-type': 'application/json' },
62
+ status: 200,
63
+ })
64
+ }) as typeof fetch
65
+
66
+ const result = await api<{ ok: boolean }>('GET', '/timeout-retry')
67
+
68
+ assert.deepEqual(result, { ok: true })
69
+ assert.equal(calls, 2)
70
+ })
@@ -1,5 +1,6 @@
1
- import { fetchWithTimeout } from '@/lib/fetch-timeout'
1
+ import { fetchWithTimeout, isAbortError, isTimeoutError } from '@/lib/fetch-timeout'
2
2
  import { safeStorageGet, safeStorageSet, safeStorageRemove } from '@/lib/safe-storage'
3
+ import { sleep } from '@/lib/shared-utils'
3
4
 
4
5
  const ACCESS_KEY_STORAGE = 'sc_access_key'
5
6
  const DEFAULT_API_TIMEOUT_MS = 12_000
@@ -19,15 +20,6 @@ export function clearStoredAccessKey() {
19
20
  safeStorageRemove(ACCESS_KEY_STORAGE)
20
21
  }
21
22
 
22
- function sleep(ms: number): Promise<void> {
23
- return new Promise((resolve) => setTimeout(resolve, ms))
24
- }
25
-
26
- function isAbortError(err: unknown): boolean {
27
- if (!err || typeof err !== 'object') return false
28
- return (err as { name?: string }).name === 'AbortError'
29
- }
30
-
31
23
  function buildInflightGetKey(path: string, key: string): string {
32
24
  return `${key}::${path}`
33
25
  }
@@ -85,7 +77,10 @@ export async function api<T = unknown>(
85
77
  return r.text() as unknown as T
86
78
  } catch (err) {
87
79
  const isLastAttempt = attempt >= retries
88
- const retryable = isAbortError(err) || (err instanceof TypeError && !String(err.message || '').includes('Unauthorized'))
80
+ const retryable =
81
+ isAbortError(err)
82
+ || isTimeoutError(err)
83
+ || (err instanceof TypeError && !String(err.message || '').includes('Unauthorized'))
89
84
  if (isLastAttempt || !retryable) throw err
90
85
  await sleep(RETRY_DELAY_BASE_MS * (attempt + 1))
91
86
  }
@@ -0,0 +1,360 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {
4
+ normalizeCanvasDocument,
5
+ normalizeCanvasContent,
6
+ isCanvasDocument,
7
+ summarizeCanvasContent,
8
+ } from './canvas-content'
9
+
10
+ describe('normalizeCanvasContent', () => {
11
+ it('returns null for null/undefined', () => {
12
+ assert.equal(normalizeCanvasContent(null), null)
13
+ assert.equal(normalizeCanvasContent(undefined), null)
14
+ })
15
+
16
+ it('returns string content as-is', () => {
17
+ assert.equal(normalizeCanvasContent('<p>hello</p>'), '<p>hello</p>')
18
+ })
19
+
20
+ it('returns null for empty string', () => {
21
+ assert.equal(normalizeCanvasContent(''), null)
22
+ })
23
+
24
+ it('normalizes a valid structured document', () => {
25
+ const doc = normalizeCanvasContent({
26
+ title: 'Test',
27
+ blocks: [{ type: 'markdown', markdown: '# Hello' }],
28
+ })
29
+ assert.ok(doc !== null && typeof doc === 'object')
30
+ assert.equal((doc as { kind: string }).kind, 'structured')
31
+ })
32
+
33
+ it('returns null for object without valid blocks', () => {
34
+ assert.equal(normalizeCanvasContent({ blocks: [] }), null)
35
+ assert.equal(normalizeCanvasContent({}), null)
36
+ assert.equal(normalizeCanvasContent({ blocks: [{ type: 'unknown' }] }), null)
37
+ })
38
+ })
39
+
40
+ describe('normalizeCanvasDocument', () => {
41
+ it('returns null for non-object input', () => {
42
+ assert.equal(normalizeCanvasDocument('string'), null)
43
+ assert.equal(normalizeCanvasDocument(42), null)
44
+ assert.equal(normalizeCanvasDocument(null), null)
45
+ assert.equal(normalizeCanvasDocument([]), null)
46
+ assert.equal(normalizeCanvasDocument(true), null)
47
+ })
48
+
49
+ it('normalizes a markdown block', () => {
50
+ const doc = normalizeCanvasDocument({
51
+ blocks: [{ type: 'markdown', markdown: '# Hi' }],
52
+ })
53
+ assert.ok(doc)
54
+ assert.equal(doc.blocks.length, 1)
55
+ assert.equal(doc.blocks[0].type, 'markdown')
56
+ assert.equal(doc.kind, 'structured')
57
+ assert.equal(doc.theme, 'slate') // default theme
58
+ })
59
+
60
+ it('applies theme when valid', () => {
61
+ const doc = normalizeCanvasDocument({
62
+ blocks: [{ type: 'markdown', markdown: 'x' }],
63
+ theme: 'emerald',
64
+ })
65
+ assert.ok(doc)
66
+ assert.equal(doc.theme, 'emerald')
67
+ })
68
+
69
+ it('defaults to slate for invalid theme', () => {
70
+ const doc = normalizeCanvasDocument({
71
+ blocks: [{ type: 'markdown', markdown: 'x' }],
72
+ theme: 'neon',
73
+ })
74
+ assert.ok(doc)
75
+ assert.equal(doc.theme, 'slate')
76
+ })
77
+
78
+ it('truncates title and subtitle', () => {
79
+ const doc = normalizeCanvasDocument({
80
+ title: 'A'.repeat(300),
81
+ subtitle: 'B'.repeat(500),
82
+ blocks: [{ type: 'markdown', markdown: 'x' }],
83
+ })
84
+ assert.ok(doc)
85
+ assert.equal(doc.title!.length, 180)
86
+ assert.equal(doc.subtitle!.length, 320)
87
+ })
88
+
89
+ it('skips invalid blocks and keeps valid ones', () => {
90
+ const doc = normalizeCanvasDocument({
91
+ blocks: [
92
+ { type: 'markdown', markdown: 'valid' },
93
+ { type: 'unknown_type' },
94
+ null,
95
+ 'just a string',
96
+ { type: 'code', code: 'console.log(1)' },
97
+ ],
98
+ })
99
+ assert.ok(doc)
100
+ assert.equal(doc.blocks.length, 2)
101
+ assert.equal(doc.blocks[0].type, 'markdown')
102
+ assert.equal(doc.blocks[1].type, 'code')
103
+ })
104
+
105
+ it('enforces max 24 blocks', () => {
106
+ const blocks = Array.from({ length: 30 }, () => ({
107
+ type: 'markdown',
108
+ markdown: 'x',
109
+ }))
110
+ const doc = normalizeCanvasDocument({ blocks })
111
+ assert.ok(doc)
112
+ assert.equal(doc.blocks.length, 24)
113
+ })
114
+
115
+ it('normalizes metrics block', () => {
116
+ const doc = normalizeCanvasDocument({
117
+ blocks: [{
118
+ type: 'metrics',
119
+ items: [
120
+ { label: 'CPU', value: '90%', tone: 'warning' },
121
+ { label: 'Mem', value: '4GB', detail: 'of 16GB', tone: 'positive' },
122
+ { label: 'missing value' }, // should be skipped
123
+ { value: 'missing label' }, // should be skipped
124
+ ],
125
+ }],
126
+ })
127
+ assert.ok(doc)
128
+ assert.equal(doc.blocks.length, 1)
129
+ const block = doc.blocks[0] as { type: string; items: Array<{ label: string; value: string; tone: string }> }
130
+ assert.equal(block.items.length, 2)
131
+ assert.equal(block.items[0].tone, 'warning')
132
+ assert.equal(block.items[1].tone, 'positive')
133
+ })
134
+
135
+ it('enforces max 24 metric items', () => {
136
+ const items = Array.from({ length: 30 }, (_, i) => ({
137
+ label: `L${i}`, value: `V${i}`,
138
+ }))
139
+ const doc = normalizeCanvasDocument({
140
+ blocks: [{ type: 'metrics', items }],
141
+ })
142
+ assert.ok(doc)
143
+ const block = doc.blocks[0] as { items: unknown[] }
144
+ assert.equal(block.items.length, 24)
145
+ })
146
+
147
+ it('normalizes cards block', () => {
148
+ const doc = normalizeCanvasDocument({
149
+ blocks: [{
150
+ type: 'cards',
151
+ items: [
152
+ { title: 'Card 1', body: 'content', tone: 'negative' },
153
+ { body: 'no title' }, // skipped
154
+ ],
155
+ }],
156
+ })
157
+ assert.ok(doc)
158
+ const block = doc.blocks[0] as { items: Array<{ title: string; tone: string }> }
159
+ assert.equal(block.items.length, 1)
160
+ assert.equal(block.items[0].tone, 'negative')
161
+ })
162
+
163
+ it('normalizes table block', () => {
164
+ const doc = normalizeCanvasDocument({
165
+ blocks: [{
166
+ type: 'table',
167
+ table: {
168
+ columns: ['Name', 'Age'],
169
+ rows: [['Alice', 30], ['Bob', true]],
170
+ caption: 'Users',
171
+ },
172
+ }],
173
+ })
174
+ assert.ok(doc)
175
+ assert.equal(doc.blocks[0].type, 'table')
176
+ })
177
+
178
+ it('rejects table with no columns', () => {
179
+ const doc = normalizeCanvasDocument({
180
+ blocks: [{
181
+ type: 'table',
182
+ table: { columns: [], rows: [] },
183
+ }],
184
+ })
185
+ assert.equal(doc, null) // no valid blocks
186
+ })
187
+
188
+ it('rejects table with no rows', () => {
189
+ const doc = normalizeCanvasDocument({
190
+ blocks: [{
191
+ type: 'table',
192
+ table: { columns: ['A'], rows: [] },
193
+ }],
194
+ })
195
+ assert.equal(doc, null)
196
+ })
197
+
198
+ it('limits table to 20 columns and 100 rows', () => {
199
+ const columns = Array.from({ length: 25 }, (_, i) => `Col${i}`)
200
+ const rows = Array.from({ length: 110 }, () => columns.map((_, i) => `val${i}`))
201
+ const doc = normalizeCanvasDocument({
202
+ blocks: [{
203
+ type: 'table',
204
+ table: { columns, rows },
205
+ }],
206
+ })
207
+ assert.ok(doc)
208
+ const block = doc.blocks[0] as { table: { columns: string[]; rows: unknown[][] } }
209
+ assert.equal(block.table.columns.length, 20)
210
+ assert.equal(block.table.rows.length, 100)
211
+ })
212
+
213
+ it('normalizes actions block', () => {
214
+ const doc = normalizeCanvasDocument({
215
+ blocks: [{
216
+ type: 'actions',
217
+ items: [
218
+ { label: 'Submit', intent: 'primary', href: 'https://x.com' },
219
+ { label: 'Delete', intent: 'danger' },
220
+ { intent: 'primary' }, // no label → skipped
221
+ ],
222
+ }],
223
+ })
224
+ assert.ok(doc)
225
+ const block = doc.blocks[0] as { items: Array<{ label: string; intent: string }> }
226
+ assert.equal(block.items.length, 2)
227
+ assert.equal(block.items[0].intent, 'primary')
228
+ assert.equal(block.items[1].intent, 'danger')
229
+ })
230
+
231
+ it('defaults action intent to secondary', () => {
232
+ const doc = normalizeCanvasDocument({
233
+ blocks: [{
234
+ type: 'actions',
235
+ items: [{ label: 'Go', intent: 'invalid' }],
236
+ }],
237
+ })
238
+ assert.ok(doc)
239
+ const block = doc.blocks[0] as { items: Array<{ intent: string }> }
240
+ assert.equal(block.items[0].intent, 'secondary')
241
+ })
242
+
243
+ it('normalizes code block with language', () => {
244
+ const doc = normalizeCanvasDocument({
245
+ blocks: [{
246
+ type: 'code',
247
+ code: 'const x = 1;',
248
+ language: 'typescript',
249
+ }],
250
+ })
251
+ assert.ok(doc)
252
+ const block = doc.blocks[0] as { type: string; code: string; language: string }
253
+ assert.equal(block.code, 'const x = 1;')
254
+ assert.equal(block.language, 'typescript')
255
+ })
256
+
257
+ it('rejects code block with empty code', () => {
258
+ const doc = normalizeCanvasDocument({
259
+ blocks: [{ type: 'code', code: '' }],
260
+ })
261
+ assert.equal(doc, null)
262
+ })
263
+
264
+ it('coerces number/boolean to string in asTrimmedString contexts', () => {
265
+ const doc = normalizeCanvasDocument({
266
+ blocks: [{
267
+ type: 'metrics',
268
+ items: [{ label: 42, value: true }],
269
+ }],
270
+ })
271
+ assert.ok(doc)
272
+ const block = doc.blocks[0] as { items: Array<{ label: string; value: string }> }
273
+ assert.equal(block.items[0].label, '42')
274
+ assert.equal(block.items[0].value, 'true')
275
+ })
276
+
277
+ it('truncates markdown content to 20000 chars', () => {
278
+ const longMarkdown = 'x'.repeat(25000)
279
+ const doc = normalizeCanvasDocument({
280
+ blocks: [{ type: 'markdown', markdown: longMarkdown }],
281
+ })
282
+ assert.ok(doc)
283
+ const block = doc.blocks[0] as { markdown: string }
284
+ assert.equal(block.markdown.length, 20000)
285
+ })
286
+
287
+ it('uses provided updatedAt when valid', () => {
288
+ const doc = normalizeCanvasDocument({
289
+ blocks: [{ type: 'markdown', markdown: 'x' }],
290
+ updatedAt: 1234567890,
291
+ })
292
+ assert.ok(doc)
293
+ assert.equal(doc.updatedAt, 1234567890)
294
+ })
295
+
296
+ it('falls back to Date.now() for invalid updatedAt', () => {
297
+ const before = Date.now()
298
+ const doc = normalizeCanvasDocument({
299
+ blocks: [{ type: 'markdown', markdown: 'x' }],
300
+ updatedAt: 'not a number',
301
+ })
302
+ const after = Date.now()
303
+ assert.ok(doc)
304
+ assert.ok(doc.updatedAt >= before && doc.updatedAt <= after)
305
+ })
306
+ })
307
+
308
+ describe('isCanvasDocument', () => {
309
+ it('returns true for valid document object', () => {
310
+ assert.equal(isCanvasDocument({
311
+ blocks: [{ type: 'markdown', markdown: 'hi' }],
312
+ }), true)
313
+ })
314
+
315
+ it('returns false for non-documents', () => {
316
+ assert.equal(isCanvasDocument('string'), false)
317
+ assert.equal(isCanvasDocument(null), false)
318
+ assert.equal(isCanvasDocument({}), false)
319
+ assert.equal(isCanvasDocument({ blocks: [] }), false)
320
+ })
321
+ })
322
+
323
+ describe('summarizeCanvasContent', () => {
324
+ it('summarizes null content', () => {
325
+ const summary = summarizeCanvasContent(null)
326
+ assert.equal(summary.kind, 'empty')
327
+ assert.equal(summary.hasContent, false)
328
+ assert.equal(summary.contentLength, 0)
329
+ })
330
+
331
+ it('summarizes string content', () => {
332
+ const summary = summarizeCanvasContent('<p>hello</p>')
333
+ assert.equal(summary.kind, 'html')
334
+ assert.equal(summary.hasContent, true)
335
+ assert.equal(summary.contentLength, 12)
336
+ assert.equal(summary.preview, '<p>hello</p>')
337
+ })
338
+
339
+ it('truncates preview to 500 chars for long strings', () => {
340
+ const long = 'x'.repeat(600)
341
+ const summary = summarizeCanvasContent(long)
342
+ assert.equal((summary.preview as string).length, 500)
343
+ })
344
+
345
+ it('summarizes structured document', () => {
346
+ const doc = normalizeCanvasDocument({
347
+ title: 'My Doc',
348
+ blocks: [
349
+ { type: 'markdown', markdown: '# Heading' },
350
+ { type: 'code', code: 'x = 1' },
351
+ ],
352
+ })!
353
+ const summary = summarizeCanvasContent(doc)
354
+ assert.equal(summary.kind, 'structured')
355
+ assert.equal(summary.hasContent, true)
356
+ assert.equal(summary.blockCount, 2)
357
+ assert.equal(summary.title, 'My Doc')
358
+ assert.deepEqual(summary.blockTypes, ['markdown', 'code'])
359
+ })
360
+ })
@@ -20,11 +20,11 @@ describe('chat-streaming-state', () => {
20
20
  }
21
21
 
22
22
  assert.equal(
23
- shouldHidePersistedStreamingAssistantMessage(message, { localStreaming: true, displayText: 'live text' }),
23
+ shouldHidePersistedStreamingAssistantMessage(message, { localStreaming: true, hasLiveArtifacts: true }),
24
24
  true,
25
25
  )
26
26
  assert.equal(
27
- shouldHidePersistedStreamingAssistantMessage(message, { localStreaming: true, displayText: '' }),
27
+ shouldHidePersistedStreamingAssistantMessage(message, { localStreaming: true, hasLiveArtifacts: false }),
28
28
  false,
29
29
  )
30
30
  })
@@ -140,6 +140,53 @@ describe('chat-streaming-state', () => {
140
140
  ])
141
141
  })
142
142
 
143
+ it('reuses the previous assistant slot when only streaming whitespace differs', () => {
144
+ const messages: Message[] = [
145
+ { role: 'user', text: 'hello', time: 1 },
146
+ { role: 'assistant', text: 'Here are the results.', time: 2, kind: 'chat' },
147
+ ]
148
+ const completed: Message = { role: 'assistant', text: 'Here\n\n are the results.', time: 3, kind: 'chat' }
149
+
150
+ assert.deepEqual(mergeCompletedAssistantMessage(messages, completed), [
151
+ { role: 'user', text: 'hello', time: 1 },
152
+ { role: 'assistant', text: 'Here\n\n are the results.', time: 2, kind: 'chat' },
153
+ ])
154
+ })
155
+
156
+ it('materializes a stale streaming artifact into the existing completed assistant slot', () => {
157
+ const messages: Message[] = [
158
+ { role: 'user', text: 'hello', time: 1 },
159
+ {
160
+ role: 'assistant',
161
+ text: 'Here are the results.',
162
+ time: 2,
163
+ kind: 'chat',
164
+ },
165
+ {
166
+ role: 'assistant',
167
+ text: 'Here\n\n are the results.',
168
+ time: 3,
169
+ streaming: true,
170
+ toolEvents: [{ name: 'web', input: '{"action":"search"}' }],
171
+ },
172
+ ]
173
+
174
+ const changed = materializeStreamingAssistantArtifacts(messages)
175
+
176
+ assert.equal(changed, true)
177
+ assert.deepEqual(messages, [
178
+ { role: 'user', text: 'hello', time: 1 },
179
+ {
180
+ role: 'assistant',
181
+ text: 'Here\n\n are the results.',
182
+ time: 2,
183
+ kind: 'chat',
184
+ streaming: false,
185
+ toolEvents: [{ name: 'web', input: '{"action":"search"}' }],
186
+ },
187
+ ])
188
+ })
189
+
143
190
  it('detects same-length message updates during reconciliation', () => {
144
191
  const previous: Message[] = [
145
192
  { role: 'assistant', text: 'partial', time: 1, streaming: true },
@@ -21,13 +21,13 @@ function isStreamingAssistantMessage(
21
21
 
22
22
  export function shouldHidePersistedStreamingAssistantMessage(
23
23
  message: Message,
24
- opts: { localStreaming: boolean; displayText: string },
24
+ opts: { localStreaming: boolean; hasLiveArtifacts: boolean },
25
25
  ): boolean {
26
26
  return (
27
27
  opts.localStreaming
28
28
  && message.role === 'assistant'
29
29
  && message.streaming === true
30
- && opts.displayText.trim().length > 0
30
+ && opts.hasLiveArtifacts
31
31
  )
32
32
  }
33
33
 
@@ -54,12 +54,32 @@ export function upsertStreamingAssistantArtifact(
54
54
  return true
55
55
  }
56
56
 
57
+ function normalizeAssistantMergeText(text: string | undefined): string {
58
+ return String(text || '').replace(/\s+/g, ' ').trim()
59
+ }
60
+
61
+ function withPreservedAssistantFields(previous: Message, next: Message): Message {
62
+ const merged: Message = {
63
+ ...previous,
64
+ ...next,
65
+ time: previous.time,
66
+ }
67
+ if (next.kind === undefined && previous.kind !== undefined) merged.kind = previous.kind
68
+ if (next.thinking === undefined && previous.thinking === undefined) delete merged.thinking
69
+ if (next.thinking === undefined && previous.thinking !== undefined) merged.thinking = previous.thinking
70
+ if (next.toolEvents === undefined && previous.toolEvents === undefined) delete merged.toolEvents
71
+ if (next.toolEvents === undefined && previous.toolEvents !== undefined) merged.toolEvents = previous.toolEvents
72
+ if (next.suggestions === undefined && previous.suggestions === undefined) delete merged.suggestions
73
+ if (next.suggestions === undefined && previous.suggestions !== undefined) merged.suggestions = previous.suggestions
74
+ return merged
75
+ }
76
+
57
77
  export function materializeStreamingAssistantArtifacts(
58
78
  messages: Message[],
59
79
  opts: StreamingArtifactWindow = {},
60
80
  ): boolean {
61
81
  let changed = false
62
- const nextMessages: Message[] = []
82
+ let nextMessages: Message[] = []
63
83
 
64
84
  for (let index = 0; index < messages.length; index += 1) {
65
85
  const message = messages[index]
@@ -81,7 +101,7 @@ export function materializeStreamingAssistantArtifacts(
81
101
  continue
82
102
  }
83
103
 
84
- nextMessages.push({
104
+ nextMessages = mergeCompletedAssistantMessage(nextMessages, {
85
105
  ...message,
86
106
  text: nextText,
87
107
  streaming: false,
@@ -107,15 +127,11 @@ export function mergeCompletedAssistantMessage(messages: Message[], assistantMes
107
127
  last
108
128
  && last.role === 'assistant'
109
129
  && (last.kind || 'chat') === (assistantMessage.kind || 'chat')
110
- && last.text.trim() === assistantMessage.text.trim()
130
+ && normalizeAssistantMergeText(last.text) === normalizeAssistantMergeText(assistantMessage.text)
111
131
  ) {
112
132
  return [
113
133
  ...base.slice(0, -1),
114
- {
115
- ...last,
116
- ...assistantMessage,
117
- time: last.time,
118
- },
134
+ withPreservedAssistantFields(last, assistantMessage),
119
135
  ]
120
136
  }
121
137
  return [...base, assistantMessage]