@swarmclawai/swarmclaw 0.8.4 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (394) hide show
  1. package/README.md +9 -9
  2. package/bin/swarmclaw.js +5 -1
  3. package/bin/worker-cmd.js +73 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +17 -7
  6. package/src/app/api/agents/route.ts +21 -8
  7. package/src/app/api/approvals/route.test.ts +6 -6
  8. package/src/app/api/approvals/route.ts +2 -1
  9. package/src/app/api/auth/route.ts +2 -3
  10. package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
  11. package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
  12. package/src/app/api/chatrooms/[id]/route.ts +7 -6
  13. package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
  14. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  15. package/src/app/api/chats/[id]/clear/route.ts +9 -9
  16. package/src/app/api/chats/[id]/devserver/route.ts +2 -1
  17. package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
  18. package/src/app/api/chats/[id]/fork/route.ts +3 -5
  19. package/src/app/api/chats/[id]/restore/route.ts +6 -7
  20. package/src/app/api/chats/[id]/retry/route.ts +3 -4
  21. package/src/app/api/chats/[id]/route.ts +61 -62
  22. package/src/app/api/chats/route.ts +7 -1
  23. package/src/app/api/connectors/[id]/route.ts +7 -8
  24. package/src/app/api/connectors/route.ts +5 -4
  25. package/src/app/api/eval/run/route.ts +2 -1
  26. package/src/app/api/eval/suite/route.ts +2 -1
  27. package/src/app/api/external-agents/route.test.ts +1 -1
  28. package/src/app/api/external-agents/route.ts +2 -2
  29. package/src/app/api/files/serve/route.ts +1 -1
  30. package/src/app/api/gateways/[id]/route.ts +7 -5
  31. package/src/app/api/gateways/route.ts +1 -1
  32. package/src/app/api/knowledge/upload/route.ts +1 -1
  33. package/src/app/api/logs/route.ts +5 -7
  34. package/src/app/api/memory-images/[filename]/route.ts +2 -3
  35. package/src/app/api/openclaw/agent-files/route.ts +4 -3
  36. package/src/app/api/openclaw/approvals/route.ts +3 -4
  37. package/src/app/api/openclaw/config-sync/route.ts +3 -2
  38. package/src/app/api/openclaw/cron/route.ts +3 -2
  39. package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
  40. package/src/app/api/openclaw/exec-config/route.ts +3 -2
  41. package/src/app/api/openclaw/gateway/route.ts +5 -4
  42. package/src/app/api/openclaw/history/route.ts +3 -2
  43. package/src/app/api/openclaw/media/route.ts +2 -1
  44. package/src/app/api/openclaw/permissions/route.ts +3 -2
  45. package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
  46. package/src/app/api/openclaw/skills/install/route.ts +2 -1
  47. package/src/app/api/openclaw/skills/remove/route.ts +2 -1
  48. package/src/app/api/openclaw/skills/route.ts +3 -2
  49. package/src/app/api/orchestrator/run/route.ts +5 -14
  50. package/src/app/api/perf/route.ts +43 -0
  51. package/src/app/api/plugins/dependencies/route.ts +2 -1
  52. package/src/app/api/plugins/install/route.ts +2 -1
  53. package/src/app/api/plugins/marketplace/route.ts +3 -2
  54. package/src/app/api/plugins/settings/route.ts +2 -1
  55. package/src/app/api/preview-server/route.ts +11 -10
  56. package/src/app/api/projects/[id]/route.ts +1 -1
  57. package/src/app/api/schedules/[id]/route.test.ts +128 -0
  58. package/src/app/api/schedules/[id]/route.ts +43 -43
  59. package/src/app/api/schedules/[id]/run/route.ts +11 -62
  60. package/src/app/api/schedules/route.ts +21 -87
  61. package/src/app/api/settings/route.ts +2 -0
  62. package/src/app/api/setup/doctor/route.ts +9 -8
  63. package/src/app/api/tasks/[id]/approve/route.ts +33 -30
  64. package/src/app/api/tasks/[id]/route.ts +12 -35
  65. package/src/app/api/tasks/import/github/route.ts +2 -1
  66. package/src/app/api/tasks/route.ts +79 -91
  67. package/src/app/api/wallets/[id]/approve/route.ts +2 -1
  68. package/src/app/api/wallets/[id]/route.ts +13 -19
  69. package/src/app/api/wallets/[id]/send/route.ts +2 -1
  70. package/src/app/api/wallets/route.ts +2 -1
  71. package/src/app/api/webhooks/[id]/route.ts +2 -1
  72. package/src/app/api/webhooks/route.test.ts +3 -1
  73. package/src/app/page.tsx +23 -331
  74. package/src/cli/index.js +19 -0
  75. package/src/cli/index.ts +38 -7
  76. package/src/cli/spec.js +9 -0
  77. package/src/components/activity/activity-feed.tsx +7 -4
  78. package/src/components/agents/agent-card.tsx +32 -6
  79. package/src/components/agents/agent-chat-list.tsx +55 -22
  80. package/src/components/agents/agent-files-editor.tsx +3 -2
  81. package/src/components/agents/agent-sheet.tsx +123 -22
  82. package/src/components/agents/inspector-panel.tsx +1 -1
  83. package/src/components/agents/openclaw-skills-panel.tsx +2 -1
  84. package/src/components/agents/trash-list.tsx +1 -1
  85. package/src/components/auth/access-key-gate.tsx +8 -2
  86. package/src/components/auth/setup-wizard.tsx +10 -9
  87. package/src/components/auth/user-picker.tsx +3 -2
  88. package/src/components/chat/chat-area.tsx +20 -1
  89. package/src/components/chat/chat-card.tsx +18 -3
  90. package/src/components/chat/chat-header.tsx +24 -4
  91. package/src/components/chat/chat-list.tsx +2 -11
  92. package/src/components/chat/heartbeat-history-panel.tsx +2 -1
  93. package/src/components/chat/message-bubble.tsx +45 -6
  94. package/src/components/chat/message-list.tsx +280 -145
  95. package/src/components/chat/streaming-bubble.tsx +217 -60
  96. package/src/components/chat/swarm-panel.test.ts +274 -0
  97. package/src/components/chat/swarm-panel.tsx +410 -0
  98. package/src/components/chat/swarm-status-card.tsx +346 -0
  99. package/src/components/chat/tool-call-bubble.tsx +48 -23
  100. package/src/components/chatrooms/chatroom-list.tsx +8 -5
  101. package/src/components/chatrooms/chatroom-message.tsx +10 -7
  102. package/src/components/chatrooms/chatroom-view.tsx +12 -9
  103. package/src/components/connectors/connector-health.tsx +6 -4
  104. package/src/components/connectors/connector-list.tsx +16 -11
  105. package/src/components/connectors/connector-sheet.tsx +12 -6
  106. package/src/components/home/home-view.tsx +38 -24
  107. package/src/components/input/chat-input.tsx +10 -1
  108. package/src/components/layout/app-layout.tsx +2 -38
  109. package/src/components/layout/sheet-layer.tsx +50 -0
  110. package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
  111. package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
  112. package/src/components/plugins/plugin-list.tsx +8 -4
  113. package/src/components/plugins/plugin-sheet.tsx +2 -1
  114. package/src/components/providers/provider-list.tsx +3 -2
  115. package/src/components/providers/provider-sheet.tsx +2 -1
  116. package/src/components/runs/run-list.tsx +11 -7
  117. package/src/components/schedules/schedule-card.tsx +5 -3
  118. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  119. package/src/components/shared/attachment-chip.tsx +19 -3
  120. package/src/components/shared/notification-center.tsx +6 -3
  121. package/src/components/shared/settings/plugin-manager.tsx +3 -2
  122. package/src/components/shared/settings/section-embedding.tsx +2 -1
  123. package/src/components/shared/settings/section-orchestrator.tsx +2 -1
  124. package/src/components/shared/settings/section-user-preferences.tsx +107 -0
  125. package/src/components/shared/settings/settings-page.tsx +13 -9
  126. package/src/components/skills/clawhub-browser.tsx +15 -4
  127. package/src/components/skills/skill-list.tsx +15 -4
  128. package/src/components/tasks/approvals-panel.tsx +2 -1
  129. package/src/components/tasks/task-board.tsx +35 -37
  130. package/src/components/tasks/task-sheet.tsx +4 -3
  131. package/src/components/ui/full-screen-loader.tsx +164 -0
  132. package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
  133. package/src/components/wallets/wallet-panel.tsx +6 -5
  134. package/src/components/wallets/wallet-section.tsx +3 -2
  135. package/src/components/webhooks/webhook-list.tsx +4 -5
  136. package/src/components/webhooks/webhook-sheet.tsx +6 -6
  137. package/src/hooks/use-app-bootstrap.ts +202 -0
  138. package/src/hooks/use-mounted-ref.ts +14 -0
  139. package/src/hooks/use-now.ts +31 -0
  140. package/src/hooks/use-openclaw-gateway.ts +2 -1
  141. package/src/instrumentation.ts +20 -8
  142. package/src/lib/agent-default-tools.test.ts +52 -0
  143. package/src/lib/agent-default-tools.ts +40 -0
  144. package/src/lib/api-client.test.ts +21 -0
  145. package/src/lib/api-client.ts +6 -11
  146. package/src/lib/canvas-content.test.ts +360 -0
  147. package/src/lib/chat-streaming-state.test.ts +49 -2
  148. package/src/lib/chat-streaming-state.ts +26 -10
  149. package/src/lib/fetch-timeout.test.ts +54 -0
  150. package/src/lib/fetch-timeout.ts +60 -3
  151. package/src/lib/live-tool-events.test.ts +77 -0
  152. package/src/lib/live-tool-events.ts +73 -0
  153. package/src/lib/local-observability.test.ts +2 -2
  154. package/src/lib/openclaw-endpoint.test.ts +1 -1
  155. package/src/lib/providers/anthropic.ts +12 -16
  156. package/src/lib/providers/index.ts +4 -2
  157. package/src/lib/providers/ollama.ts +9 -6
  158. package/src/lib/providers/openai.ts +11 -14
  159. package/src/lib/runtime-env.test.ts +8 -8
  160. package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
  161. package/src/lib/schedule-dedupe.test.ts +1 -1
  162. package/src/lib/schedule-dedupe.ts +3 -2
  163. package/src/lib/server/agent-thread-session.test.ts +6 -6
  164. package/src/lib/server/agent-thread-session.ts +6 -9
  165. package/src/lib/server/alert-dispatch.ts +2 -1
  166. package/src/lib/server/api-routes.test.ts +6 -6
  167. package/src/lib/server/approval-connector-notify.test.ts +4 -4
  168. package/src/lib/server/approvals-auto-approve.test.ts +29 -29
  169. package/src/lib/server/approvals.test.ts +317 -0
  170. package/src/lib/server/approvals.ts +5 -4
  171. package/src/lib/server/autonomy-runtime.test.ts +11 -11
  172. package/src/lib/server/browser-state.ts +2 -2
  173. package/src/lib/server/capability-router.test.ts +1 -1
  174. package/src/lib/server/capability-router.ts +3 -2
  175. package/src/lib/server/chat-execution-advanced.test.ts +15 -2
  176. package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
  177. package/src/lib/server/chat-execution-disabled.test.ts +3 -3
  178. package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
  179. package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
  180. package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
  181. package/src/lib/server/chat-execution-tool-events.ts +116 -0
  182. package/src/lib/server/chat-execution-utils.test.ts +479 -0
  183. package/src/lib/server/chat-execution-utils.ts +533 -0
  184. package/src/lib/server/chat-execution.ts +153 -748
  185. package/src/lib/server/chat-streaming-utils.ts +174 -0
  186. package/src/lib/server/chat-turn-tool-routing.ts +310 -0
  187. package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
  188. package/src/lib/server/clawhub-client.ts +2 -1
  189. package/src/lib/server/collection-helpers.test.ts +92 -0
  190. package/src/lib/server/collection-helpers.ts +25 -3
  191. package/src/lib/server/connectors/access.ts +146 -0
  192. package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
  193. package/src/lib/server/connectors/bluebubbles.ts +4 -4
  194. package/src/lib/server/connectors/commands.ts +367 -0
  195. package/src/lib/server/connectors/connector-routing.test.ts +4 -4
  196. package/src/lib/server/connectors/delivery.ts +142 -0
  197. package/src/lib/server/connectors/discord.ts +37 -40
  198. package/src/lib/server/connectors/email.ts +11 -10
  199. package/src/lib/server/connectors/googlechat.ts +4 -4
  200. package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
  201. package/src/lib/server/connectors/ingress-delivery.ts +23 -0
  202. package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
  203. package/src/lib/server/connectors/manager.test.ts +352 -77
  204. package/src/lib/server/connectors/manager.ts +134 -673
  205. package/src/lib/server/connectors/matrix.ts +4 -4
  206. package/src/lib/server/connectors/message-sentinel.ts +7 -0
  207. package/src/lib/server/connectors/openclaw.test.ts +1 -1
  208. package/src/lib/server/connectors/openclaw.ts +8 -10
  209. package/src/lib/server/connectors/outbox.test.ts +192 -0
  210. package/src/lib/server/connectors/outbox.ts +369 -0
  211. package/src/lib/server/connectors/pairing.test.ts +18 -1
  212. package/src/lib/server/connectors/pairing.ts +49 -4
  213. package/src/lib/server/connectors/policy.ts +9 -3
  214. package/src/lib/server/connectors/reconnect-state.ts +71 -0
  215. package/src/lib/server/connectors/response-media.ts +256 -0
  216. package/src/lib/server/connectors/runtime-state.ts +67 -0
  217. package/src/lib/server/connectors/session.test.ts +357 -0
  218. package/src/lib/server/connectors/session.ts +422 -0
  219. package/src/lib/server/connectors/signal.ts +7 -7
  220. package/src/lib/server/connectors/slack.ts +43 -43
  221. package/src/lib/server/connectors/teams.ts +4 -4
  222. package/src/lib/server/connectors/telegram.ts +37 -43
  223. package/src/lib/server/connectors/types.ts +31 -1
  224. package/src/lib/server/connectors/whatsapp.test.ts +108 -0
  225. package/src/lib/server/connectors/whatsapp.ts +106 -34
  226. package/src/lib/server/context-manager.test.ts +409 -0
  227. package/src/lib/server/cost.test.ts +1 -1
  228. package/src/lib/server/daemon-policy.ts +78 -0
  229. package/src/lib/server/daemon-state-connectors.test.ts +167 -0
  230. package/src/lib/server/daemon-state.test.ts +283 -55
  231. package/src/lib/server/daemon-state.ts +106 -109
  232. package/src/lib/server/data-dir.test.ts +5 -5
  233. package/src/lib/server/data-dir.ts +4 -0
  234. package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
  235. package/src/lib/server/delegation-jobs.test.ts +87 -0
  236. package/src/lib/server/delegation-jobs.ts +42 -48
  237. package/src/lib/server/devserver-launch.ts +1 -1
  238. package/src/lib/server/document-utils.ts +7 -9
  239. package/src/lib/server/elevenlabs.ts +2 -1
  240. package/src/lib/server/embeddings.test.ts +105 -0
  241. package/src/lib/server/ethereum.ts +3 -2
  242. package/src/lib/server/eval/agent-regression.ts +3 -2
  243. package/src/lib/server/eval/runner.ts +2 -1
  244. package/src/lib/server/eval/scorer.ts +2 -1
  245. package/src/lib/server/evm-swap.ts +2 -1
  246. package/src/lib/server/gateway/protocol.test.ts +1 -1
  247. package/src/lib/server/guardian.ts +2 -1
  248. package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
  249. package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
  250. package/src/lib/server/heartbeat-service.test.ts +406 -0
  251. package/src/lib/server/heartbeat-service.ts +54 -7
  252. package/src/lib/server/heartbeat-wake.test.ts +19 -0
  253. package/src/lib/server/heartbeat-wake.ts +17 -16
  254. package/src/lib/server/integrity-monitor.test.ts +149 -0
  255. package/src/lib/server/json-utils.ts +22 -0
  256. package/src/lib/server/knowledge-db.test.ts +13 -13
  257. package/src/lib/server/link-understanding.ts +2 -1
  258. package/src/lib/server/llm-response-cache.test.ts +1 -1
  259. package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
  260. package/src/lib/server/main-agent-loop.test.ts +6 -6
  261. package/src/lib/server/main-agent-loop.ts +21 -7
  262. package/src/lib/server/mcp-client.test.ts +1 -1
  263. package/src/lib/server/mcp-conformance.test.ts +1 -1
  264. package/src/lib/server/mcp-conformance.ts +3 -2
  265. package/src/lib/server/memory-consolidation.ts +2 -1
  266. package/src/lib/server/memory-db.test.ts +485 -0
  267. package/src/lib/server/memory-db.ts +39 -26
  268. package/src/lib/server/memory-graph.test.ts +2 -2
  269. package/src/lib/server/memory-policy.test.ts +7 -7
  270. package/src/lib/server/memory-retrieval.test.ts +1 -1
  271. package/src/lib/server/openclaw-config-sync.ts +2 -1
  272. package/src/lib/server/openclaw-deploy.test.ts +1 -1
  273. package/src/lib/server/openclaw-deploy.ts +8 -12
  274. package/src/lib/server/openclaw-exec-config.ts +2 -1
  275. package/src/lib/server/openclaw-gateway.ts +6 -7
  276. package/src/lib/server/openclaw-skills-normalize.ts +2 -1
  277. package/src/lib/server/openclaw-sync.ts +7 -5
  278. package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
  279. package/src/lib/server/orchestrator-lg.ts +199 -327
  280. package/src/lib/server/path-utils.ts +31 -0
  281. package/src/lib/server/perf.ts +161 -0
  282. package/src/lib/server/plugins-approval-guidance.ts +115 -0
  283. package/src/lib/server/plugins.test.ts +1 -1
  284. package/src/lib/server/plugins.ts +22 -132
  285. package/src/lib/server/process-manager.ts +5 -8
  286. package/src/lib/server/provider-health.test.ts +137 -0
  287. package/src/lib/server/provider-health.ts +3 -3
  288. package/src/lib/server/provider-model-discovery.ts +3 -12
  289. package/src/lib/server/queue-followups.test.ts +9 -9
  290. package/src/lib/server/queue-reconcile.test.ts +2 -2
  291. package/src/lib/server/queue-recovery.test.ts +269 -0
  292. package/src/lib/server/queue.test.ts +570 -0
  293. package/src/lib/server/queue.ts +62 -455
  294. package/src/lib/server/resolve-image.ts +30 -0
  295. package/src/lib/server/runtime-settings.test.ts +4 -4
  296. package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
  297. package/src/lib/server/schedule-normalization.test.ts +279 -0
  298. package/src/lib/server/schedule-service.ts +263 -0
  299. package/src/lib/server/scheduler.ts +17 -74
  300. package/src/lib/server/session-mailbox.test.ts +191 -0
  301. package/src/lib/server/session-run-manager.test.ts +640 -0
  302. package/src/lib/server/session-run-manager.ts +59 -15
  303. package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
  304. package/src/lib/server/session-tools/calendar.ts +2 -1
  305. package/src/lib/server/session-tools/canvas.ts +2 -1
  306. package/src/lib/server/session-tools/chatroom.ts +2 -1
  307. package/src/lib/server/session-tools/connector.ts +26 -28
  308. package/src/lib/server/session-tools/context-mgmt.ts +3 -2
  309. package/src/lib/server/session-tools/crawl.ts +4 -3
  310. package/src/lib/server/session-tools/crud.ts +105 -324
  311. package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
  312. package/src/lib/server/session-tools/delegate.ts +6 -8
  313. package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
  314. package/src/lib/server/session-tools/discovery.ts +4 -3
  315. package/src/lib/server/session-tools/document.ts +2 -1
  316. package/src/lib/server/session-tools/email.ts +2 -1
  317. package/src/lib/server/session-tools/extract.ts +2 -1
  318. package/src/lib/server/session-tools/file.ts +4 -3
  319. package/src/lib/server/session-tools/http.ts +2 -1
  320. package/src/lib/server/session-tools/human-loop.ts +2 -1
  321. package/src/lib/server/session-tools/image-gen.ts +4 -3
  322. package/src/lib/server/session-tools/index.ts +26 -30
  323. package/src/lib/server/session-tools/mailbox.ts +2 -1
  324. package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
  325. package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
  326. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
  327. package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
  328. package/src/lib/server/session-tools/monitor.ts +2 -1
  329. package/src/lib/server/session-tools/platform.ts +2 -1
  330. package/src/lib/server/session-tools/plugin-creator.ts +2 -1
  331. package/src/lib/server/session-tools/replicate.ts +3 -2
  332. package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
  333. package/src/lib/server/session-tools/shell.ts +4 -9
  334. package/src/lib/server/session-tools/subagent.ts +322 -170
  335. package/src/lib/server/session-tools/table.ts +6 -5
  336. package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
  337. package/src/lib/server/session-tools/wallet.ts +7 -6
  338. package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
  339. package/src/lib/server/session-tools/web-utils.ts +317 -0
  340. package/src/lib/server/session-tools/web.ts +62 -328
  341. package/src/lib/server/skill-prompt-budget.test.ts +1 -1
  342. package/src/lib/server/skills-normalize.ts +2 -1
  343. package/src/lib/server/storage-item-access.test.ts +302 -0
  344. package/src/lib/server/storage.ts +366 -314
  345. package/src/lib/server/stream-agent-chat.test.ts +82 -3
  346. package/src/lib/server/stream-agent-chat.ts +146 -510
  347. package/src/lib/server/stream-continuation.ts +412 -0
  348. package/src/lib/server/subagent-lineage.test.ts +647 -0
  349. package/src/lib/server/subagent-lineage.ts +435 -0
  350. package/src/lib/server/subagent-runtime.test.ts +484 -0
  351. package/src/lib/server/subagent-runtime.ts +419 -0
  352. package/src/lib/server/subagent-swarm.test.ts +391 -0
  353. package/src/lib/server/subagent-swarm.ts +564 -0
  354. package/src/lib/server/system-events.ts +3 -3
  355. package/src/lib/server/task-followups.test.ts +491 -0
  356. package/src/lib/server/task-followups.ts +391 -0
  357. package/src/lib/server/task-lifecycle.test.ts +205 -0
  358. package/src/lib/server/task-lifecycle.ts +200 -0
  359. package/src/lib/server/task-quality-gate.test.ts +1 -1
  360. package/src/lib/server/task-resume.ts +208 -0
  361. package/src/lib/server/task-service.test.ts +108 -0
  362. package/src/lib/server/task-service.ts +264 -0
  363. package/src/lib/server/task-validation.test.ts +1 -1
  364. package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
  365. package/src/lib/server/tool-capability-policy.test.ts +2 -2
  366. package/src/lib/server/tool-capability-policy.ts +3 -2
  367. package/src/lib/server/tool-planning.ts +2 -1
  368. package/src/lib/server/tool-retry.ts +2 -3
  369. package/src/lib/server/wake-dispatcher.test.ts +303 -0
  370. package/src/lib/server/wake-dispatcher.ts +318 -0
  371. package/src/lib/server/wake-mode.test.ts +161 -0
  372. package/src/lib/server/wake-mode.ts +174 -0
  373. package/src/lib/server/wallet-service.ts +8 -9
  374. package/src/lib/server/watch-jobs.ts +2 -1
  375. package/src/lib/server/workspace-context.ts +2 -2
  376. package/src/lib/shared-utils.test.ts +142 -0
  377. package/src/lib/shared-utils.ts +62 -0
  378. package/src/lib/tool-event-summary.ts +2 -1
  379. package/src/lib/view-routes.test.ts +100 -0
  380. package/src/lib/wallet.test.ts +322 -6
  381. package/src/proxy.test.ts +4 -4
  382. package/src/proxy.ts +2 -3
  383. package/src/stores/set-if-changed.ts +40 -0
  384. package/src/stores/slices/agent-slice.ts +111 -0
  385. package/src/stores/slices/auth-slice.ts +25 -0
  386. package/src/stores/slices/data-slice.ts +301 -0
  387. package/src/stores/slices/index.ts +7 -0
  388. package/src/stores/slices/session-slice.ts +112 -0
  389. package/src/stores/slices/task-slice.ts +63 -0
  390. package/src/stores/slices/ui-slice.ts +192 -0
  391. package/src/stores/use-app-store.ts +17 -822
  392. package/src/stores/use-approval-store.ts +2 -1
  393. package/src/stores/use-chat-store.ts +8 -1
  394. package/src/types/index.ts +10 -0
