@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
@@ -1,13 +1,14 @@
1
1
  'use client'
2
2
 
3
3
  import { DEFAULT_HEARTBEAT_SHOW_ALERTS, DEFAULT_HEARTBEAT_SHOW_OK } from '@/lib/heartbeat-defaults'
4
- import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
4
+ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
5
5
  import type { Message } from '@/types'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
7
7
  import { useAppStore } from '@/stores/use-app-store'
8
8
  import { api } from '@/lib/api-client'
9
9
  import { shouldHidePersistedStreamingAssistantMessage } from '@/lib/chat-streaming-state'
10
10
  import { dedupeMessagesForDisplay } from '@/lib/chat-display'
11
+ import { errorMessage } from '@/lib/shared-utils'
11
12
  import { AgentAvatar } from '@/components/agents/agent-avatar'
12
13
  import { MessageBubble } from './message-bubble'
13
14
  import { StreamingBubble } from './streaming-bubble'
@@ -71,12 +72,63 @@ interface Props {
71
72
  loading?: boolean
72
73
  }
73
74
 
75
+ interface LiveStreamLaneProps {
76
+ streaming: boolean
77
+ hasVisiblePersistedStreamingMessage: boolean
78
+ assistantName?: string
79
+ agentAvatarSeed?: string
80
+ agentAvatarUrl?: string | null
81
+ agentName?: string
82
+ }
83
+
84
+ const LiveStreamLane = memo(function LiveStreamLane({
85
+ streaming,
86
+ hasVisiblePersistedStreamingMessage,
87
+ assistantName,
88
+ agentAvatarSeed,
89
+ agentAvatarUrl,
90
+ agentName,
91
+ }: LiveStreamLaneProps) {
92
+ const displayText = useChatStore((s) => s.displayText)
93
+ const hasToolEvents = useChatStore((s) => s.toolEvents.length > 0)
94
+
95
+ if (!streaming) return null
96
+
97
+ if (!displayText && !hasToolEvents) {
98
+ if (hasVisiblePersistedStreamingMessage) return null
99
+ return (
100
+ <ThinkingIndicator
101
+ assistantName={assistantName}
102
+ agentAvatarSeed={agentAvatarSeed}
103
+ agentAvatarUrl={agentAvatarUrl}
104
+ agentName={agentName}
105
+ />
106
+ )
107
+ }
108
+
109
+ return (
110
+ <StreamingBubble
111
+ text={displayText}
112
+ assistantName={assistantName}
113
+ agentAvatarSeed={agentAvatarSeed}
114
+ agentAvatarUrl={agentAvatarUrl}
115
+ agentName={agentName}
116
+ />
117
+ )
118
+ })
119
+
74
120
  export function MessageList({ messages, streaming, connectorFilter = null, loading = false }: Props) {
75
121
  const scrollRef = useRef<HTMLDivElement>(null)
76
122
  const [showScrollToBottom, setShowScrollToBottom] = useState(false)
123
+ const settledCountRef = useRef(0)
77
124
  const snapUntilRef = useRef(0)
78
125
  const prevSessionIdRef = useRef<string | null>(null)
79
- const displayText = useChatStore((s) => s.displayText)
126
+ const hasLiveText = useChatStore((s) => s.displayText.trim().length > 0)
127
+ const hasLiveArtifacts = useChatStore((s) => (
128
+ s.displayText.trim().length > 0
129
+ || s.toolEvents.length > 0
130
+ || s.thinkingText.trim().length > 0
131
+ ))
80
132
  const setMessages = useChatStore((s) => s.setMessages)
81
133
  const retryLastMessage = useChatStore((s) => s.retryLastMessage)
82
134
  const editAndResend = useChatStore((s) => s.editAndResend)
@@ -152,33 +204,41 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
152
204
 
153
205
  // Connector filtering is handled via connectorFilter prop from chat-area
154
206
 
207
+ // Use refs for callbacks so transcriptNodes memo doesn't bust on every messages change
208
+ const messagesCallbackRef = useRef(messages)
209
+ messagesCallbackRef.current = messages
210
+ const sessionIdRef = useRef(sessionId)
211
+ sessionIdRef.current = sessionId
212
+
155
213
  const toggleBookmark = useCallback(async (index: number) => {
156
- if (!sessionId) return
157
- const msg = messages[index]
214
+ const sid = sessionIdRef.current
215
+ const msgs = messagesCallbackRef.current
216
+ if (!sid) return
217
+ const msg = msgs[index]
158
218
  if (!msg) return
159
219
  const next = !msg.bookmarked
160
220
  try {
161
- await api('PUT', `/chats/${sessionId}/messages`, { messageIndex: index, bookmarked: next })
162
- const updated = [...messages]
221
+ await api('PUT', `/chats/${sid}/messages`, { messageIndex: index, bookmarked: next })
222
+ const updated = [...msgs]
163
223
  updated[index] = { ...updated[index], bookmarked: next }
164
224
  setMessages(updated)
165
225
  } catch (err: unknown) {
166
- console.error('Failed to toggle bookmark:', err instanceof Error ? err.message : String(err))
226
+ console.error('Failed to toggle bookmark:', errorMessage(err))
167
227
  }
168
228
  // eslint-disable-next-line react-hooks/exhaustive-deps
169
- }, [sessionId, messages])
229
+ }, [])
170
230
 
