@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
@@ -4,6 +4,7 @@ import { useEffect, useRef, useCallback, useState, useMemo } from 'react'
4
4
  import { useChatroomStore } from '@/stores/use-chatroom-store'
5
5
  import type { StreamingAgent } from '@/stores/use-chatroom-store'
6
6
  import { useAppStore } from '@/stores/use-app-store'
7
+ import { useNow } from '@/hooks/use-now'
7
8
  import { useWs } from '@/hooks/use-ws'
8
9
  import { ChatroomMessageBubble } from './chatroom-message'
9
10
  import { ChatroomInput } from './chatroom-input'
@@ -35,10 +36,10 @@ function getMemberRole(chatroom: Chatroom, agentId: string): string {
35
36
  return member?.role || 'member'
36
37
  }
37
38
 
38
- function isAgentMuted(chatroom: Chatroom, agentId: string): boolean {
39
+ function isAgentMuted(chatroom: Chatroom, agentId: string, now: number | null): boolean {
39
40
  const member = getMemberFromChatroom(chatroom, agentId)
40
41
  if (!member?.mutedUntil) return false
41
- return new Date(member.mutedUntil).getTime() > Date.now()
42
+ return !!now && new Date(member.mutedUntil).getTime() > now
42
43
  }
43
44
 
44
45
  type MomentType = { kind: 'heartbeat' } | { kind: 'tool'; name: string; input: string }
@@ -69,10 +70,11 @@ function AgentHeartbeatListeners({ agentIds, onPulse }: { agentIds: string[]; on
69
70
 
70
71
  const GROUP_THRESHOLD_MS = 2 * 60 * 1000
71
72
 
72
- function dayLabel(ts: number): string {
73
+ function dayLabel(ts: number, now: number | null): string {
73
74
  const d = new Date(ts)
74
- const now = new Date()
75
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
75
+ if (!now) return d.toISOString().slice(0, 10)
76
+ const nowDate = new Date(now)
77
+ const today = new Date(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate())
76
78
  const msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate())
77
79
  const diff = today.getTime() - msgDay.getTime()
78
80
  if (diff === 0) return 'Today'
@@ -81,6 +83,7 @@ function dayLabel(ts: number): string {
81
83
  }
82
84
 
83
85
  export function ChatroomView() {
86
+ const now = useNow()
84
87
  const currentChatroomId = useChatroomStore((s) => s.currentChatroomId)
85
88
  const chatrooms = useChatroomStore((s) => s.chatrooms)
86
89
  const streamingAgents = useChatroomStore((s) => s.streamingAgents)
@@ -165,7 +168,7 @@ export function ChatroomView() {
165
168
  : []
166
169
  ), [chatroom, pinnedIds])
167
170
  const memberAgentIds = chatroom?.agentIds || []
168
- const mutedCount = chatroom ? chatroom.agentIds.filter((agentId) => isAgentMuted(chatroom, agentId)).length : 0
171
+ const mutedCount = chatroom ? chatroom.agentIds.filter((agentId) => isAgentMuted(chatroom, agentId, now)).length : 0
169
172
  const adminCount = chatroom ? chatroom.agentIds.filter((agentId) => getMemberRole(chatroom, agentId) === 'admin').length : 0
170
173
  const lastReadAt = chatroom ? (lastReadTimestamps[chatroom.id] || 0) : 0
171
174
  const unreadCount = useMemo(() => (
@@ -277,7 +280,7 @@ export function ChatroomView() {
277
280
  {memberAgents.slice(0, 5).map((agent) => {
278
281
  const role = getMemberRole(chatroom, agent.id)
279
282
  const badge = getRoleBadge(role)
280
- const muted = isAgentMuted(chatroom, agent.id)
283
+ const muted = isAgentMuted(chatroom, agent.id, Date.now())
281
284
  return (
282
285
  <Tooltip key={agent.id}>
283
286
  <TooltipTrigger asChild>
@@ -411,7 +414,7 @@ export function ChatroomView() {
411
414
  {showDaySep && (
412
415
  <div className="flex items-center gap-3 px-4 py-3">
413
416
  <div className="flex-1 h-px bg-white/[0.06]" />
414
- <span className="text-[10px] font-600 text-text-3 uppercase tracking-wider">{dayLabel(msg.time)}</span>
417
+ <span className="text-[10px] font-600 text-text-3 uppercase tracking-wider">{dayLabel(msg.time, now)}</span>
415
418
  <div className="flex-1 h-px bg-white/[0.06]" />
416
419
  </div>
417
420
  )}
@@ -540,7 +543,7 @@ function RoomDetailsPanel({
540
543
  <div className="space-y-2">
541
544
  {memberAgents.map((agent) => {
542
545
  const role = getMemberRole(chatroom, agent.id)
543
- const muted = isAgentMuted(chatroom, agent.id)
546
+ const muted = isAgentMuted(chatroom, agent.id, Date.now())
544
547
  return (
545
548
  <button
546
549
  key={agent.id}
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useEffect } from 'react'
4
4
  import { api } from '@/lib/api-client'
5
+ import { useNow } from '@/hooks/use-now'
5
6
  import type { ConnectorHealthEvent, ConnectorHealthEventType } from '@/types'
6
7
 
7
8
  interface HealthResponse {
@@ -17,10 +18,10 @@ const EVENT_CONFIG: Record<ConnectorHealthEventType, { color: string; label: str
17
18
  disconnected: { color: 'bg-amber-400', label: 'Disconnected' },
18
19
  }
19
20
 
20
- function formatTimestamp(ts: string): string {
21
+ function formatTimestamp(ts: string, now: number | null): string {
21
22
  const d = new Date(ts)
22
- const now = new Date()
23
- const diffMs = now.getTime() - d.getTime()
23
+ if (!now) return 'recently'
24
+ const diffMs = now - d.getTime()
24
25
  const diffMin = Math.floor(diffMs / 60_000)
25
26
  const diffHr = Math.floor(diffMs / 3_600_000)
26
27
  const diffDay = Math.floor(diffMs / 86_400_000)
@@ -39,6 +40,7 @@ function uptimeBadgeColor(pct: number): string {
39
40
  }
40
41
 
41
42
  export function ConnectorHealth({ connectorId }: { connectorId: string }) {
43
+ const now = useNow()
42
44
  const [data, setData] = useState<HealthResponse | null>(null)
43
45
  const [loading, setLoading] = useState(true)
44
46
 
@@ -104,7 +106,7 @@ export function ConnectorHealth({ connectorId }: { connectorId: string }) {
104
106
  <div className="min-w-0 flex-1">
105
107
  <div className="flex items-center gap-2">
106
108
  <span className="text-[13px] font-600 text-text-2">{cfg.label}</span>
107
- <span className="text-[11px] text-text-3">{formatTimestamp(ev.timestamp)}</span>
109
+ <span className="text-[11px] text-text-3">{formatTimestamp(ev.timestamp, now)}</span>
108
110
  </div>
109
111
  {ev.message && (
110
112
  <p className="text-[12px] text-text-3/70 mt-0.5 leading-[1.4] break-words">{ev.message}</p>
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { useChatroomStore } from '@/stores/use-chatroom-store'
6
6
  import { useWs } from '@/hooks/use-ws'
7
+ import { useMountedRef } from '@/hooks/use-mounted-ref'
7
8
  import { api } from '@/lib/api-client'
8
9
  import type { Connector } from '@/types'
9
10
  import {
@@ -55,6 +56,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
55
56
  const [loaded, setLoaded] = useState(false)
56
57
  const [error, setError] = useState<string | null>(null)
57
58
  const [groupFilter, setGroupFilter] = useState<'all' | ConnectorGroup>('all')
59
+ const mountedRef = useMountedRef()
58
60
  const openConnector = useCallback((id: string | null) => {
59
61
  setEditingConnectorId(id)
60
62
  setConnectorSheetOpen(true)
@@ -62,9 +64,8 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
62
64
 
63
65
  const refresh = useCallback(async () => {
64
66
  await Promise.all([loadConnectors(), loadAgents(), loadChatrooms()])
65
- setLoaded(true)
66
- // eslint-disable-next-line react-hooks/exhaustive-deps
67
- }, [loadConnectors, loadAgents])
67
+ if (mountedRef.current) setLoaded(true)
68
+ }, [loadConnectors, loadAgents, loadChatrooms, mountedRef])
68
69
 
69
70
  useEffect(() => { void refresh() }, [refresh])
70
71
  useWs('connectors', loadConnectors, 15_000)
@@ -77,34 +78,38 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
77
78
  const handleToggle = async (e: React.MouseEvent, c: Connector) => {
78
79
  e.stopPropagation()
79
80
  const action = c.status === 'running' ? 'stop' : 'start'
80
- setToggling(c.id)
81
- setError(null)
81
+ if (mountedRef.current) {
82
+ setToggling(c.id)
83
+ setError(null)
84
+ }
82
85
  try {
83
86
  await api('PUT', `/connectors/${c.id}`, { action })
84
87
  await refresh()
85
88
  } catch (err: unknown) {
86
89
  const msg = err instanceof Error && err.message ? err.message : `Failed to ${action}`
87
- setError(msg)
90
+ if (mountedRef.current) setError(msg)
88
91
  await refresh()
89
92
  } finally {
90
- setToggling(null)
93
+ if (mountedRef.current) setToggling(null)
91
94
  }
92
95
  }
93
96
 
94
97
  const handleReconnect = async (e: React.MouseEvent, c: Connector) => {
95
98
  e.stopPropagation()
96
- setReconnecting(c.id)
97
- setError(null)
99
+ if (mountedRef.current) {
100
+ setReconnecting(c.id)
101
+ setError(null)
102
+ }
98
103
  try {
99
104
  try { await api('PUT', `/connectors/${c.id}`, { action: 'stop' }) } catch { /* may already be stopped */ }
100
105
  await api('PUT', `/connectors/${c.id}`, { action: 'start' })
101
106
  await refresh()
102
107
  } catch (err: unknown) {
103
108
  const msg = err instanceof Error && err.message ? err.message : 'Failed to reconnect'
104
- setError(msg)
109
+ if (mountedRef.current) setError(msg)
105
110
  await refresh()
106
111
  } finally {
107
- setReconnecting(null)
112
+ if (mountedRef.current) setReconnecting(null)
108
113
  }
109
114
  }
110
115
 
@@ -16,6 +16,7 @@ import { HintTip } from '@/components/shared/hint-tip'
16
16
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
17
17
  import { useChatroomStore } from '@/stores/use-chatroom-store'
18
18
  import { ConnectorHealth } from '@/components/connectors/connector-health'
19
+ import { errorMessage } from '@/lib/shared-utils'
19
20
 
20
21
  /** Auto-detect URLs in text and make them clickable links that open in a new tab */
21
22
  function linkify(text: string) {
@@ -489,6 +490,7 @@ export function ConnectorSheet() {
489
490
  const connectors = useAppStore((s) => s.connectors)
490
491
  const loadConnectors = useAppStore((s) => s.loadConnectors)
491
492
  const agents = useAppStore((s) => s.agents)
493
+ const appSettings = useAppStore((s) => s.appSettings)
492
494
  const credentials = useAppStore((s) => s.credentials)
493
495
  const loadAgents = useAppStore((s) => s.loadAgents)
494
496
  const loadCredentials = useAppStore((s) => s.loadCredentials)
@@ -520,6 +522,10 @@ export function ConnectorSheet() {
520
522
  const [confirmDelete, setConfirmDelete] = useState(false)
521
523
  const [confirmWhatsAppAction, setConfirmWhatsAppAction] = useState<'unlink' | 'repair' | null>(null)
522
524
  const [deleting, setDeleting] = useState(false)
525
+ const localAllowlistCount = config.allowFrom ? config.allowFrom.split(',').map((entry) => entry.trim()).filter(Boolean).length : 0
526
+ const globalWhatsAppAllowlistCount = platform === 'whatsapp' && Array.isArray(appSettings.whatsappApprovedContacts)
527
+ ? appSettings.whatsappApprovedContacts.length
528
+ : 0
523
529
 
524
530
  const editing = editingId ? connectors[editingId] as Connector | undefined : null
525
531
 
@@ -640,7 +646,7 @@ export function ConnectorSheet() {
640
646
  setOpen(false)
641
647
  setEditingId(null)
642
648
  } catch (err: unknown) {
643
- toast.error(err instanceof Error ? err.message : String(err))
649
+ toast.error(errorMessage(err))
644
650
  } finally {
645
651
  setSaving(false)
646
652
  }
@@ -665,7 +671,7 @@ export function ConnectorSheet() {
665
671
  await loadConnectors()
666
672
  } catch (err: unknown) {
667
673
  setWaConnecting(false)
668
- toast.error(`Failed to ${action}: ${err instanceof Error ? err.message : String(err)}`)
674
+ toast.error(`Failed to ${action}: ${errorMessage(err)}`)
669
675
  } finally {
670
676
  setActionLoading(false)
671
677
  }
@@ -681,7 +687,7 @@ export function ConnectorSheet() {
681
687
  setOpen(false)
682
688
  setEditingId(null)
683
689
  } catch (err: unknown) {
684
- toast.error(`Failed to delete connector: ${err instanceof Error ? err.message : String(err)}`)
690
+ toast.error(`Failed to delete connector: ${errorMessage(err)}`)
685
691
  } finally {
686
692
  setDeleting(false)
687
693
  }
@@ -699,7 +705,7 @@ export function ConnectorSheet() {
699
705
  setConfirmWhatsAppAction(null)
700
706
  await loadConnectors()
701
707
  } catch (err: unknown) {
702
- toast.error(`Failed to ${mode === 'unlink' ? 'unlink' : 're-pair'}: ${err instanceof Error ? err.message : String(err)}`)
708
+ toast.error(`Failed to ${mode === 'unlink' ? 'unlink' : 're-pair'}: ${errorMessage(err)}`)
703
709
  } finally {
704
710
  setActionLoading(false)
705
711
  }
@@ -1051,7 +1057,7 @@ export function ConnectorSheet() {
1051
1057
  setNewCredName('')
1052
1058
  setNewCredValue('')
1053
1059
  } catch (err: unknown) {
1054
- toast.error(`Failed to save: ${err instanceof Error ? err.message : String(err)}`)
1060
+ toast.error(`Failed to save: ${errorMessage(err)}`)
1055
1061
  } finally {
1056
1062
  setSavingCred(false)
1057
1063
  }
@@ -1158,7 +1164,7 @@ export function ConnectorSheet() {
1158
1164
  · Debounce: <span className="text-text-2">{doctorPolicy.inboundDebounceMs ?? 700}ms</span>
1159
1165
  </div>
1160
1166
  <div className="rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-[12px] text-text-3/80">
1161
- Allowlist: <span className="text-text-2">{config.allowFrom ? config.allowFrom.split(',').map((entry) => entry.trim()).filter(Boolean).length : 0}</span>{' '}
1167
+ Allowlist: <span className="text-text-2">{localAllowlistCount + globalWhatsAppAllowlistCount}</span>{' '}
1162
1168
  · Reactions: <span className="text-text-2">{doctorPolicy.statusReactions === false ? 'off' : 'on'}</span>{' '}
1163
1169
  · Typing: <span className="text-text-2">{doctorPolicy.typingIndicators === false ? 'off' : 'on'}</span>
1164
1170
  </div>
@@ -5,6 +5,8 @@ import { AreaChart, Area, ResponsiveContainer, Tooltip } from 'recharts'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
7
7
  import { AgentAvatar } from '@/components/agents/agent-avatar'
8
+ import { useMountedRef } from '@/hooks/use-mounted-ref'
9
+ import { useNow } from '@/hooks/use-now'
8
10
  import { api } from '@/lib/api-client'
9
11
  import { isLocalhostBrowser, isVisibleSessionForViewer } from '@/lib/local-observability'
10
12
  import { getSessionLastMessage } from '@/lib/session-summary'
@@ -12,8 +14,9 @@ import { getNotificationActivityAt, getNotificationOccurrenceCount } from '@/lib
12
14
  import type { Agent, Session, ActivityEntry, BoardTask, AppNotification } from '@/types'
13
15
  import { HintTip } from '@/components/shared/hint-tip'
14
16
 
15
- function timeAgo(ts: number): string {
16
- const diff = Date.now() - ts
17
+ function timeAgo(ts: number, now: number | null): string {
18
+ if (!now) return 'recently'
19
+ const diff = now - ts
17
20
  const mins = Math.floor(diff / 60000)
18
21
  if (mins < 1) return 'just now'
19
22
  if (mins < 60) return `${mins}m ago`
@@ -23,8 +26,9 @@ function timeAgo(ts: number): string {
23
26
  return `${days}d ago`
24
27
  }
25
28
 
26
- function timeUntil(ts: number): string {
27
- const diff = ts - Date.now()
29
+ function timeUntil(ts: number, now: number | null): string {
30
+ if (!now) return 'soon'
31
+ const diff = ts - now
28
32
  if (diff <= 0) return 'now'
29
33
  const mins = Math.floor(diff / 60000)
30
34
  if (mins < 60) return `in ${mins}m`
@@ -69,6 +73,7 @@ const PLATFORM_LABELS: Record<string, string> = {
69
73
  }
70
74
 
71
75
  export function HomeView() {
76
+ const now = useNow()
72
77
  const agents = useAppStore((s) => s.agents)
73
78
  const sessions = useAppStore((s) => s.sessions)
74
79
  const currentUser = useAppStore((s) => s.currentUser)
@@ -89,9 +94,14 @@ export function HomeView() {
89
94
  const setCurrentSession = useAppStore((s) => s.setCurrentSession)
90
95
  const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
91
96
  const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
92
- const setMessages = useChatStore((s) => s.setMessages)
93
97
  const [todayCost, setTodayCost] = useState(0)
94
98
  const [costTrend, setCostTrend] = useState<{ cost: number; bucket: string }[]>([])
99
+ const [localhostBrowser, setLocalhostBrowser] = useState(false)
100
+ const mountedRef = useMountedRef()
101
+
102
+ useEffect(() => {
103
+ setLocalhostBrowser(isLocalhostBrowser())
104
+ }, [])
95
105
 
96
106
  const allAgents = Object.values(agents).filter((a) => !a.trashedAt)
97
107
  const pinnedAgents = allAgents.filter((a) => a.pinned)
@@ -99,10 +109,10 @@ export function HomeView() {
99
109
  const recentChats = useMemo(
100
110
  () =>
101
111
  Object.values(sessions)
102
- .filter((session) => isVisibleSessionForViewer(session, currentUser, { localhost: isLocalhostBrowser() }))
112
+ .filter((session) => isVisibleSessionForViewer(session, currentUser, { localhost: localhostBrowser }))
103
113
  .sort((a, b) => (b.lastActiveAt || 0) - (a.lastActiveAt || 0))
104
114
  .slice(0, 5),
105
- [currentUser, sessions],
115
+ [currentUser, localhostBrowser, sessions],
106
116
  )
107
117
 
108
118
  // Quick stats
@@ -130,12 +140,12 @@ export function HomeView() {
130
140
 
131
141
  // Upcoming schedules
132
142
  const upcomingSchedules = useMemo(() => {
133
- const now = Date.now()
143
+ const currentNow = now ?? 0
134
144
  return Object.values(schedules)
135
- .filter((s) => s.status === 'active' && s.nextRunAt && s.nextRunAt > now)
145
+ .filter((s) => s.status === 'active' && s.nextRunAt && s.nextRunAt > currentNow)
136
146
  .sort((a, b) => (a.nextRunAt || 0) - (b.nextRunAt || 0))
137
147
  .slice(0, 5)
138
- }, [schedules])
148
+ }, [now, schedules])
139
149
 
140
150
  // Unread notifications
141
151
  const unreadNotifications = useMemo(
@@ -148,14 +158,16 @@ export function HomeView() {
148
158
 
149
159
  // Load data on mount
150
160
  useEffect(() => {
161
+ let cancelled = false
151
162
  void loadActivity({ limit: 8 })
152
163
  void loadSchedules()
153
164
  void loadNotifications()
154
165
  const connectorTimer = window.setTimeout(() => {
155
- void loadConnectors()
166
+ if (!cancelled) void loadConnectors()
156
167
  }, 1200)
157
168
  api<{ records: Array<{ estimatedCost: number }>; timeSeries: Array<{ cost: number; bucket: string }> }>('GET', '/usage?range=7d')
158
169
  .then((data) => {
170
+ if (cancelled || !mountedRef.current) return
159
171
  const series = (data.timeSeries || []).map((pt: { cost: number; bucket?: string }) => ({ cost: pt.cost, bucket: pt.bucket || '' }))
160
172
  setCostTrend(series)
161
173
  const todayBucket = new Date().toISOString().slice(0, 10)
@@ -163,12 +175,14 @@ export function HomeView() {
163
175
  setTodayCost(todayPt?.cost || 0)
164
176
  })
165
177
  .catch(() => {})
166
- return () => window.clearTimeout(connectorTimer)
178
+ return () => {
179
+ cancelled = true
180
+ window.clearTimeout(connectorTimer)
181
+ }
167
182
  // eslint-disable-next-line react-hooks/exhaustive-deps
168
- }, [])
183
+ }, [mountedRef])
169
184
 
170
185
  const handleAgentClick = (agent: Agent) => {
171
- setMessages([])
172
186
  void setCurrentAgent(agent.id)
173
187
  setActiveView('agents')
174
188
  }
@@ -225,7 +239,7 @@ export function HomeView() {
225
239
  </div>
226
240
 
227
241
  {(() => {
228
- const now = Date.now()
242
+ const currentNow = now ?? 0
229
243
  const items = [
230
244
  ...allTasks
231
245
  .filter((task) => task.pendingApproval)
@@ -244,7 +258,7 @@ export function HomeView() {
244
258
  id: `failed:${task.id}`,
245
259
  tone: 'danger' as const,
246
260
  label: task.title,
247
- meta: `Failed ${timeAgo(task.updatedAt || task.createdAt)}`,
261
+ meta: `Failed ${timeAgo(task.updatedAt || task.createdAt, now)}`,
248
262
  onClick: () => handleTaskClick(task),
249
263
  })),
250
264
  ...allConnectors
@@ -258,7 +272,7 @@ export function HomeView() {
258
272
  onClick: () => setActiveView('connectors'),
259
273
  })),
260
274
  ...Object.values(schedules)
261
- .filter((schedule) => schedule.status === 'active' && schedule.nextRunAt && schedule.nextRunAt < now)
275
+ .filter((schedule) => schedule.status === 'active' && schedule.nextRunAt && schedule.nextRunAt < currentNow)
262
276
  .slice(0, 2)
263
277
  .map((schedule) => ({
264
278
  id: `schedule:${schedule.id}`,
@@ -396,7 +410,7 @@ export function HomeView() {
396
410
  x{getNotificationOccurrenceCount(n)}
397
411
  </span>
398
412
  )}
399
- <span className="text-[10px] text-text-3/40">{timeAgo(getNotificationActivityAt(n))}</span>
413
+ <span className="text-[10px] text-text-3/40">{timeAgo(getNotificationActivityAt(n), now)}</span>
400
414
  </div>
401
415
  </button>
402
416
  ))}
@@ -452,7 +466,7 @@ export function HomeView() {
452
466
  <div className="flex-1 min-w-0">
453
467
  <span className="text-[13px] font-500 text-text truncate block">{task.title}</span>
454
468
  <span className="text-[11px] text-text-3/50">
455
- {agent?.name || 'Unassigned'} · {task.status === 'running' ? 'running' : 'queued'}{task.startedAt ? ` · ${timeAgo(task.startedAt)}` : ''}
469
+ {agent?.name || 'Unassigned'} · {task.status === 'running' ? 'running' : 'queued'}{task.startedAt ? ` · ${timeAgo(task.startedAt, now)}` : ''}
456
470
  </span>
457
471
  </div>
458
472
  </button>
@@ -482,7 +496,7 @@ export function HomeView() {
482
496
  <div className="flex-1 min-w-0">
483
497
  <span className="text-[13px] font-500 text-text truncate block">{sched.name}</span>
484
498
  <span className="text-[11px] text-text-3/50">
485
- {agent?.name || 'No agent'} · {sched.nextRunAt ? timeUntil(sched.nextRunAt) : '—'}
499
+ {agent?.name || 'No agent'} · {sched.nextRunAt ? timeUntil(sched.nextRunAt, now) : '—'}
486
500
  </span>
487
501
  </div>
488
502
  </div>
@@ -503,7 +517,7 @@ export function HomeView() {
503
517
  {pinnedAgents.map((agent) => {
504
518
  const threadSession = agent.threadSessionId ? sessions[agent.threadSessionId] as Session | undefined : undefined
505
519
  const heartbeatOn = agent.heartbeatEnabled === true && (agent.plugins?.length ?? 0) > 0
506
- const recentlyActive = (threadSession?.lastActiveAt ?? 0) > Date.now() - 30 * 60 * 1000
520
+ const recentlyActive = !!now && (threadSession?.lastActiveAt ?? 0) > now - 30 * 60 * 1000
507
521
  const isOnline = runningAgentIds.has(agent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive
508
522
  const isTyping = streamingSessionId === agent.threadSessionId
509
523
  const lastActive = threadSession?.lastActiveAt || agent.lastUsedAt || agent.updatedAt
@@ -539,7 +553,7 @@ export function HomeView() {
539
553
  </span>
540
554
  ) : (
541
555
  <span className={`text-[10px] ${isOnline ? 'text-emerald-400/80' : 'text-text-3/50'}`}>
542
- {isOnline ? 'Online' : lastActive ? timeAgo(lastActive) : 'Idle'}
556
+ {isOnline ? 'Online' : lastActive ? timeAgo(lastActive, now) : 'Idle'}
543
557
  </span>
544
558
  )}
545
559
  {modelLabel && (
@@ -589,7 +603,7 @@ export function HomeView() {
589
603
  {displayName}
590
604
  </span>
591
605
  <span className="text-[11px] text-text-3/50 shrink-0">
592
- {timeAgo(session.lastActiveAt || session.createdAt)}
606
+ {timeAgo(session.lastActiveAt || session.createdAt, now)}
593
607
  </span>
594
608
  </div>
595
609
  {lastMsg && (
@@ -619,7 +633,7 @@ export function HomeView() {
619
633
  <path d={ACTIVITY_ICONS[entry.action] || ACTIVITY_ICONS.updated} />
620
634
  </svg>
621
635
  <span className="text-[12px] text-text-3/80 flex-1 truncate">{entry.summary}</span>
622
- <span className="text-[10px] text-text-3/40 shrink-0">{timeAgo(entry.timestamp)}</span>
636
+ <span className="text-[10px] text-text-3/40 shrink-0">{timeAgo(entry.timestamp, now)}</span>
623
637
  </div>
624
638
  ))}
625
639
  </div>
@@ -10,6 +10,7 @@ import { FilePreview } from '@/components/shared/file-preview'
10
10
  import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
11
11
  import { toast } from 'sonner'
12
12
  import { safeStorageGet, safeStorageRemove, safeStorageSet } from '@/lib/safe-storage'
13
+ import { errorMessage } from '@/lib/shared-utils'
13
14
 
14
15
  interface Props {
15
16
  streaming: boolean
@@ -119,7 +120,7 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
119
120
  const result = await uploadImage(file)
120
121
  addPendingFile({ file, path: result.path, url: result.url })
121
122
  } catch (err: unknown) {
122
- console.error('File upload failed:', err instanceof Error ? err.message : String(err))
123
+ console.error('File upload failed:', errorMessage(err))
123
124
  }
124
125
  }, [addPendingFile])
125
126
 
@@ -161,6 +162,8 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
161
162
  </div>
162
163
  <button
163
164
  onClick={onStop}
165
+ aria-label="Stop response"
166
+ data-testid="chat-stop"
164
167
  className="px-4 py-2 rounded-pill border border-danger/20 bg-danger/[0.06]
165
168
  text-danger text-[12px] font-600 cursor-pointer transition-all duration-200
166
169
  active:scale-95 hover:bg-danger/[0.1] hover:border-danger/30 shrink-0"
@@ -210,6 +213,8 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
210
213
  onKeyDown={handleKeyDown}
211
214
  onPaste={handlePaste}
212
215
  placeholder="Ask me anything..."
216
+ aria-label="Message input"
217
+ data-testid="chat-input"
213
218
  rows={1}
214
219
  className="w-full px-5 pt-4 pb-2 bg-transparent text-text text-[15px] outline-none resize-none
215
220
  max-h-[140px] leading-[1.55] placeholder:text-text-3/70 border-none"
@@ -220,6 +225,8 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
220
225
  <button
221
226
  type="button"
222
227
  onClick={() => setExtrasOpen((open) => !open)}
228
+ aria-label="Add attachment"
229
+ data-testid="chat-add"
223
230
  className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
224
231
  text-text-3 text-[13px] cursor-pointer hover:text-text-2 hover:bg-white/[0.05] transition-all duration-200"
225
232
  style={{ fontFamily: 'inherit' }}
@@ -240,6 +247,8 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
240
247
  <button
241
248
  onClick={handleSend}
242
249
  disabled={!hasContent}
250
+ aria-label={streaming ? 'Queue message' : 'Send message'}
251
+ data-testid="chat-send"
243
252
  className={`w-9 h-9 rounded-[11px] border-none flex items-center justify-center
244
253
  shrink-0 cursor-pointer transition-all duration-250
245
254
  ${hasContent
@@ -10,49 +10,31 @@ import { SettingsPage } from '@/components/shared/settings/settings-page'
10
10
  import { AgentList } from '@/components/agents/agent-list'
11
11
  import { AgentChatList } from '@/components/agents/agent-chat-list'
12
12
  import { AgentAvatar } from '@/components/agents/agent-avatar'
13
- import { AgentSheet } from '@/components/agents/agent-sheet'
14
13
  import { ScheduleList } from '@/components/schedules/schedule-list'
15
- import { ScheduleSheet } from '@/components/schedules/schedule-sheet'
16
14
  import { MemoryAgentList } from '@/components/memory/memory-agent-list'
17
- import { MemorySheet } from '@/components/memory/memory-sheet'
18
15
  import { MemoryBrowser } from '@/components/memory/memory-browser'
19
16
  import { TaskList } from '@/components/tasks/task-list'
20
- import { TaskSheet } from '@/components/tasks/task-sheet'
21
17
  import { TaskBoard } from '@/components/tasks/task-board'
22
18
  import { ApprovalsPanel } from '@/components/tasks/approvals-panel'
23
19
  import { SecretsList } from '@/components/secrets/secrets-list'
24
- import { SecretSheet } from '@/components/secrets/secret-sheet'
25
20
  import { ProviderList } from '@/components/providers/provider-list'
26
- import { ProviderSheet } from '@/components/providers/provider-sheet'
27
- import { GatewaySheet } from '@/components/gateways/gateway-sheet'
28
21
  import { SkillList } from '@/components/skills/skill-list'
29
- import { SkillSheet } from '@/components/skills/skill-sheet'
30
22
  import { ConnectorList } from '@/components/connectors/connector-list'
31
- import { ConnectorSheet } from '@/components/connectors/connector-sheet'
32
23
  import { ChatroomList } from '@/components/chatrooms/chatroom-list'
33
24
  import { ChatroomView } from '@/components/chatrooms/chatroom-view'
34
- import { ChatroomSheet } from '@/components/chatrooms/chatroom-sheet'
35
25
  import { useChatroomStore } from '@/stores/use-chatroom-store'
36
26
  import { WebhookList } from '@/components/webhooks/webhook-list'
37
- import { WebhookSheet } from '@/components/webhooks/webhook-sheet'
38
27
  import { LogList } from '@/components/logs/log-list'
39
28
  import { McpServerList } from '@/components/mcp-servers/mcp-server-list'
40
- import { McpServerSheet } from '@/components/mcp-servers/mcp-server-sheet'
41
29
  import { KnowledgeList } from '@/components/knowledge/knowledge-list'
42
- import { KnowledgeSheet } from '@/components/knowledge/knowledge-sheet'
43
30
  import { PluginList } from '@/components/plugins/plugin-list'
44
- import { PluginSheet } from '@/components/plugins/plugin-sheet'
45
31
  import { RunList } from '@/components/runs/run-list'
46
32
  import { ActivityFeed } from '@/components/activity/activity-feed'
47
33
  import { MetricsDashboard } from '@/components/usage/metrics-dashboard'
48
34
  import { WalletPanel } from '@/components/wallets/wallet-panel'
49
35
  import { ProjectList } from '@/components/projects/project-list'
50
36
  import { ProjectDetail } from '@/components/projects/project-detail'
51
- import { ProjectSheet } from '@/components/projects/project-sheet'
52
- import { SearchDialog } from '@/components/shared/search-dialog'
53
- import { AgentSwitchDialog } from '@/components/shared/agent-switch-dialog'
54
- import { KeyboardShortcutsDialog } from '@/components/shared/keyboard-shortcuts-dialog'
55
- import { ProfileSheet } from '@/components/shared/profile-sheet'
37
+ import { SheetLayer } from './sheet-layer'
56
38
  import { HomeView } from '@/components/home/home-view'
57
39
  import { NetworkBanner } from './network-banner'
58
40
  import { UpdateBanner } from './update-banner'
@@ -1106,25 +1088,7 @@ export function AppLayout() {
1106
1088
  </ErrorBoundary>
1107
1089
 
1108
1090
  <CommandPalette />
1109
- <SearchDialog />
1110
- <AgentSwitchDialog />
1111
- <KeyboardShortcutsDialog />
1112
- <AgentSheet />
1113
- <ScheduleSheet />
1114
- <MemorySheet />
1115
- <TaskSheet />
1116
- <SecretSheet />
1117
- <ProviderSheet />
1118
- <GatewaySheet />
1119
- <SkillSheet />
1120
- <ConnectorSheet />
1121
- <ChatroomSheet />
1122
- <WebhookSheet />
1123
- <McpServerSheet />
1124
- <KnowledgeSheet />
1125
- <PluginSheet />
1126
- <ProjectSheet />
1127
- <ProfileSheet open={profileSheetOpen} onClose={() => setProfileSheetOpen(false)} />
1091
+ <SheetLayer profileSheetOpen={profileSheetOpen} setProfileSheetOpen={setProfileSheetOpen} />
1128
1092
 
1129
1093
  </div>
1130
1094
  )