@@ -1,12 +1,43 @@
1
1
  'use client'
2
2
 
3
+ import { useState } from 'react'
3
4
  import { useAppStore } from '@/stores/use-app-store'
4
5
  import { AgentAvatar } from '@/components/agents/agent-avatar'
5
6
  import type { SettingsSectionProps } from './types'
6
7
 
8
+ function buildWhatsAppContactId(): string {
9
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
10
+ return crypto.randomUUID()
11
+ }
12
+ return `wa-contact-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
13
+ }
14
+
7
15
  export function UserPreferencesSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
8
16
  const agents = useAppStore((s) => s.agents)
9
17
  const sortedAgents = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
18
+ const whatsappApprovedContacts = Array.isArray(appSettings.whatsappApprovedContacts) ? appSettings.whatsappApprovedContacts : []
19
+ const [nextWhatsAppLabel, setNextWhatsAppLabel] = useState('')
20
+ const [nextWhatsAppPhone, setNextWhatsAppPhone] = useState('')
21
+
22
+ const addWhatsAppContact = () => {
23
+ const phone = nextWhatsAppPhone.trim()
24
+ if (!phone) return
25
+ const label = nextWhatsAppLabel.trim() || phone
26
+ patchSettings({
27
+ whatsappApprovedContacts: [
28
+ ...whatsappApprovedContacts,
29
+ { id: buildWhatsAppContactId(), label, phone },
30
+ ],
31
+ })
32
+ setNextWhatsAppLabel('')
33
+ setNextWhatsAppPhone('')
34
+ }
35
+
36
+ const removeWhatsAppContact = (id: string) => {
37
+ patchSettings({
38
+ whatsappApprovedContacts: whatsappApprovedContacts.filter((entry) => entry.id !== id),
39
+ })
40
+ }
10
41
 
11
42
  return (
12
43
  <div className="mb-10">
@@ -51,6 +82,7 @@ export function UserPreferencesSection({ appSettings, patchSettings, inputClass
51
82
  </p>
52
83
  <div className="flex flex-wrap gap-2">
53
84
  <button
85
+ type="button"
54
86
  onClick={() => patchSettings({ defaultAgentId: null })}
55
87
  className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[12px] font-600 cursor-pointer transition-all border
56
88
  ${!appSettings.defaultAgentId
@@ -76,6 +108,81 @@ export function UserPreferencesSection({ appSettings, patchSettings, inputClass
76
108
  ))}
77
109
  </div>
78
110
  </div>
111
+
112
+ <div className="mt-6">
113
+ <label className="text-[12px] font-600 text-text-2 block mb-1.5">WhatsApp Approved Users</label>
114
+ <p className="text-[11px] text-text-3/60 mb-3">
115
+ These numbers or JIDs are globally approved for WhatsApp DMs. They bypass per-connector pairing and are merged into WhatsApp allowlists.
116
+ </p>
117
+
118
+ {whatsappApprovedContacts.length > 0 ? (
119
+ <div className="space-y-2 mb-3">
120
+ {whatsappApprovedContacts.map((entry) => (
121
+ <div
122
+ key={entry.id}
123
+ className="flex items-center justify-between gap-3 rounded-[12px] border border-white/[0.06] bg-white/[0.03] px-3 py-2"
124
+ >
125
+ <div className="min-w-0">
126
+ <div className="text-[12px] font-600 text-text truncate">{entry.label}</div>
127
+ <div className="text-[11px] text-text-3/70 truncate">{entry.phone}</div>
128
+ </div>
129
+ <button
130
+ type="button"
131
+ onClick={() => removeWhatsAppContact(entry.id)}
132
+ className="shrink-0 px-2.5 py-1.5 rounded-[8px] bg-white/[0.04] text-[11px] text-text-3 hover:text-text hover:bg-white/[0.08] transition-colors border-none cursor-pointer"
133
+ style={{ fontFamily: 'inherit' }}
134
+ >
135
+ Remove
136
+ </button>
137
+ </div>
138
+ ))}
139
+ </div>
140
+ ) : (
141
+ <div className="mb-3 rounded-[12px] border border-dashed border-white/[0.08] bg-white/[0.02] px-3 py-3 text-[11px] text-text-3/70">
142
+ No globally approved WhatsApp users yet.
143
+ </div>
144
+ )}
145
+
146
+ <div className="grid grid-cols-1 md:grid-cols-[minmax(0,1fr)_minmax(0,1.2fr)_auto] gap-2">
147
+ <input
148
+ type="text"
149
+ value={nextWhatsAppLabel}
150
+ onChange={(e) => setNextWhatsAppLabel(e.target.value)}
151
+ onKeyDown={(e) => {
152
+ if (e.key === 'Enter') {
153
+ e.preventDefault()
154
+ addWhatsAppContact()
155
+ }
156
+ }}
157
+ placeholder="Label (e.g. Family, Alice)"
158
+ className={inputClass}
159
+ style={{ fontFamily: 'inherit' }}
160
+ />
161
+ <input
162
+ type="text"
163
+ value={nextWhatsAppPhone}
164
+ onChange={(e) => setNextWhatsAppPhone(e.target.value)}
165
+ onKeyDown={(e) => {
166
+ if (e.key === 'Enter') {
167
+ e.preventDefault()
168
+ addWhatsAppContact()
169
+ }
170
+ }}
171
+ placeholder="+15551234567 or 15551234567@s.whatsapp.net"
172
+ className={inputClass}
173
+ style={{ fontFamily: 'inherit' }}
174
+ />
175
+ <button
176
+ type="button"
177
+ onClick={addWhatsAppContact}
178
+ disabled={!nextWhatsAppPhone.trim()}
179
+ className="px-3 py-2 rounded-[10px] text-[12px] font-600 border border-white/[0.06] bg-white/[0.04] text-text transition-colors disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer hover:bg-white/[0.08]"
180
+ style={{ fontFamily: 'inherit' }}
181
+ >
182
+ Add User
183
+ </button>
184
+ </div>
185
+ </div>
79
186
  </div>
80
187
  )
81
188
  }
@@ -72,6 +72,8 @@ const TABS: Tab[] = [
72
72
  },
73
73
  ]
74
74
 
75
+ const VALID_TAB_IDS = TABS.map((t) => t.id)
76
+
75
77
  export function SettingsPage() {
76
78
  const loadProviders = useAppStore((s) => s.loadProviders)
77
79
  const loadCredentials = useAppStore((s) => s.loadCredentials)
@@ -81,13 +83,7 @@ export function SettingsPage() {
81
83
  const loadSecrets = useAppStore((s) => s.loadSecrets)
82
84
  const loadAgents = useAppStore((s) => s.loadAgents)
83
85
  const credentials = useAppStore((s) => s.credentials)
84
- const validTabIds = TABS.map((t) => t.id)
85
- const [activeTab, setActiveTabRaw] = useState(() => {
86
- if (typeof window === 'undefined') return 'general'
87
- const params = new URLSearchParams(window.location.search)
88
- const tab = params.get('tab')
89
- return tab && validTabIds.includes(tab) ? tab : 'general'
90
- })
86
+ const [activeTab, setActiveTabRaw] = useState('general')
91
87
  const contentRef = useRef<HTMLDivElement>(null)
92
88
  const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({})
93
89
  const [pendingSectionId, setPendingSectionId] = useState<string | null>(null)
@@ -109,6 +105,14 @@ export function SettingsPage() {
109
105
  // eslint-disable-next-line react-hooks/exhaustive-deps
110
106
  }, [])
111
107
 
108
+ useEffect(() => {
109
+ const params = new URLSearchParams(window.location.search)
110
+ const tab = params.get('tab')
111
+ if (tab && VALID_TAB_IDS.includes(tab)) {
112
+ setActiveTabRaw(tab)
113
+ }
114
+ }, [])
115
+
112
116
  // Scroll to top when switching tabs
113
117
  useEffect(() => {
114
118
  contentRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
@@ -124,8 +128,8 @@ export function SettingsPage() {
124
128
  id: 'user-preferences',
125
129
  tabId: 'general',
126
130
  title: 'Profile & Default Chat',
127
- description: 'User identity, language, and which agent your sidebar shortcut opens.',
128
- keywords: ['profile', 'default chat', 'default agent', 'shortcut', 'user', 'language', 'main chat'],
131
+ description: 'User identity, language, default-chat behavior, and global WhatsApp approvals.',
132
+ keywords: ['profile', 'default chat', 'default agent', 'shortcut', 'user', 'language', 'main chat', 'whatsapp', 'contacts', 'approved users'],
129
133
  render: () => <UserPreferencesSection {...sectionProps} />,
130
134
  },
131
135
  {
@@ -1,12 +1,13 @@
1
1
  'use client'
2
2
 
3
- import { useState, useEffect, useCallback } from 'react'
3
+ import { useState, useEffect, useCallback, useRef } from 'react'
4
4
  import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
5
5
  import { Input } from '@/components/ui/input'
6
6
  import { Button } from '@/components/ui/button'
7
7
  import { Badge } from '@/components/ui/badge'
8
8
  import { api } from '@/lib/api-client'
9
9
  import { toast } from 'sonner'
10
+ import { useMountedRef } from '@/hooks/use-mounted-ref'
10
11
 
11
12
  interface ClawHubSkill {
12
13
  id: string
@@ -33,6 +34,7 @@ interface ClawHubBrowserProps {
33
34
  }
34
35
 
35
36
  export function ClawHubBrowser({ open, onOpenChange, onInstalled }: ClawHubBrowserProps) {
37
+ const mountedRef = useMountedRef()
36
38
  const [query, setQuery] = useState('')
37
39
  const [skills, setSkills] = useState<ClawHubSkill[]>([])
38
40
  const [page, setPage] = useState(1)
@@ -41,12 +43,16 @@ export function ClawHubBrowser({ open, onOpenChange, onInstalled }: ClawHubBrows
41
43
  const [error, setError] = useState<string | null>(null)
42
44
  const [installing, setInstalling] = useState<string | null>(null)
43
45
  const [searched, setSearched] = useState(false)
46
+ const searchRequestIdRef = useRef(0)
44
47
 
45
48
  const search = useCallback(async (q: string, p: number, append = false) => {
49
+ const requestId = searchRequestIdRef.current + 1
50
+ searchRequestIdRef.current = requestId
46
51
  setLoading(true)
47
52
  setError(null)
48
53
  try {
49
54
  const res = await api<SearchResponse>('GET', `/clawhub/search?q=${encodeURIComponent(q)}&page=${p}`)
55
+ if (!mountedRef.current || searchRequestIdRef.current !== requestId) return
50
56
  if (append) {
51
57
  setSkills(prev => [...prev, ...res.skills])
52
58
  } else {
@@ -56,11 +62,14 @@ export function ClawHubBrowser({ open, onOpenChange, onInstalled }: ClawHubBrows
56
62
  setPage(res.page)
57
63
  setSearched(true)
58
64
  } catch (err) {
65
+ if (!mountedRef.current || searchRequestIdRef.current !== requestId) return
59
66
  setError(err instanceof Error ? err.message : 'Failed to search ClawHub')
60
67
  } finally {
61
- setLoading(false)
68
+ if (mountedRef.current && searchRequestIdRef.current === requestId) {
69
+ setLoading(false)
70
+ }
62
71
  }
63
- }, [])
72
+ }, [mountedRef])
64
73
 
65
74
  useEffect(() => {
66
75
  if (open) {
@@ -101,7 +110,9 @@ export function ClawHubBrowser({ open, onOpenChange, onInstalled }: ClawHubBrows
101
110
  } catch (err) {
102
111
  toast.error(err instanceof Error ? err.message : 'Install failed')
103
112
  } finally {
104
- setInstalling(null)
113
+ if (mountedRef.current) {
114
+ setInstalling(null)
115
+ }
105
116
  }
106
117
  }
107
118
 
@@ -1,12 +1,13 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState, useCallback } from 'react'
3
+ import { useEffect, useState, useCallback, useRef } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { api } from '@/lib/api-client'
6
6
  import { Badge } from '@/components/ui/badge'
7
7
  import { AgentAvatar } from '@/components/agents/agent-avatar'
8
8
  import { ClawHubBrowser } from './clawhub-browser'
9
9
  import { toast } from 'sonner'
10
+ import { useMountedRef } from '@/hooks/use-mounted-ref'
10
11
 
11
12
  interface ClawHubSkill {
12
13
  id: string
@@ -26,6 +27,7 @@ interface SearchResponse {
26
27
  }
27
28
 
28
29
  export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
30
+ const mountedRef = useMountedRef()
29
31
  const skills = useAppStore((s) => s.skills)
30
32
  const loadSkills = useAppStore((s) => s.loadSkills)
31
33
  const agents = useAppStore((s) => s.agents)
@@ -45,6 +47,7 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
45
47
  const [hubSearched, setHubSearched] = useState(false)
46
48
  const [hubError, setHubError] = useState<string | null>(null)
47
49
  const [installing, setInstalling] = useState<string | null>(null)
50
+ const hubSearchRequestIdRef = useRef(0)
48
51
 
49
52
  useEffect(() => {
50
53
  loadSkills()
@@ -67,10 +70,13 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
67
70
 
68
71
  // Embedded ClawHub search
69
72
  const searchHub = useCallback(async (q: string, p: number, append = false) => {
73
+ const requestId = hubSearchRequestIdRef.current + 1
74
+ hubSearchRequestIdRef.current = requestId
70
75
  setHubLoading(true)
71
76
  setHubError(null)
72
77
  try {
73
78
  const res = await api<SearchResponse>('GET', `/clawhub/search?q=${encodeURIComponent(q)}&page=${p}`)
79
+ if (!mountedRef.current || hubSearchRequestIdRef.current !== requestId) return
74
80
  if (append) {
75
81
  setHubSkills(prev => [...prev, ...res.skills])
76
82
  } else {
@@ -80,11 +86,14 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
80
86
  setHubPage(res.page)
81
87
  setHubSearched(true)
82
88
  } catch (err) {
89
+ if (!mountedRef.current || hubSearchRequestIdRef.current !== requestId) return
83
90
  setHubError(err instanceof Error ? err.message : 'Failed to search ClawHub')
84
91
  } finally {
85
- setHubLoading(false)
92
+ if (mountedRef.current && hubSearchRequestIdRef.current === requestId) {
93
+ setHubLoading(false)
94
+ }
86
95
  }
87
- }, [])
96
+ }, [mountedRef])
88
97
 
89
98
  useEffect(() => {
90
99
  if (!inSidebar && tab === 'clawhub' && !hubSearched) {
@@ -111,7 +120,9 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
111
120
  } catch (err) {
112
121
  toast.error(err instanceof Error ? err.message : 'Install failed')
113
122
  } finally {
114
- setInstalling(null)
123
+ if (mountedRef.current) {
124
+ setInstalling(null)
125
+ }
115
126
  }
116
127
  }
117
128
 
@@ -9,6 +9,7 @@ import { useWs } from '@/hooks/use-ws'
9
9
  import { ExecApprovalCard } from '@/components/chat/exec-approval-card'
10
10
  import { getApprovalPayload, getApprovalTitle } from '@/lib/approval-display'
11
11
  import type { AppSettings, ApprovalCategory, ApprovalRequest } from '@/types'
12
+ import { dedup } from '@/lib/shared-utils'
12
13
 
13
14
  const CATEGORY_LABELS: Record<string, string> = {
14
15
  tool_access: 'Plugin Access',
@@ -137,7 +138,7 @@ export function ApprovalsPanel() {
137
138
  const searchTerm = search.trim().toLowerCase()
138
139
 
139
140
  const workflowCategories = useMemo(() => (
140
- Array.from(new Set(workflowApprovals.map((req) => req.category))).sort()
141
+ dedup(workflowApprovals.map((req) => req.category)).sort()
141
142
  ), [workflowApprovals])
142
143
 
143
144
  const filteredExecApprovals = useMemo(() => {
@@ -10,7 +10,9 @@ import { Skeleton } from '@/components/shared/skeleton'
10
10
  import { AgentAvatar } from '@/components/agents/agent-avatar'
11
11
  import { BottomSheet } from '@/components/shared/bottom-sheet'
12
12
  import { inputClass } from '@/components/shared/form-styles'
13
+ import { useNow } from '@/hooks/use-now'
13
14
  import type { BoardTask, BoardTaskStatus } from '@/types'
15
+ import { dedup } from '@/lib/shared-utils'
14
16
  import { toast } from 'sonner'
15
17
 
16
18
  const ACTIVE_COLUMNS: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed']
@@ -18,8 +20,8 @@ type BoardViewMode = 'board' | 'list'
18
20
  type AttentionFilter = 'all' | 'needs-attention' | 'approval' | 'blocked' | 'overdue' | 'failed'
19
21
  type TaskScopeFilter = 'user-facing' | 'all' | 'agent'
20
22
 
21
- function isTaskOverdue(task: BoardTask): boolean {
22
- return !!task.dueAt && task.dueAt < Date.now() && task.status !== 'completed' && task.status !== 'archived'
23
+ function isTaskOverdue(task: BoardTask, now: number | null): boolean {
24
+ return !!now && !!task.dueAt && task.dueAt < now && task.status !== 'completed' && task.status !== 'archived'
23
25
  }
24
26
 
25
27
  function isInternalAgentTask(task: BoardTask): boolean {
@@ -33,10 +35,10 @@ function isTaskRelevantToAgent(task: BoardTask, agentId: string): boolean {
33
35
  || task.delegatedByAgentId === agentId
34
36
  }
35
37
 
36
- function matchesAttentionFilter(task: BoardTask, filter: AttentionFilter): boolean {
38
+ function matchesAttentionFilter(task: BoardTask, filter: AttentionFilter, now: number | null): boolean {
37
39
  const blocked = !!task.blockedBy?.length
38
40
  const pendingApproval = !!task.pendingApproval
39
- const overdue = isTaskOverdue(task)
41
+ const overdue = isTaskOverdue(task, now)
40
42
  const failed = task.status === 'failed'
41
43
  if (filter === 'all') return true
42
44
  if (filter === 'approval') return pendingApproval
@@ -46,17 +48,18 @@ function matchesAttentionFilter(task: BoardTask, filter: AttentionFilter): boole
46
48
  return blocked || pendingApproval || overdue || failed
47
49
  }
48
50
 
49
- function attentionRank(task: BoardTask): number {
51
+ function attentionRank(task: BoardTask, now: number | null): number {
50
52
  if (task.pendingApproval) return 0
51
53
  if (task.status === 'failed') return 1
52
54
  if (task.blockedBy?.length) return 2
53
- if (isTaskOverdue(task)) return 3
55
+ if (isTaskOverdue(task, now)) return 3
54
56
  if (task.status === 'running') return 4
55
57
  if (task.status === 'queued') return 5
56
58
  return 6
57
59
  }
58
60
 
59
61
  export function TaskBoard() {
62
+ const now = useNow()
60
63
  const tasks = useAppStore((s) => s.tasks)
61
64
  const loadTasks = useAppStore((s) => s.loadTasks)
62
65
  const loadAgents = useAppStore((s) => s.loadAgents)
@@ -145,21 +148,10 @@ export function TaskBoard() {
145
148
  const bulkStatusRef = useRef<HTMLDivElement>(null)
146
149
 
147
150
  // URL-based filter state
148
- const [filterAgentId, setFilterAgentId] = useState<string>(() => {
149
- if (typeof window === 'undefined') return ''
150
- return new URLSearchParams(window.location.search).get('agent') || ''
151
- })
152
- const [filterTag, setFilterTag] = useState<string>(() => {
153
- if (typeof window === 'undefined') return ''
154
- return new URLSearchParams(window.location.search).get('tag') || ''
155
- })
156
- const [taskScopeFilter, setTaskScopeFilter] = useState<TaskScopeFilter>(() => {
157
- if (typeof window === 'undefined') return 'user-facing'
158
- const params = new URLSearchParams(window.location.search)
159
- if (params.get('agent')) return 'agent'
160
- const raw = params.get('taskView')
161
- return raw === 'all' ? 'all' : 'user-facing'
162
- })
151
+ const [filterAgentId, setFilterAgentId] = useState<string>('')
152
+ const [filterTag, setFilterTag] = useState<string>('')
153
+ const [taskScopeFilter, setTaskScopeFilter] = useState<TaskScopeFilter>('user-facing')
154
+ const [filtersHydrated, setFiltersHydrated] = useState(false)
163
155
  const [viewMode, setViewMode] = useState<BoardViewMode>('board')
164
156
  const [attentionFilter, setAttentionFilter] = useState<AttentionFilter>('all')
165
157
  const [githubImportOpen, setGitHubImportOpen] = useState(false)
@@ -172,19 +164,25 @@ export function TaskBoard() {
172
164
  const [githubImportError, setGitHubImportError] = useState<string | null>(null)
173
165
  const [githubImportResult, setGitHubImportResult] = useState<GitHubIssueImportResult | null>(null)
174
166
 
175
- // Seed activeProjectFilter from URL on mount
167
+ // Seed URL-backed filters after hydration so the initial tree stays deterministic.
176
168
  useEffect(() => {
177
- if (typeof window === 'undefined') return
178
- const urlProject = new URLSearchParams(window.location.search).get('project')
179
- if (urlProject && !activeProjectFilter) {
180
- setActiveProjectFilter(urlProject)
181
- }
169
+ const params = new URLSearchParams(window.location.search)
170
+ const urlAgent = params.get('agent') || ''
171
+ const urlTag = params.get('tag') || ''
172
+ const urlProject = params.get('project')
173
+ const rawTaskView = params.get('taskView')
174
+
175
+ setFilterAgentId(urlAgent)
176
+ setFilterTag(urlTag)
177
+ setTaskScopeFilter(urlAgent ? 'agent' : rawTaskView === 'all' ? 'all' : 'user-facing')
178
+ if (urlProject && !activeProjectFilter) setActiveProjectFilter(urlProject)
179
+ setFiltersHydrated(true)
182
180
  // eslint-disable-next-line react-hooks/exhaustive-deps
183
181
  }, [])
184
182
 
185
183
  // Sync filters to URL
186
184
  useEffect(() => {
187
- if (typeof window === 'undefined') return
185
+ if (!filtersHydrated) return
188
186
  const params = new URLSearchParams()
189
187
  if (taskScopeFilter === 'agent' && filterAgentId) params.set('agent', filterAgentId)
190
188
  else if (taskScopeFilter === 'all') params.set('taskView', 'all')
@@ -193,7 +191,7 @@ export function TaskBoard() {
193
191
  const qs = params.toString()
194
192
  const newUrl = `${window.location.pathname}${qs ? `?${qs}` : ''}`
195
193
  window.history.replaceState(null, '', newUrl)
196
- }, [filterAgentId, filterTag, activeProjectFilter, taskScopeFilter])
194
+ }, [activeProjectFilter, filterAgentId, filterTag, filtersHydrated, taskScopeFilter])
197
195
 
198
196
  const [loaded, setLoaded] = useState(Object.keys(tasks).length > 0)
199
197
  useEffect(() => {
@@ -202,7 +200,7 @@ export function TaskBoard() {
202
200
  useWs('tasks', loadTasks, 5000)
203
201
 
204
202
  // Collect all unique tags across tasks
205
- const allTags = Array.from(new Set(Object.values(tasks).flatMap((t) => t.tags || []))).sort()
203
+ const allTags = dedup(Object.values(tasks).flatMap((t) => t.tags || [])).sort()
206
204
 
207
205
  const columns: BoardTaskStatus[] = showArchived ? [...ACTIVE_COLUMNS, 'archived'] : ACTIVE_COLUMNS
208
206
 
@@ -217,9 +215,9 @@ export function TaskBoard() {
217
215
 
218
216
  const matchesBaseFilters = useCallback((task: BoardTask) => {
219
217
  if (!matchesScopeFilters(task)) return false
220
- if (!matchesAttentionFilter(task, attentionFilter)) return false
218
+ if (!matchesAttentionFilter(task, attentionFilter, now)) return false
221
219
  return true
222
- }, [attentionFilter, matchesScopeFilters])
220
+ }, [attentionFilter, matchesScopeFilters, now])
223
221
 
224
222
  const scopedTasks = useMemo(
225
223
  () => Object.values(tasks).filter(matchesScopeFilters),
@@ -230,13 +228,13 @@ export function TaskBoard() {
230
228
  scopedTasks
231
229
  .filter(matchesBaseFilters)
232
230
  .sort((a, b) => {
233
- const rankDiff = attentionRank(a) - attentionRank(b)
231
+ const rankDiff = attentionRank(a, now) - attentionRank(b, now)
234
232
  if (rankDiff !== 0) return rankDiff
235
233
  const dueDiff = (a.dueAt || Number.MAX_SAFE_INTEGER) - (b.dueAt || Number.MAX_SAFE_INTEGER)
236
234
  if (dueDiff !== 0) return dueDiff
237
235
  return b.updatedAt - a.updatedAt
238
236
  })
239
- ), [scopedTasks, matchesBaseFilters])
237
+ ), [scopedTasks, matchesBaseFilters, now])
240
238
 
241
239
  const tasksByStatus = useCallback((status: BoardTaskStatus) =>
242
240
  filteredTasks
@@ -334,12 +332,12 @@ export function TaskBoard() {
334
332
  running: all.filter((t) => t.status === 'running').length,
335
333
  completed: all.filter((t) => t.status === 'completed').length,
336
334
  failed: all.filter((t) => t.status === 'failed').length,
337
- overdue: all.filter((t) => isTaskOverdue(t)).length,
335
+ overdue: all.filter((t) => isTaskOverdue(t, now)).length,
338
336
  blocked: all.filter((t) => (t.blockedBy?.length || 0) > 0).length,
339
337
  approvals: all.filter((t) => !!t.pendingApproval).length,
340
- attention: all.filter((t) => matchesAttentionFilter(t, 'needs-attention')).length,
338
+ attention: all.filter((t) => matchesAttentionFilter(t, 'needs-attention', now)).length,
341
339
  }
342
- }, [scopedTasks])
340
+ }, [now, scopedTasks])
343
341
 
344
342
  const activeScopeLabel = useMemo(() => {
345
343
  if (taskScopeFilter === 'all') return 'All tasks'
@@ -11,6 +11,7 @@ import { DirBrowser } from '@/components/shared/dir-browser'
11
11
  import { SheetFooter } from '@/components/shared/sheet-footer'
12
12
  import { inputClass } from '@/components/shared/form-styles'
13
13
  import type { BoardTask, TaskComment, TaskQualityGateConfig } from '@/types'
14
+ import { dedup, errorMessage } from '@/lib/shared-utils'
14
15
  import { SectionLabel } from '@/components/shared/section-label'
15
16
  import { AgentAvatar } from '@/components/agents/agent-avatar'
16
17
 
@@ -187,7 +188,7 @@ export function TaskSheet() {
187
188
  }
188
189
  }
189
190
  } catch (err: unknown) {
190
- setDepError(err instanceof Error ? err.message : String(err))
191
+ setDepError(errorMessage(err))
191
192
  return
192
193
  }
193
194
  setDepError(null)
@@ -208,7 +209,7 @@ export function TaskSheet() {
208
209
  const data = await res.json()
209
210
  if (data.url) setImages((prev) => [...prev, data.url])
210
211
  } catch (err: unknown) {
211
- console.error('Image upload failed:', err instanceof Error ? err.message : String(err))
212
+ console.error('Image upload failed:', errorMessage(err))
212
213
  }
213
214
  setUploading(false)
214
215
  e.target.value = ''
@@ -758,7 +759,7 @@ export function TaskSheet() {
758
759
  list="tag-suggestions"
759
760
  />
760
761
  <datalist id="tag-suggestions">
761
- {Array.from(new Set(Object.values(tasks).flatMap((t) => t.tags || [])))
762
+ {dedup(Object.values(tasks).flatMap((t) => t.tags || []))
762
763
  .filter((t) => !tags.includes(t) && t.includes(tagInput.toLowerCase()))
763
764
  .slice(0, 10)
764
765
  .map((t) => <option key={t} value={t} />)}