@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,346 @@
1
+ 'use client'
2
+
3
+ import { memo, useCallback, useState } from 'react'
4
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Types (mirror server-side SwarmSnapshot for the UI)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ type MemberStatus = 'initializing' | 'ready' | 'running' | 'waiting' | 'completed' | 'failed' | 'cancelled' | 'timed_out' | 'spawn_error'
12
+ type SwarmStatus = 'spawning' | 'running' | 'completed' | 'partial' | 'failed'
13
+
14
+ export interface SwarmMemberData {
15
+ index: number
16
+ agentId: string
17
+ agentName: string
18
+ jobId: string
19
+ sessionId: string
20
+ task: string
21
+ status: MemberStatus
22
+ resultPreview: string | null
23
+ error: string | null
24
+ durationMs: number
25
+ }
26
+
27
+ export interface SwarmStatusData {
28
+ swarmId: string
29
+ parentSessionId: string | null
30
+ parentAgentName: string
31
+ parentAgentSeed: string | null
32
+ parentAgentAvatarUrl?: string | null
33
+ status: SwarmStatus
34
+ createdAt: number
35
+ completedAt: number | null
36
+ memberCount: number
37
+ completedCount: number
38
+ failedCount: number
39
+ members: SwarmMemberData[]
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Status config
44
+ // ---------------------------------------------------------------------------
45
+
46
+ const SWARM_STATUS_CONFIG: Record<SwarmStatus, { label: string; color: string; bg: string; border: string }> = {
47
+ spawning: { label: 'Spawning', color: '#818CF8', bg: 'rgba(99,102,241,0.06)', border: 'rgba(99,102,241,0.12)' },
48
+ running: { label: 'Running', color: '#818CF8', bg: 'rgba(99,102,241,0.06)', border: 'rgba(99,102,241,0.12)' },
49
+ completed:{ label: 'All completed', color: '#34D399', bg: 'rgba(52,211,153,0.05)', border: 'rgba(52,211,153,0.12)' },
50
+ partial: { label: 'Partial', color: '#FBBF24', bg: 'rgba(251,191,36,0.05)', border: 'rgba(251,191,36,0.12)' },
51
+ failed: { label: 'Failed', color: '#F43F5E', bg: 'rgba(244,63,94,0.05)', border: 'rgba(244,63,94,0.12)' },
52
+ }
53
+
54
+ const MEMBER_STATUS_CONFIG: Record<MemberStatus, { label: string; color: string; dotColor: string }> = {
55
+ initializing: { label: 'Initializing', color: '#A78BFA', dotColor: '#A78BFA' },
56
+ ready: { label: 'Queued', color: '#818CF8', dotColor: '#818CF8' },
57
+ running: { label: 'Running', color: '#60A5FA', dotColor: '#60A5FA' },
58
+ waiting: { label: 'Waiting', color: '#818CF8', dotColor: '#818CF8' },
59
+ completed: { label: 'Completed', color: '#34D399', dotColor: '#34D399' },
60
+ failed: { label: 'Failed', color: '#F43F5E', dotColor: '#F43F5E' },
61
+ cancelled: { label: 'Cancelled', color: '#6B7280', dotColor: '#6B7280' },
62
+ timed_out: { label: 'Timed out', color: '#F59E0B', dotColor: '#F59E0B' },
63
+ spawn_error: { label: 'Spawn failed', color: '#F43F5E', dotColor: '#F43F5E' },
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Swarm icon
68
+ // ---------------------------------------------------------------------------
69
+
70
+ function SwarmIcon({ size = 16, color = '#818CF8' }: { size?: number; color?: string }) {
71
+ return (
72
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" className="shrink-0">
73
+ {/* Center bee */}
74
+ <circle cx="12" cy="12" r="2.5" fill={color} opacity="0.9" />
75
+ {/* Orbital bees */}
76
+ <circle cx="6" cy="8" r="1.5" fill={color} opacity="0.6" />
77
+ <circle cx="18" cy="8" r="1.5" fill={color} opacity="0.6" />
78
+ <circle cx="6" cy="16" r="1.5" fill={color} opacity="0.6" />
79
+ <circle cx="18" cy="16" r="1.5" fill={color} opacity="0.6" />
80
+ {/* Connection lines */}
81
+ <line x1="8" y1="9" x2="10" y2="11" stroke={color} strokeWidth="0.8" opacity="0.3" />
82
+ <line x1="16" y1="9" x2="14" y2="11" stroke={color} strokeWidth="0.8" opacity="0.3" />
83
+ <line x1="8" y1="15" x2="10" y2="13" stroke={color} strokeWidth="0.8" opacity="0.3" />
84
+ <line x1="16" y1="15" x2="14" y2="13" stroke={color} strokeWidth="0.8" opacity="0.3" />
85
+ </svg>
86
+ )
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Member status dot
91
+ // ---------------------------------------------------------------------------
92
+
93
+ function StatusDot({ status }: { status: MemberStatus }) {
94
+ const cfg = MEMBER_STATUS_CONFIG[status]
95
+ const isActive = status === 'running' || status === 'initializing' || status === 'waiting'
96
+ return (
97
+ <span
98
+ className="shrink-0 rounded-full"
99
+ style={{
100
+ width: 7,
101
+ height: 7,
102
+ backgroundColor: cfg.dotColor,
103
+ ...(isActive ? { animation: 'swarm-pulse 1.5s ease-in-out infinite' } : {}),
104
+ }}
105
+ />
106
+ )
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Member card
111
+ // ---------------------------------------------------------------------------
112
+
113
+ const SwarmMemberCard = memo(function SwarmMemberCard({
114
+ member,
115
+ agents,
116
+ }: {
117
+ member: SwarmMemberData
118
+ agents: Record<string, any>
119
+ }) {
120
+ const [expanded, setExpanded] = useState(false)
121
+ const cfg = MEMBER_STATUS_CONFIG[member.status]
122
+ const agent = agents[member.agentId]
123
+ const avatarSeed = agent?.avatarSeed || member.agentId
124
+ const avatarUrl = agent?.avatarUrl || null
125
+ const hasContent = !!(member.resultPreview || member.error)
126
+
127
+ const formatDuration = (ms: number) => {
128
+ if (ms < 1000) return `${ms}ms`
129
+ return `${(ms / 1000).toFixed(1)}s`
130
+ }
131
+
132
+ return (
133
+ <div
134
+ className="rounded-[10px] overflow-hidden transition-all"
135
+ style={{
136
+ background: 'rgba(255,255,255,0.015)',
137
+ border: `1px solid rgba(255,255,255,0.05)`,
138
+ animation: `swarm-member-in 0.35s cubic-bezier(0.16, 1, 0.3, 1) ${member.index * 80}ms both`,
139
+ }}
140
+ >
141
+ {/* Header */}
142
+ <button
143
+ type="button"
144
+ className="w-full flex items-center gap-2 px-3 py-2 bg-transparent border-none cursor-pointer text-left"
145
+ onClick={() => hasContent && setExpanded(!expanded)}
146
+ disabled={!hasContent}
147
+ >
148
+ <AgentAvatar seed={avatarSeed} avatarUrl={avatarUrl} name={member.agentName} size={20} />
149
+ <StatusDot status={member.status} />
150
+ <div className="flex flex-col gap-0 min-w-0 flex-1">
151
+ <span className="text-[11px] font-600 text-text-2 truncate">
152
+ {member.agentName}
153
+ </span>
154
+ <span className="text-[10px] truncate" style={{ color: cfg.color }}>
155
+ {cfg.label}
156
+ {member.durationMs > 0 && member.status !== 'running' && (
157
+ <span className="text-text-3/50 ml-1">
158
+ {formatDuration(member.durationMs)}
159
+ </span>
160
+ )}
161
+ </span>
162
+ </div>
163
+ {hasContent && (
164
+ <svg
165
+ width="12"
166
+ height="12"
167
+ viewBox="0 0 24 24"
168
+ fill="none"
169
+ stroke="currentColor"
170
+ strokeWidth="2"
171
+ strokeLinecap="round"
172
+ className="shrink-0 text-text-3/30 transition-transform"
173
+ style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
174
+ >
175
+ <polyline points="6 9 12 15 18 9" />
176
+ </svg>
177
+ )}
178
+ </button>
179
+
180
+ {/* Expanded content */}
181
+ {expanded && hasContent && (
182
+ <div
183
+ className="px-3 pb-2.5 pt-0"
184
+ style={{ borderTop: '1px solid rgba(255,255,255,0.04)' }}
185
+ >
186
+ {member.error && (
187
+ <div className="text-[11px] text-rose-400/80 leading-relaxed break-words mt-1.5">
188
+ {member.error}
189
+ </div>
190
+ )}
191
+ {member.resultPreview && !member.error && (
192
+ <div className="text-[11px] text-text-3/70 leading-relaxed break-words mt-1.5 max-h-[120px] overflow-y-auto">
193
+ {member.resultPreview}
194
+ </div>
195
+ )}
196
+ </div>
197
+ )}
198
+ </div>
199
+ )
200
+ })
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Aggregate summary bar
204
+ // ---------------------------------------------------------------------------
205
+
206
+ function SwarmSummaryBar({ data }: { data: SwarmStatusData }) {
207
+ const cfg = SWARM_STATUS_CONFIG[data.status]
208
+ const isTerminal = data.status === 'completed' || data.status === 'partial' || data.status === 'failed'
209
+
210
+ const formatDuration = (ms: number) => {
211
+ if (ms < 1000) return `${ms}ms`
212
+ if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`
213
+ return `${(ms / 60_000).toFixed(1)}m`
214
+ }
215
+
216
+ const durationMs = data.completedAt
217
+ ? data.completedAt - data.createdAt
218
+ : Date.now() - data.createdAt
219
+
220
+ return (
221
+ <div
222
+ className="flex items-center gap-2 px-3 py-2 rounded-[8px]"
223
+ style={{ background: `${cfg.color}08` }}
224
+ >
225
+ <span className="text-[11px] font-700" style={{ color: cfg.color }}>
226
+ {data.completedCount}/{data.memberCount} completed
227
+ </span>
228
+ {data.failedCount > 0 && (
229
+ <span className="text-[11px] font-600 text-rose-400/70">
230
+ {data.failedCount} failed
231
+ </span>
232
+ )}
233
+ {isTerminal && (
234
+ <span className="text-[10px] text-text-3/40 ml-auto">
235
+ {formatDuration(durationMs)}
236
+ </span>
237
+ )}
238
+ {!isTerminal && (
239
+ <span
240
+ className="text-[10px] ml-auto"
241
+ style={{ color: cfg.color, opacity: 0.6 }}
242
+ >
243
+ in progress...
244
+ </span>
245
+ )}
246
+ </div>
247
+ )
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // Main component
252
+ // ---------------------------------------------------------------------------
253
+
254
+ export const SwarmStatusCard = memo(function SwarmStatusCard({
255
+ data,
256
+ }: {
257
+ data: SwarmStatusData
258
+ }) {
259
+ const cfg = SWARM_STATUS_CONFIG[data.status]
260
+ const agents = useAppStore((s) => s.agents || {})
261
+ const isActive = data.status === 'spawning' || data.status === 'running'
262
+
263
+ return (
264
+ <div
265
+ className="rounded-[14px] overflow-hidden"
266
+ style={{
267
+ background: cfg.bg,
268
+ border: `1px solid ${cfg.border}`,
269
+ animation: 'delegation-handoff-in 0.45s cubic-bezier(0.16, 1, 0.3, 1)',
270
+ }}
271
+ >
272
+ {/* Header */}
273
+ <div
274
+ className="flex items-center gap-2.5 px-4 py-3"
275
+ style={{
276
+ borderBottom: `1px solid ${cfg.border}`,
277
+ }}
278
+ >
279
+ <SwarmIcon size={18} color={cfg.color} />
280
+ <div className="shrink-0">
281
+ <AgentAvatar
282
+ seed={data.parentAgentSeed}
283
+ avatarUrl={data.parentAgentAvatarUrl}
284
+ name={data.parentAgentName}
285
+ size={22}
286
+ />
287
+ </div>
288
+ <div className="flex flex-col gap-0 min-w-0 flex-1">
289
+ <span className="text-[12px] font-700" style={{ color: cfg.color }}>
290
+ Swarm spawned by {data.parentAgentName}
291
+ </span>
292
+ <span className="text-[10px] text-text-3/50">
293
+ {data.memberCount} agent{data.memberCount !== 1 ? 's' : ''}
294
+ {' · '}
295
+ {new Date(data.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
296
+ </span>
297
+ </div>
298
+ {isActive && (
299
+ <span
300
+ className="shrink-0 w-2 h-2 rounded-full"
301
+ style={{
302
+ backgroundColor: cfg.color,
303
+ animation: 'swarm-pulse 1.5s ease-in-out infinite',
304
+ }}
305
+ />
306
+ )}
307
+ </div>
308
+
309
+ {/* Members grid */}
310
+ <div className="px-3 py-2.5 flex flex-col gap-1.5">
311
+ {data.members.map((member) => (
312
+ <SwarmMemberCard
313
+ key={member.index}
314
+ member={member}
315
+ agents={agents}
316
+ />
317
+ ))}
318
+ </div>
319
+
320
+ {/* Summary bar */}
321
+ <div className="px-3 pb-3">
322
+ <SwarmSummaryBar data={data} />
323
+ </div>
324
+ </div>
325
+ )
326
+ })
327
+
328
+ // ---------------------------------------------------------------------------
329
+ // CSS keyframes (inject once)
330
+ // ---------------------------------------------------------------------------
331
+
332
+ const SWARM_STYLES = `
333
+ @keyframes swarm-pulse {
334
+ 0%, 100% { opacity: 1; transform: scale(1); }
335
+ 50% { opacity: 0.5; transform: scale(0.85); }
336
+ }
337
+ @keyframes swarm-member-in {
338
+ from { opacity: 0; transform: translateY(6px) scale(0.97); }
339
+ to { opacity: 1; transform: translateY(0) scale(1); }
340
+ }
341
+ `
342
+
343
+ /** Call this once in a layout or the component tree root */
344
+ export function SwarmStatusStyles() {
345
+ return <style dangerouslySetInnerHTML={{ __html: SWARM_STYLES }} />
346
+ }
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useState, useMemo } from 'react'
3
+ import { memo, useMemo, useState } from 'react'
4
4
  import type { ToolEvent } from '@/stores/use-chat-store'
5
5
  import { useChatStore } from '@/stores/use-chat-store'
6
6
  import { useAppStore } from '@/stores/use-app-store'
@@ -19,6 +19,7 @@ const TOOL_COLORS: Record<string, string> = {
19
19
  create_spreadsheet: '#10B981',
20
20
  web_search: '#3B82F6',
21
21
  web_fetch: '#3B82F6',
22
+ spawn_subagent: '#8B5CF6',
22
23
  delegate_to_agent: '#6366F1',
23
24
  check_delegation_status: '#6366F1',
24
25
  delegate_to_claude_code: '#6366F1',
@@ -72,6 +73,7 @@ export const TOOL_LABELS: Record<string, string> = {
72
73
  claude_code: 'Claude Code',
73
74
  codex_cli: 'Codex CLI',
74
75
  opencode_cli: 'OpenCode CLI',
76
+ spawn_subagent: 'Subagent',
75
77
  delegate_to_agent: 'Agent Delegation',
76
78
  check_delegation_status: 'Check Delegation',
77
79
  delegate_to_claude_code: 'Claude Code',
@@ -110,6 +112,7 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
110
112
  claude_code: 'Enable delegation to Claude Code CLI',
111
113
  codex_cli: 'Enable delegation to OpenAI Codex CLI',
112
114
  opencode_cli: 'Enable delegation to OpenCode CLI',
115
+ spawn_subagent: 'Spawn native subagents with lineage tracking and batch support',
113
116
  delegate_to_agent: 'Delegate a task to another agent',
114
117
  check_delegation_status: 'Check the status of a delegated task',
115
118
  delegate_to_claude_code: 'Delegate complex coding tasks to Claude Code',
@@ -210,7 +213,7 @@ function formatToolOutput(toolName: string, raw: string): string {
210
213
  }
211
214
 
212
215
  /** Extract a human-readable preview from tool input */
213
- function getInputPreview(name: string, input: string): string {
216
+ export function getInputPreview(name: string, input: string): string {
214
217
  try {
215
218
  let parsed = JSON.parse(input)
216
219
  // Unwrap LangChain's { input: ... } wrapper
@@ -262,6 +265,25 @@ function getInputPreview(name: string, input: string): string {
262
265
  }
263
266
  }
264
267
 
268
+ export function getToolLabel(name: string, input: string): string {
269
+ if (name === 'browser') {
270
+ try {
271
+ let parsed = JSON.parse(input)
272
+ if (parsed?.input && Object.keys(parsed).length === 1) {
273
+ const inner = typeof parsed.input === 'string' ? JSON.parse(parsed.input) : parsed.input
274
+ if (typeof inner === 'object' && inner !== null) parsed = inner
275
+ }
276
+ const action = parsed?.action || ''
277
+ const sub = BROWSER_ACTION_LABELS[action]
278
+ return sub ? `Browser · ${sub}` : 'Browser'
279
+ } catch {
280
+ return 'Browser'
281
+ }
282
+ }
283
+
284
+ return TOOL_LABELS[name] || name.replace(/_/g, ' ')
285
+ }
286
+
265
287
  /** Extract embedded images, videos, PDFs, and file links from tool output */
266
288
  export function extractMedia(output: string): { images: string[]; videos: string[]; pdfs: { name: string; url: string }[]; files: { name: string; url: string }[]; cleanText: string } {
267
289
  const images: string[] = []
@@ -364,29 +386,14 @@ function TimeoutQuickFix({ event }: { event: ToolEvent }) {
364
386
  )
365
387
  }
366
388
 
367
- export function ToolCallBubble({ event }: { event: ToolEvent }) {
389
+ export const ToolCallBubble = memo(function ToolCallBubble({ event }: { event: ToolEvent }) {
368
390
  const [imgExpanded, setImgExpanded] = useState(false)
369
391
  const isError = event.status === 'error'
370
392
  const color = isError ? '#F43F5E' : (TOOL_COLORS[event.name] || '#6366F1')
371
393
  const isRunning = event.status === 'running'
394
+ const statusLabel = isRunning ? 'Running' : (isError ? 'Failed' : 'Done')
372
395
 
373
- // For browser tool, extract the action to show a more specific label
374
- const label = useMemo(() => {
375
- if (event.name === 'browser') {
376
- try {
377
- let parsed = JSON.parse(event.input)
378
- // Unwrap LangChain {input: "..."} wrapper — inner value is a stringified JSON
379
- if (parsed?.input && Object.keys(parsed).length === 1) {
380
- const inner = typeof parsed.input === 'string' ? JSON.parse(parsed.input) : parsed.input
381
- if (typeof inner === 'object' && inner !== null) parsed = inner
382
- }
383
- const action = parsed?.action || ''
384
- const sub = BROWSER_ACTION_LABELS[action]
385
- return sub ? `Browser · ${sub}` : 'Browser'
386
- } catch { return 'Browser' }
387
- }
388
- return TOOL_LABELS[event.name] || event.name.replace(/_/g, ' ')
389
- }, [event.name, event.input])
396
+ const label = useMemo(() => getToolLabel(event.name, event.input), [event.input, event.name])
390
397
 
391
398
  const inputPreview = useMemo(() => getInputPreview(event.name, event.input), [event.name, event.input])
392
399
  const formattedInput = useMemo(() => formatJson(event.input), [event.input])
@@ -422,9 +429,15 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
422
429
  }
423
430
 
424
431
  return (
425
- <div className="w-full text-left">
426
- <details open={isError || isRunning || undefined} className="group/tool">
432
+ <div
433
+ className="w-full text-left"
434
+ data-testid="tool-call-card"
435
+ data-tool-name={event.name}
436
+ data-tool-status={event.status}
437
+ >
438
+ <details open={isError || isRunning} className="group/tool">
427
439
  <summary
440
+ data-testid="tool-call-summary"
428
441
  className="w-full text-left rounded-[12px] border bg-surface/80 backdrop-blur-sm transition-all duration-200 hover:bg-surface-2 cursor-pointer list-none [&::-webkit-details-marker]:hidden"
429
442
  style={{ borderLeft: `3px solid ${color}`, borderColor: `${color}33` }}
430
443
  >
@@ -462,6 +475,17 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
462
475
  {inputPreview}
463
476
  </span>
464
477
  )}
478
+ <span
479
+ className={`shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-600 uppercase tracking-[0.08em] ${
480
+ isRunning
481
+ ? 'border-current/20 bg-white/[0.05] text-text-3'
482
+ : isError
483
+ ? 'border-rose-500/25 bg-rose-500/10 text-rose-300'
484
+ : 'border-emerald-500/25 bg-emerald-500/10 text-emerald-300'
485
+ }`}
486
+ >
487
+ {statusLabel}
488
+ </span>
465
489
  {hasMedia && (
466
490
  <span className="text-[10px] text-text-3/50 font-500 shrink-0 group-open/tool:hidden">
467
491
  {media.images.length > 0 && `${media.images.length} image${media.images.length > 1 ? 's' : ''}`}
@@ -504,6 +528,7 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
504
528
  <div className="mt-2 flex flex-col gap-2">
505
529
  {media.images.map((src, i) => (
506
530
  <div key={i} className="relative group/img">
531
+ {/* eslint-disable-next-line @next/next/no-img-element */}
507
532
  <img
508
533
  src={src}
509
534
  alt={`Screenshot ${i + 1}`}
@@ -605,4 +630,4 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
605
630
  )}
606
631
  </div>
607
632
  )
608
- }
633
+ })
@@ -3,12 +3,14 @@
3
3
  import { useEffect, useCallback, useMemo, useState } from 'react'
4
4
  import { useChatroomStore } from '@/stores/use-chatroom-store'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
+ import { useNow } from '@/hooks/use-now'
6
7
  import { useWs } from '@/hooks/use-ws'
7
8
  import type { Chatroom } from '@/types'
8
9
  import { EmptyState } from '@/components/shared/empty-state'
9
10
 
10
- function formatRoomTime(ts: number): string {
11
- const diff = Date.now() - ts
11
+ function formatRoomTime(ts: number, now: number | null): string {
12
+ if (!now) return 'recently'
13
+ const diff = now - ts
12
14
  if (diff < 60_000) return 'Now'
13
15
  if (diff < 3_600_000) return `${Math.max(1, Math.floor(diff / 60_000))}m`
14
16
  if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h`
@@ -16,6 +18,7 @@ function formatRoomTime(ts: number): string {
16
18
  }
17
19
 
18
20
  export function ChatroomList() {
21
+ const now = useNow()
19
22
  const chatrooms = useChatroomStore((s) => s.chatrooms)
20
23
  const currentChatroomId = useChatroomStore((s) => s.currentChatroomId)
21
24
  const loadChatrooms = useChatroomStore((s) => s.loadChatrooms)
@@ -72,15 +75,15 @@ export function ChatroomList() {
72
75
 
73
76
  const filtered = useMemo(() => {
74
77
  const query = search.trim().toLowerCase()
75
- const now = Date.now()
76
78
  return enriched.filter((item) => {
77
79
  if (query && !item.searchText.includes(query)) return false
80
+ if (!now) return filter === 'unread' ? item.unreadCount > 0 : true
78
81
  if (filter === 'active') return now - item.chatroom.updatedAt < 3_600_000
79
82
  if (filter === 'recent') return now - item.chatroom.updatedAt < 86_400_000
80
83
  if (filter === 'unread') return item.unreadCount > 0
81
84
  return true
82
85
  })
83
- }, [enriched, filter, search])
86
+ }, [enriched, filter, now, search])
84
87
 
85
88
  return (
86
89
  <div className="flex-1 overflow-y-auto">
@@ -170,7 +173,7 @@ export function ChatroomList() {
170
173
  </span>
171
174
  )}
172
175
  <span className="ml-auto shrink-0 text-[10px] font-mono text-text-3/55">
173
- {formatRoomTime(lastMsg?.time || chatroom.updatedAt)}
176
+ {formatRoomTime(lastMsg?.time || chatroom.updatedAt, now)}
174
177
  </span>
175
178
  </div>
176
179
  <div className="mt-0.5 flex flex-wrap items-center gap-2 text-[11px] text-text-3">
@@ -5,6 +5,7 @@ import ReactMarkdown from 'react-markdown'
5
5
  import remarkGfm from 'remark-gfm'
6
6
  import rehypeHighlight from 'rehype-highlight'
7
7
  import { AgentAvatar } from '@/components/agents/agent-avatar'
8
+ import { useNow } from '@/hooks/use-now'
8
9
  import { CodeBlock } from '@/components/chat/code-block'
9
10
  import { ReactionPicker } from './reaction-picker'
10
11
  import { ReplyQuote } from '@/components/shared/reply-quote'
@@ -40,8 +41,8 @@ interface Props {
40
41
  momentOverlay?: React.ReactNode
41
42
  }
42
43
 
43
- function formatRelativeTime(ts: number): string {
44
- const now = Date.now()
44
+ function formatRelativeTime(ts: number, now: number | null): string {
45
+ if (!now) return 'recently'
45
46
  const diffSec = Math.floor((now - ts) / 1000)
46
47
  if (diffSec < 60) return 'just now'
47
48
  const diffMin = Math.floor(diffSec / 60)
@@ -62,11 +63,11 @@ function getMemberRoleFromChatroom(chatroom: Chatroom | undefined, agentId: stri
62
63
  return member?.role || 'member'
63
64
  }
64
65
 
65
- function isAgentMutedInChatroom(chatroom: Chatroom | undefined, agentId: string): boolean {
66
+ function isAgentMutedInChatroom(chatroom: Chatroom | undefined, agentId: string, now: number | null): boolean {
66
67
  if (!chatroom?.members?.length) return false
67
68
  const member = chatroom.members.find((m) => m.agentId === agentId)
68
69
  if (!member?.mutedUntil) return false
69
- return new Date(member.mutedUntil).getTime() > Date.now()
70
+ return !!now && new Date(member.mutedUntil).getTime() > now
70
71
  }
71
72
 
72
73
  function roleBadgeStyle(role: string): { label: string; className: string } | null {
@@ -136,6 +137,7 @@ function renderChatroomAttachments(message: ChatroomMessage) {
136
137
  }
137
138
 
138
139
  export function ChatroomMessageBubble({ message, agents, onToggleReaction, onReply, onTogglePin, onTransfer, onDeleteMessage, onMuteAgent, onUnmuteAgent, onSetRole, chatroom, pinnedMessageIds, streamingAgentIds, messages, grouped: isGrouped, momentOverlay }: Props) {
140
+ const now = useNow({ enabled: false })
139
141
  const [showPicker, setShowPicker] = useState(false)
140
142
  const [showTransferPicker, setShowTransferPicker] = useState(false)
141
143
  const [showModMenu, setShowModMenu] = useState(false)
@@ -213,7 +215,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
213
215
  {!isGrouped && (() => {
214
216
  const role = !isUser ? getMemberRoleFromChatroom(chatroom, message.senderId) : 'member'
215
217
  const badge = !isUser ? roleBadgeStyle(role) : null
216
- const muted = !isUser ? isAgentMutedInChatroom(chatroom, message.senderId) : false
218
+ const muted = !isUser ? isAgentMutedInChatroom(chatroom, message.senderId, now) : false
217
219
  return (
218
220
  <div className="flex items-baseline gap-2 mb-0.5">
219
221
  {!isUser && agent ? (
@@ -241,7 +243,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
241
243
  Muted
242
244
  </span>
243
245
  )}
244
- <span className="label-mono" title={new Date(message.time).toLocaleString()}>{formatRelativeTime(message.time)}</span>
246
+ <span className="label-mono" title={new Date(message.time).toISOString()}>{formatRelativeTime(message.time, now)}</span>
245
247
  </div>
246
248
  )
247
249
  })()}
@@ -352,6 +354,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
352
354
  }
353
355
  return (
354
356
  <a href={src} download target="_blank" rel="noopener noreferrer" className="block my-2">
357
+ {/* eslint-disable-next-line @next/next/no-img-element */}
355
358
  <img src={src} alt={alt || 'Image'} loading="lazy" className="max-w-full max-h-[400px] rounded-[8px] border border-white/[0.06]" onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
356
359
  </a>
357
360
  )
@@ -476,7 +479,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
476
479
  </button>
477
480
  )}
478
481
  {onMuteAgent && onUnmuteAgent && (() => {
479
- const muted = isAgentMutedInChatroom(chatroom, message.senderId)
482
+ const muted = isAgentMutedInChatroom(chatroom, message.senderId, now)
480
483
  return muted ? (
481
484
  <button
482
485
  onClick={() => {