171
231
  const handleEditResend = useCallback(async (index: number, newText: string) => {
172
- if (!sessionId || !editAndResend) return
232
+ if (!sessionIdRef.current || !editAndResend) return
173
233
  await editAndResend(index, newText)
174
234
  // eslint-disable-next-line react-hooks/exhaustive-deps
175
- }, [sessionId])
235
+ }, [])
176
236
 
177
237
  const handleFork = useCallback(async (index: number) => {
178
- if (!sessionId || !forkSession) return
179
- await forkSession(sessionId, index)
238
+ if (!sessionIdRef.current || !forkSession) return
239
+ await forkSession(sessionIdRef.current, index)
180
240
  // eslint-disable-next-line react-hooks/exhaustive-deps
181
- }, [sessionId])
241
+ }, [])
182
242
 
183
243
  // In-thread search
184
244
  const [searchOpen, setSearchOpen] = useState(false)
@@ -195,36 +255,43 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
195
255
  const isHeartbeatOk = (msg: Message) =>
196
256
  msg.suppressed === true || (msg.kind === 'heartbeat' && (/^\s*HEARTBEAT_OK\b/i.test(msg.text || '') || /^\s*NO_MESSAGE\b/i.test(msg.text || '')))
197
257
 
198
- const displayedMessages: Message[] = []
199
- for (const msg of messages) {
200
- if (shouldHidePersistedStreamingAssistantMessage(msg, { localStreaming: streaming, displayText })) continue
201
- const isHeartbeat = isHeartbeatMessage(msg)
258
+ const dedupedDisplayedMessages = useMemo(() => {
259
+ const displayedMessages: Message[] = []
260
+ for (const msg of messages) {
261
+ if (shouldHidePersistedStreamingAssistantMessage(msg, { localStreaming: streaming, hasLiveArtifacts })) continue
262
+ const isHeartbeat = isHeartbeatMessage(msg)
202
263
 
203
- // Visibility filtering based on settings
204
- if (isHeartbeat) {
205
- if (!showAlerts) continue // Hide all heartbeat messages
206
- if (!showOk && isHeartbeatOk(msg)) continue // Hide OK messages
207
- }
264
+ if (isHeartbeat) {
265
+ if (!showAlerts) continue
266
+ if (!showOk && isHeartbeatOk(msg)) continue
267
+ }
208
268
 
209
- const last = displayedMessages[displayedMessages.length - 1]
210
- const lastIsHeartbeat = !!last && isHeartbeatMessage(last)
211
- if (isHeartbeat && lastIsHeartbeat) {
212
- displayedMessages[displayedMessages.length - 1] = msg
213
- } else {
214
- displayedMessages.push(msg)
269
+ const last = displayedMessages[displayedMessages.length - 1]
270
+ const lastIsHeartbeat = !!last && isHeartbeatMessage(last)
271
+ if (isHeartbeat && lastIsHeartbeat) {
272
+ displayedMessages[displayedMessages.length - 1] = msg
273
+ } else {
274
+ displayedMessages.push(msg)
275
+ }
215
276
  }
216
- }
217
277
 
218
- const dedupedDisplayedMessages = dedupeMessagesForDisplay(displayedMessages)
278
+ return dedupeMessagesForDisplay(displayedMessages)
279
+ }, [hasLiveArtifacts, messages, showAlerts, showOk, streaming])
219
280
 
220
- // Apply bookmark + connector filter
221
- let filteredMessages = bookmarkFilter
222
- ? dedupedDisplayedMessages.filter((msg) => msg.bookmarked)
223
- : dedupedDisplayedMessages
224
- if (connectorFilter) {
225
- filteredMessages = filteredMessages.filter((msg) => msg.source?.connectorId === connectorFilter)
226
- }
227
- const hasVisiblePersistedStreamingMessage = filteredMessages.some((msg) => msg.role === 'assistant' && msg.streaming === true)
281
+ const filteredMessages = useMemo(() => {
282
+ let nextMessages = bookmarkFilter
283
+ ? dedupedDisplayedMessages.filter((msg) => msg.bookmarked)
284
+ : dedupedDisplayedMessages
285
+ if (connectorFilter) {
286
+ nextMessages = nextMessages.filter((msg) => msg.source?.connectorId === connectorFilter)
287
+ }
288
+ return nextMessages
289
+ }, [bookmarkFilter, connectorFilter, dedupedDisplayedMessages])
290
+
291
+ const hasVisiblePersistedStreamingMessage = useMemo(
292
+ () => filteredMessages.some((msg) => msg.role === 'assistant' && msg.streaming === true),
293
+ [filteredMessages],
294
+ )
228
295
 
229
296
  // Search matches
230
297
  const searchMatches = useMemo(() => {
@@ -234,6 +301,158 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
234
301
  .map((msg, i) => ({ msg, i }))
235
302
  .filter(({ msg }) => msg.text.toLowerCase().includes(normalizedQuery))
236
303
  }, [filteredMessages, searchQuery])
304
+ const searchMatchSet = useMemo(() => new Set(searchMatches.map((match) => match.i)), [searchMatches])
305
+ const currentSearchMatchIndex = searchQuery ? (searchMatches[searchIdx]?.i ?? null) : null
306
+ const originalIndexMap = useMemo(() => {
307
+ const indexMap = new Map<Message, number>()
308
+ messages.forEach((msg, index) => {
309
+ indexMap.set(msg, index)
310
+ })
311
+ return indexMap
312
+ }, [messages])
313
+
314
+ const handleDeleteMessage = useCallback(async (messageIndex: number) => {
315
+ const sid = sessionIdRef.current
316
+ const msgs = messagesCallbackRef.current
317
+ if (!sid || messageIndex < 0) return
318
+ try {
319
+ await api('DELETE', `/chats/${sid}/messages`, { messageIndex })
320
+ setMessages(msgs.filter((_: Message, idx: number) => idx !== messageIndex))
321
+ } catch {
322
+ // best-effort
323
+ }
324
+ // eslint-disable-next-line react-hooks/exhaustive-deps
325
+ }, [])
326
+
327
+ // Snapshot the settled count at memo time so it's captured in the closure.
328
+ // Messages up to this count appear instantly; only new ones get entrance animations.
329
+ const settledSnapshot = settledCountRef.current
330
+
331
+ const transcriptNodes = useMemo(() => {
332
+ let lastAssistantIndex = -1
333
+ if (!streaming) {
334
+ for (let i = filteredMessages.length - 1; i >= 0; i--) {
335
+ if (filteredMessages[i].role === 'assistant') {
336
+ lastAssistantIndex = i
337
+ break
338
+ }
339
+ }
340
+ }
341
+
342
+ return filteredMessages.map((msg, i) => {
343
+ if (msg.kind === 'context-clear') {
344
+ const originalIndex = originalIndexMap.get(msg) ?? -1
345
+ return (
346
+ <div key={`ctx-clear-${msg.time}-${i}`} className="group/ctx flex items-center gap-4 py-3">
347
+ <div className="flex-1 h-px bg-amber-400/20" />
348
+ <span className="flex items-center gap-1.5 text-[10px] font-600 text-amber-400/60 uppercase tracking-[0.1em]">
349
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="shrink-0">
350
+ <line x1="2" y1="12" x2="22" y2="12" />
351
+ <polyline points="8 8 4 12 8 16" />
352
+ <polyline points="16 8 20 12 16 16" />
353
+ </svg>
354
+ New context
355
+ {msg.time ? ` · ${new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : ''}
356
+ </span>
357
+ {sessionId && originalIndex >= 0 && (
358
+ <button
359
+ type="button"
360
+ onClick={() => void handleDeleteMessage(originalIndex)}
361
+ className="opacity-0 group-hover/ctx:opacity-100 text-[10px] font-600 text-amber-400/60 hover:text-amber-400 bg-transparent border-none cursor-pointer transition-all px-1.5 py-0.5 rounded-[4px] hover:bg-amber-400/10"
362
+ title="Undo — restore full context"
363
+ >
364
+ Undo
365
+ </button>
366
+ )}
367
+ <div className="flex-1 h-px bg-amber-400/20" />
368
+ </div>
369
+ )
370
+ }
371
+
372
+ const originalIndex = originalIndexMap.get(msg) ?? -1
373
+ const isLastAssistant = i === lastAssistantIndex
374
+ const isSearchMatch = !!searchQuery && searchMatchSet.has(i)
375
+ const isCurrentMatch = currentSearchMatchIndex === i
376
+ const prevMsg = i > 0 ? filteredMessages[i - 1] : null
377
+ const showDateSep = msg.time && (!prevMsg?.time || new Date(msg.time).toDateString() !== new Date(prevMsg.time).toDateString())
378
+
379
+ let momentOverlay: React.ReactNode = null
380
+ if (isLastAssistant && currentMoment && !streaming) {
381
+ if (currentMoment.kind === 'heartbeat') {
382
+ momentOverlay = <HeartbeatMoment onDismiss={() => setCurrentMoment(null)} />
383
+ } else {
384
+ momentOverlay = (
385
+ <ActivityMoment
386
+ key={currentMoment.key}
387
+ toolName={currentMoment.name}
388
+ toolInput={currentMoment.input}
389
+ onDismiss={() => setCurrentMoment(null)}
390
+ />
391
+ )
392
+ }
393
+ }
394
+
395
+ // Only animate genuinely new messages (arrived after the batch load).
396
+ // Settled messages (loaded on session switch) appear instantly.
397
+ const isSettled = i < settledSnapshot
398
+ const animStyle = isSettled
399
+ ? undefined
400
+ : {
401
+ animation: `${msg.role === 'user' ? 'msg-in-right' : 'msg-in-left'} 0.4s var(--ease-spring) both`,
402
+ animationDelay: `${Math.min((i - settledSnapshot) * 0.05, 0.4)}s`,
403
+ }
404
+
405
+ return (
406
+ <div
407
+ key={`${sessionId}-${msg.role}-${originalIndex >= 0 ? originalIndex : i}`}
408
+ data-message-index={i}
409
+ style={animStyle}
410
+ >
411
+ {showDateSep && (
412
+ <div className="flex items-center gap-4 py-2 mb-2">
413
+ <div className="flex-1 h-px bg-white/[0.06]" />
414
+ <span className="text-[10px] font-600 text-text-3/50 uppercase tracking-[0.1em]">
415
+ {dateSeparator(msg.time)}
416
+ </span>
417
+ <div className="flex-1 h-px bg-white/[0.06]" />
418
+ </div>
419
+ )}
420
+ <div className={isCurrentMatch ? 'ring-1 ring-amber-400/50 rounded-[16px] bg-amber-400/[0.04]' : isSearchMatch ? 'bg-white/[0.02] rounded-[16px]' : ''}>
421
+ <MessageBubble
422
+ message={msg}
423
+ assistantName={assistantName}
424
+ agentAvatarSeed={agent?.avatarSeed}
425
+ agentAvatarUrl={agent?.avatarUrl}
426
+ agentName={agent?.name}
427
+ isLast={isLastAssistant}
428
+ onRetry={isLastAssistant ? retryLastMessage : undefined}
429
+ messageIndex={originalIndex >= 0 ? originalIndex : undefined}
430
+ onToggleBookmark={toggleBookmark}
431
+ onEditResend={handleEditResend}
432
+ onFork={handleFork}
433
+ momentOverlay={momentOverlay}
434
+ />
435
+ </div>
436
+ </div>
437
+ )
438
+ })
439
+ // eslint-disable-next-line react-hooks/exhaustive-deps
440
+ }, [
441
+ agent?.avatarSeed,
442
+ agent?.avatarUrl,
443
+ agent?.name,
444
+ assistantName,
445
+ currentMoment,
446
+ currentSearchMatchIndex,
447
+ filteredMessages,
448
+ originalIndexMap,
449
+ retryLastMessage,
450
+ searchMatchSet,
451
+ searchQuery,
452
+ sessionId,
453
+ settledSnapshot,
454
+ streaming,
455
+ ])
237
456
 
238
457
  // Track whether user is at/near bottom so we know whether to auto-scroll on new content
239
458
  const wasAtBottomRef = useRef(true)
@@ -275,8 +494,15 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
275
494
  prevSessionIdRef.current = sessionId
276
495
  wasAtBottomRef.current = true
277
496
  snapUntilRef.current = Date.now() + 2000
497
+ // Mark all messages as settled — new messages loaded for this session
498
+ // will also be settled (see below).
499
+ settledCountRef.current = 0
500
+ } else if (Date.now() < snapUntilRef.current) {
501
+ // Still in the snap window (messages just loaded for this session).
502
+ // Treat the new batch as settled so they appear without stagger.
503
+ settledCountRef.current = messages.length
278
504
  }
279
- }, [sessionId])
505
+ }, [messages.length, sessionId])
280
506
 
281
507
  // Position scroll before paint — no setState here to avoid cascading renders.
282
508
  // The onScroll handler and the state-update effect below handle UI state.
@@ -290,14 +516,14 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
290
516
  el.scrollTop = el.scrollHeight
291
517
  wasAtBottomRef.current = true
292
518
  }
293
- }, [messages.length, displayText])
519
+ }, [hasLiveText, messages.length])
294
520
 
295
521
  // Update scroll-related UI state after render (separate from layoutEffect to avoid cascading)
296
522
  useEffect(() => {
297
523
  const el = scrollRef.current
298
524
  if (!el || messages.length === 0) return
299
525
  updateScrollState()
300
- }, [messages.length, displayText, updateScrollState])
526
+ }, [hasLiveText, messages.length, updateScrollState])
301
527
 
302
528
  // Re-snap when content resizes during snap window (lazy images increasing scrollHeight)
303
529
  useEffect(() => {
@@ -379,7 +605,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
379
605
  }, [searchOpen])
380
606
 
381
607
  return (
382
- <div className="relative flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden">
608
+ <div className="relative flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden" data-testid="message-list">
383
609
  <div className="shrink-0 px-4 md:px-12 lg:px-16 pt-3">
384
610
  <div className="flex flex-wrap items-center gap-2 rounded-[14px] border border-white/[0.06] bg-surface/55 px-3 py-2 backdrop-blur-sm">
385
611
  <button
@@ -519,6 +745,9 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
519
745
  <div
520
746
  ref={scrollRef}
521
747
  onScroll={updateScrollState}
748
+ role="log"
749
+ aria-label="Conversation transcript"
750
+ data-testid="chat-thread"
522
751
  className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-4 md:px-12 lg:px-16 pt-4 pb-[120px] md:pb-10 fade-up"
523
752
  >
524
753
  <div className="flex flex-col gap-6 relative">
@@ -603,110 +832,16 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
603
832
  </div>
604
833
  )
605
834
  )}
606
- {filteredMessages.map((msg, i) => {
607
- // Context-clear divider — render a visual separator instead of a bubble
608
- if (msg.kind === 'context-clear') {
609
- const originalIndex = messages.indexOf(msg)
610
- return (
611
- <div key={`ctx-clear-${msg.time}-${i}`} className="group/ctx flex items-center gap-4 py-3">
612
- <div className="flex-1 h-px bg-amber-400/20" />
613
- <span className="flex items-center gap-1.5 text-[10px] font-600 text-amber-400/60 uppercase tracking-[0.1em]">
614
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="shrink-0">
615
- <line x1="2" y1="12" x2="22" y2="12" />
616
- <polyline points="8 8 4 12 8 16" />
617
- <polyline points="16 8 20 12 16 16" />
618
- </svg>
619
- New context
620
- {msg.time ? ` · ${new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : ''}
621
- </span>
622
- {sessionId && originalIndex >= 0 && (
623
- <button
624
- type="button"
625
- onClick={async () => {
626
- try {
627
- await api('DELETE', `/chats/${sessionId}/messages`, { messageIndex: originalIndex })
628
- setMessages(messages.filter((_: Message, idx: number) => idx !== originalIndex))
629
- } catch { /* best-effort */ }
630
- }}
631
- className="opacity-0 group-hover/ctx:opacity-100 text-[10px] font-600 text-amber-400/60 hover:text-amber-400 bg-transparent border-none cursor-pointer transition-all px-1.5 py-0.5 rounded-[4px] hover:bg-amber-400/10"
632
- title="Undo — restore full context"
633
- >
634
- Undo
635
- </button>
636
- )}
637
- <div className="flex-1 h-px bg-amber-400/20" />
638
- </div>
639
- )
640
- }
641
-
642
- // Find original index in the full messages array for API calls
643
- const originalIndex = messages.indexOf(msg)
644
- const isLastAssistant = msg.role === 'assistant' && !streaming
645
- && filteredMessages.slice(i + 1).every((m) => m.role !== 'assistant')
646
- const isSearchMatch = searchQuery && searchMatches.some((m) => m.i === i)
647
- const isCurrentMatch = searchQuery && searchMatches[searchIdx]?.i === i
648
-
649
- // Date separator
650
- const prevMsg = i > 0 ? filteredMessages[i - 1] : null
651
- const showDateSep = msg.time && (!prevMsg?.time || new Date(msg.time).toDateString() !== new Date(prevMsg.time).toDateString())
652
-
653
- // Moment overlay — only on the last assistant message
654
- let momentOverlay: React.ReactNode = null
655
- if (isLastAssistant && currentMoment && !streaming) {
656
- if (currentMoment.kind === 'heartbeat') {
657
- momentOverlay = <HeartbeatMoment onDismiss={() => setCurrentMoment(null)} />
658
- } else {
659
- momentOverlay = (
660
- <ActivityMoment
661
- key={currentMoment.key}
662
- toolName={currentMoment.name}
663
- toolInput={currentMoment.input}
664
- onDismiss={() => setCurrentMoment(null)}
665
- />
666
- )
667
- }
668
- }
669
-
670
- return (
671
- <div
672
- key={`${sessionId}-${msg.role}-${originalIndex >= 0 ? originalIndex : i}`}
673
- data-message-index={i}
674
- style={{
675
- animation: `${msg.role === 'user' ? 'msg-in-right' : 'msg-in-left'} 0.4s var(--ease-spring) both`,
676
- animationDelay: `${Math.min(i * 0.05, 0.4)}s`
677
- }}
678
- >
679
- {showDateSep && (
680
- <div className="flex items-center gap-4 py-2 mb-2">
681
- <div className="flex-1 h-px bg-white/[0.06]" />
682
- <span className="text-[10px] font-600 text-text-3/50 uppercase tracking-[0.1em]">
683
- {dateSeparator(msg.time)}
684
- </span>
685
- <div className="flex-1 h-px bg-white/[0.06]" />
686
- </div>
687
- )}
688
- <div className={isCurrentMatch ? 'ring-1 ring-amber-400/50 rounded-[16px] bg-amber-400/[0.04]' : isSearchMatch ? 'bg-white/[0.02] rounded-[16px]' : ''}>
689
- <MessageBubble
690
- message={msg}
691
- assistantName={assistantName}
692
- agentAvatarSeed={agent?.avatarSeed}
693
- agentAvatarUrl={agent?.avatarUrl}
694
- agentName={agent?.name}
695
- isLast={isLastAssistant}
696
- onRetry={isLastAssistant ? retryLastMessage : undefined}
697
- messageIndex={originalIndex >= 0 ? originalIndex : undefined}
698
- onToggleBookmark={toggleBookmark}
699
- onEditResend={handleEditResend}
700
- onFork={handleFork}
701
- momentOverlay={momentOverlay}
702
- />
703
- </div>
704
- </div>
705
- )
706
- })}
835
+ {transcriptNodes}
707
836
  <ApprovalCards agentId={agent?.id} />
708
- {streaming && !displayText && !hasVisiblePersistedStreamingMessage && <ThinkingIndicator assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentAvatarUrl={agent?.avatarUrl} agentName={agent?.name} />}
709
- {streaming && displayText && <StreamingBubble text={displayText} assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentAvatarUrl={agent?.avatarUrl} agentName={agent?.name} />}
837
+ <LiveStreamLane
838
+ streaming={streaming}
839
+ hasVisiblePersistedStreamingMessage={hasVisiblePersistedStreamingMessage}
840
+ assistantName={assistantName}
841
+ agentAvatarSeed={agent?.avatarSeed}
842
+ agentAvatarUrl={agent?.avatarUrl}
843
+ agentName={agent?.name}
844
+ />
710
845
  {appSettings.suggestionsEnabled === true && !streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
711
846
  <SuggestionsBar lastMessage={filteredMessages[filteredMessages.length - 1]} onSend={sendMessage} />
712
847
  )}