@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
package/src/app/page.tsx CHANGED
@@ -1,342 +1,32 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useState, useCallback } from 'react'
4
- import { useAppStore } from '@/stores/use-app-store'
5
4
  import { initAudioContext } from '@/lib/tts'
6
- import { getStoredAccessKey, clearStoredAccessKey, api } from '@/lib/api-client'
7
- import { safeStorageGet, safeStorageRemove, safeStorageSet } from '@/lib/safe-storage'
8
- import { connectWs, disconnectWs } from '@/lib/ws-client'
9
- import { fetchWithTimeout } from '@/lib/fetch-timeout'
10
- import { isDevelopmentLikeRuntime } from '@/lib/runtime-env'
11
- import { useWs } from '@/hooks/use-ws'
5
+ import { clearStoredAccessKey } from '@/lib/api-client'
6
+ import { safeStorageRemove, safeStorageSet } from '@/lib/safe-storage'
7
+ import { disconnectWs } from '@/lib/ws-client'
8
+ import { useViewRouter } from '@/hooks/use-view-router'
9
+ import { useAppBootstrap } from '@/hooks/use-app-bootstrap'
10
+
12
11
  import { AccessKeyGate } from '@/components/auth/access-key-gate'
13
12
  import { UserPicker } from '@/components/auth/user-picker'
14
13
  import { SetupWizard } from '@/components/auth/setup-wizard'
15
14
  import { AppLayout } from '@/components/layout/app-layout'
16
- import { useViewRouter } from '@/hooks/use-view-router'
17
- import type { Agent } from '@/types'
18
-
19
- const AUTH_CHECK_TIMEOUT_MS = isDevelopmentLikeRuntime() ? 20_000 : 8_000
20
- const POST_AUTH_BOOTSTRAP_TIMEOUT_MS = isDevelopmentLikeRuntime() ? 20_000 : 8_000
21
-
22
- function FullScreenLoader(props: {
23
- stage?: string | null
24
- stalled?: boolean
25
- onReload?: () => void
26
- onReset?: () => void
27
- }) {
28
- return (
29
- <div className="h-full flex flex-col items-center justify-center bg-bg overflow-hidden select-none">
30
- {/* Animated orbital ring */}
31
- <div className="relative w-[120px] h-[120px] mb-8">
32
- {/* Outer glow pulse */}
33
- <div
34
- className="absolute inset-[-20px] rounded-full"
35
- style={{
36
- background: 'radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%)',
37
- animation: 'sc-glow 2.5s ease-in-out infinite',
38
- }}
39
- />
40
-
41
- {/* Orbital ring */}
42
- <div
43
- className="absolute inset-0 rounded-full border border-white/[0.06]"
44
- style={{ animation: 'sc-ring 3s linear infinite' }}
45
- />
46
-
47
- {/* Orbiting dots */}
48
- {[0, 1, 2, 3, 4, 5].map((i) => (
49
- <div
50
- key={i}
51
- className="absolute inset-0"
52
- style={{
53
- animation: `sc-orbit 2.4s cubic-bezier(0.4, 0, 0.2, 1) infinite`,
54
- animationDelay: `${i * -0.4}s`,
55
- }}
56
- >
57
- <div
58
- className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
59
- style={{
60
- width: i === 0 ? 8 : 6,
61
- height: i === 0 ? 8 : 6,
62
- background: i === 0 ? '#818CF8' : `rgba(129, 140, 248, ${0.7 - i * 0.1})`,
63
- boxShadow: i === 0 ? '0 0 12px rgba(99,102,241,0.5)' : 'none',
64
- }}
65
- />
66
- </div>
67
- ))}
68
-
69
- {/* Center logo mark */}
70
- <div className="absolute inset-0 flex items-center justify-center">
71
- <div
72
- className="relative"
73
- style={{ animation: 'sc-breathe 2.5s ease-in-out infinite' }}
74
- >
75
- <svg width="36" height="36" viewBox="0 0 36 36" fill="none">
76
- {/* Hexagonal claw mark */}
77
- <path
78
- d="M18 4L30 11V25L18 32L6 25V11L18 4Z"
79
- stroke="rgba(129, 140, 248, 0.3)"
80
- strokeWidth="1"
81
- fill="none"
82
- />
83
- <path
84
- d="M18 9L25 13V23L18 27L11 23V13L18 9Z"
85
- stroke="rgba(129, 140, 248, 0.5)"
86
- strokeWidth="1.5"
87
- fill="rgba(99, 102, 241, 0.06)"
88
- />
89
- {/* Claw lines */}
90
- <path d="M14 15L18 20L22 15" stroke="#818CF8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
91
- <path d="M12 13L18 20L24 13" stroke="rgba(129, 140, 248, 0.3)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
92
- </svg>
93
- </div>
94
- </div>
95
- </div>
96
-
97
- {/* Brand text */}
98
- <div
99
- className="text-[15px] font-display font-700 tracking-[0.15em] uppercase"
100
- style={{
101
- background: 'linear-gradient(135deg, rgba(255,255,255,0.6), rgba(129, 140, 248, 0.8))',
102
- WebkitBackgroundClip: 'text',
103
- WebkitTextFillColor: 'transparent',
104
- animation: 'sc-text-fade 2s ease-in-out infinite alternate, fade-up 0.6s var(--ease-spring) 0.2s both',
105
- }}
106
- >
107
- SwarmClaw
108
- </div>
109
-
110
- {/* Loading bar */}
111
- <div className="mt-4 w-[100px] h-[2px] rounded-full bg-white/[0.06] overflow-hidden" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
112
- <div
113
- className="h-full rounded-full bg-accent-bright/60"
114
- style={{ animation: 'sc-progress 1.5s ease-in-out infinite' }}
115
- />
116
- </div>
117
-
118
- {props.stage ? (
119
- <p
120
- className="mt-4 text-[12px] text-text-3"
121
- style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.4s both' }}
122
- >
123
- {props.stage}
124
- </p>
125
- ) : null}
126
-
127
- {props.stalled ? (
128
- <div
129
- className="mt-6 max-w-[360px] px-4 text-center"
130
- style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.5s both' }}
131
- >
132
- <p className="text-[12px] text-text-2">
133
- Startup is taking longer than expected. This usually means the browser kept stale local state while the dev server restarted.
134
- </p>
135
- <div className="mt-4 flex items-center justify-center gap-3">
136
- <button
137
- type="button"
138
- onClick={props.onReload}
139
- className="px-4 py-2 rounded-[12px] border border-white/[0.08] bg-surface text-[12px] text-text-2 transition-colors hover:bg-surface-2"
140
- >
141
- Reload
142
- </button>
143
- <button
144
- type="button"
145
- onClick={props.onReset}
146
- className="px-4 py-2 rounded-[12px] border border-white/[0.08] bg-transparent text-[12px] text-text-3 transition-colors hover:bg-white/[0.04]"
147
- >
148
- Reset Local Session
149
- </button>
150
- </div>
151
- </div>
152
- ) : null}
153
-
154
- {/* Loading animation keyframes */}
155
- <style>{`
156
- @keyframes sc-orbit {
157
- from { transform: rotate(0deg); }
158
- to { transform: rotate(360deg); }
159
- }
160
- @keyframes sc-ring {
161
- from { transform: rotate(0deg) scale(1); }
162
- 50% { transform: rotate(180deg) scale(1.02); }
163
- to { transform: rotate(360deg) scale(1); }
164
- }
165
- @keyframes sc-breathe {
166
- 0%, 100% { transform: scale(1); opacity: 0.9; }
167
- 50% { transform: scale(1.06); opacity: 1; }
168
- }
169
- @keyframes sc-glow {
170
- 0%, 100% { opacity: 0.5; transform: scale(0.9); }
171
- 50% { opacity: 1; transform: scale(1.1); }
172
- }
173
- @keyframes sc-text-fade {
174
- 0% { opacity: 0.6; }
175
- 100% { opacity: 1; }
176
- }
177
- @keyframes sc-progress {
178
- 0% { width: 0; margin-left: 0; }
179
- 50% { width: 70%; margin-left: 15%; }
180
- 100% { width: 0; margin-left: 100%; }
181
- }
182
- `}</style>
183
- </div>
184
- )
185
- }
15
+ import { FullScreenLoader } from '@/components/ui/full-screen-loader'
186
16
 
187
17
  export default function Home() {
188
- const currentUser = useAppStore((s) => s.currentUser)
189
- const setUser = useAppStore((s) => s.setUser)
190
- const hydrated = useAppStore((s) => s._hydrated)
191
- const hydrate = useAppStore((s) => s.hydrate)
192
- const loadNetworkInfo = useAppStore((s) => s.loadNetworkInfo)
193
- const loadSessions = useAppStore((s) => s.loadSessions)
194
- const loadSettings = useAppStore((s) => s.loadSettings)
18
+ const {
19
+ hydrated,
20
+ authChecked,
21
+ authenticated,
22
+ setAuthenticated,
23
+ currentUser,
24
+ setupDone,
25
+ setSetupDone,
26
+ agentReady
27
+ } = useAppBootstrap()
195
28
 
196
- const [authChecked, setAuthChecked] = useState(false)
197
- const [authenticated, setAuthenticated] = useState(false)
198
29
  const [bootTimedOut, setBootTimedOut] = useState(false)
199
- const [setupDone, setSetupDone] = useState<boolean | null>(() => {
200
- if (safeStorageGet('sc_setup_done') === '1') return true
201
- return null
202
- })
203
-
204
- const checkAuth = useCallback(async () => {
205
- try {
206
- const res = await fetchWithTimeout('/api/auth', {}, AUTH_CHECK_TIMEOUT_MS)
207
- const data = await res.json().catch(() => ({}))
208
- if (data?.authenticated === true) {
209
- setAuthenticated(true)
210
- setAuthChecked(true)
211
- return
212
- }
213
- } catch {
214
- // Fall back to stored-key bootstrap below if the auth probe is unavailable.
215
- }
216
-
217
- const key = getStoredAccessKey()
218
- if (!key) {
219
- setAuthenticated(false)
220
- setAuthChecked(true)
221
- return
222
- }
223
-
224
- try {
225
- const res = await fetchWithTimeout('/api/auth', {
226
- method: 'POST',
227
- headers: { 'Content-Type': 'application/json' },
228
- body: JSON.stringify({ key }),
229
- }, AUTH_CHECK_TIMEOUT_MS)
230
- if (res.ok) {
231
- setAuthenticated(true)
232
- } else {
233
- clearStoredAccessKey()
234
- setAuthenticated(false)
235
- }
236
- } catch {
237
- clearStoredAccessKey()
238
- setAuthenticated(false)
239
- } finally {
240
- setAuthChecked(true)
241
- }
242
- }, [])
243
-
244
- // After auth, try to restore username from server settings
245
- const syncUserFromServer = useCallback(async () => {
246
- if (currentUser) return // already have a name locally
247
- try {
248
- const settings = await api<{ userName?: string }>('GET', '/settings', undefined, {
249
- timeoutMs: POST_AUTH_BOOTSTRAP_TIMEOUT_MS,
250
- retries: 0,
251
- })
252
- if (settings.userName) {
253
- setUser(settings.userName)
254
- }
255
- } catch { /* ignore */ }
256
- }, [currentUser, setUser])
257
-
258
- useEffect(() => {
259
- hydrate()
260
- }, [])
261
-
262
- useEffect(() => {
263
- if (hydrated) checkAuth()
264
- }, [hydrated, checkAuth])
265
-
266
- useEffect(() => {
267
- if (!authenticated) return
268
- connectWs()
269
- syncUserFromServer()
270
- loadNetworkInfo()
271
- loadSettings()
272
- loadSessions()
273
- return () => { disconnectWs() }
274
- }, [authenticated])
275
-
276
- useWs('sessions', loadSessions, 15000)
277
-
278
- // Auto-select agent's thread on load — resolves a persisted agentId into a session,
279
- // or falls back to defaultAgentId from settings, then first agent.
280
- const [agentReady, setAgentReady] = useState(false)
281
- useEffect(() => {
282
- if (!authenticated || !currentUser) return
283
- let cancelled = false
284
- ;(async () => {
285
- try {
286
- const agents = await api<Record<string, Agent>>('GET', '/agents', undefined, {
287
- timeoutMs: POST_AUTH_BOOTSTRAP_TIMEOUT_MS,
288
- retries: 0,
289
- })
290
- if (cancelled) return
291
- useAppStore.setState({ agents })
292
-
293
- const { currentAgentId, appSettings } = useAppStore.getState()
294
- // Priority: persisted agent > settings default > first agent
295
- const targetId = (currentAgentId && agents[currentAgentId])
296
- ? currentAgentId
297
- : (appSettings.defaultAgentId && agents[appSettings.defaultAgentId])
298
- ? appSettings.defaultAgentId
299
- : Object.values(agents)[0]?.id || null
300
-
301
- if (targetId) {
302
- await useAppStore.getState().setCurrentAgent(targetId)
303
- }
304
- } catch { /* ignore */ }
305
- if (!cancelled) setAgentReady(true)
306
- })()
307
- return () => { cancelled = true }
308
- }, [authenticated, currentUser])
309
-
310
- // Check if first-run setup is needed
311
- useEffect(() => {
312
- if (!authenticated || !currentUser) return
313
- let cancelled = false
314
- ;(async () => {
315
- try {
316
- const [settingsResult, credsResult] = await Promise.allSettled([
317
- api<{ setupCompleted?: boolean }>('GET', '/settings', undefined, {
318
- timeoutMs: POST_AUTH_BOOTSTRAP_TIMEOUT_MS,
319
- retries: 0,
320
- }),
321
- api<Record<string, unknown>>('GET', '/credentials', undefined, {
322
- timeoutMs: POST_AUTH_BOOTSTRAP_TIMEOUT_MS,
323
- retries: 0,
324
- }),
325
- ])
326
- if (cancelled) return
327
- const settings = settingsResult.status === 'fulfilled' ? settingsResult.value : {}
328
- const creds = credsResult.status === 'fulfilled' ? credsResult.value : {}
329
- const bothFailed = settingsResult.status === 'rejected' && credsResult.status === 'rejected'
330
- const hasCreds = Object.keys(creds).length > 0
331
- const done = bothFailed ? true : settings.setupCompleted === true || hasCreds
332
- if (done) safeStorageSet('sc_setup_done', '1')
333
- setSetupDone(done)
334
- } catch {
335
- if (!cancelled) setSetupDone(true) // on error, skip wizard
336
- }
337
- })()
338
- return () => { cancelled = true }
339
- }, [authenticated, currentUser])
340
30
 
341
31
  useEffect(() => {
342
32
  const handler = () => {
@@ -351,11 +41,12 @@ export default function Home() {
351
41
  const handler = () => {
352
42
  disconnectWs()
353
43
  setAuthenticated(false)
354
- setAuthChecked(true)
44
+ // Note: we can't fully control authChecked here unless we pass a setter from useAppBootstrap,
45
+ // but usually auth dropping just sets authenticated to false.
355
46
  }
356
47
  window.addEventListener('sc_auth_required', handler)
357
48
  return () => window.removeEventListener('sc_auth_required', handler)
358
- }, [])
49
+ }, [setAuthenticated])
359
50
 
360
51
  useViewRouter()
361
52
 
@@ -371,8 +62,9 @@ export default function Home() {
371
62
 
372
63
  useEffect(() => {
373
64
  if (!bootStage) {
374
- setBootTimedOut(false)
375
- return
65
+ // Defer resetting to avoid synchronous state update in effect
66
+ const t = window.setTimeout(() => setBootTimedOut(false), 0)
67
+ return () => window.clearTimeout(t)
376
68
  }
377
69
  const timer = window.setTimeout(() => setBootTimedOut(true), 15_000)
378
70
  return () => window.clearTimeout(timer)
package/src/cli/index.js CHANGED
@@ -160,6 +160,25 @@ const COMMAND_GROUPS = [
160
160
  cmd('pick', 'POST', '/dirs/pick', 'Open native picker (mode=file|folder)', { expectsJsonBody: true }),
161
161
  ],
162
162
  },
163
+ {
164
+ name: 'perf',
165
+ description: 'Inspect or control runtime perf tracing',
166
+ commands: [
167
+ cmd('status', 'GET', '/perf', 'Get current perf tracing status and recent entries'),
168
+ cmd('enable', 'POST', '/perf', 'Enable perf tracing and clear existing entries', {
169
+ expectsJsonBody: true,
170
+ defaultBody: { action: 'enable' },
171
+ }),
172
+ cmd('disable', 'POST', '/perf', 'Disable perf tracing', {
173
+ expectsJsonBody: true,
174
+ defaultBody: { action: 'disable' },
175
+ }),
176
+ cmd('clear', 'POST', '/perf', 'Clear recent perf entries', {
177
+ expectsJsonBody: true,
178
+ defaultBody: { action: 'clear' },
179
+ }),
180
+ ],
181
+ },
163
182
  {
164
183
  name: 'documents',
165
184
  description: 'Manage documents',
package/src/cli/index.ts CHANGED
@@ -4,6 +4,7 @@ import { Command } from 'commander'
4
4
  import { pathToFileURL } from 'node:url'
5
5
  import fs from 'node:fs'
6
6
  import path from 'node:path'
7
+ import { errorMessage, sleep } from '../lib/shared-utils.ts'
7
8
  import {
8
9
  SETUP_PROVIDERS,
9
10
  DEFAULT_AGENTS,
@@ -74,7 +75,7 @@ function parseMetadata(raw: string | undefined): Record<string, string> | undefi
74
75
  try {
75
76
  parsed = JSON.parse(raw)
76
77
  } catch (err) {
77
- throw new Error(`Invalid --metadata JSON: ${err instanceof Error ? err.message : String(err)}`)
78
+ throw new Error(`Invalid --metadata JSON: ${errorMessage(err)}`)
78
79
  }
79
80
 
80
81
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
@@ -93,7 +94,7 @@ function parseJsonValue(raw: string | undefined, label: string): unknown {
93
94
  try {
94
95
  return JSON.parse(raw)
95
96
  } catch (err) {
96
- throw new Error(`Invalid ${label} JSON: ${err instanceof Error ? err.message : String(err)}`)
97
+ throw new Error(`Invalid ${label} JSON: ${errorMessage(err)}`)
97
98
  }
98
99
  }
99
100
 
@@ -171,7 +172,7 @@ async function apiRequestWithAccessKey<T = unknown>(
171
172
  body: body === undefined ? undefined : JSON.stringify(body),
172
173
  })
173
174
  } catch (err) {
174
- const msg = err instanceof Error ? err.message : String(err)
175
+ const msg = errorMessage(err)
175
176
  throw new Error(`Failed to reach ${ctx.baseUrl}. Is SwarmClaw running? (${msg})`)
176
177
  }
177
178
 
@@ -269,7 +270,7 @@ async function runWithHandler(command: Command, task: (ctx: CliContext) => Promi
269
270
  const result = await task(ctx)
270
271
  printResult(result, ctx.rawOutput)
271
272
  } catch (err) {
272
- const msg = err instanceof Error ? err.message : String(err)
273
+ const msg = errorMessage(err)
273
274
  console.error(msg)
274
275
  process.exitCode = 1
275
276
  }
@@ -413,7 +414,7 @@ async function runInteractiveSetup(ctx: CliContext): Promise<unknown> {
413
414
  console.log(` FAILED: ${check?.message || 'Unknown error'}`)
414
415
  }
415
416
  } catch (err: unknown) {
416
- console.log(` FAILED: ${err instanceof Error ? err.message : String(err)}`)
417
+ console.log(` FAILED: ${errorMessage(err)}`)
417
418
  }
418
419
  }
419
420
 
@@ -666,6 +667,36 @@ export function buildProgram(): Command {
666
667
  await runWithHandler(this as Command, (ctx) => apiRequest(ctx, 'GET', `/runs/${encodeURIComponent(id)}`))
667
668
  })
668
669
 
670
+ const perf = program.command('perf').description('Inspect or control runtime perf tracing')
671
+
672
+ perf
673
+ .command('status')
674
+ .description('Get current perf tracing status and recent entries')
675
+ .action(async function () {
676
+ await runWithHandler(this as Command, (ctx) => apiRequest(ctx, 'GET', '/perf'))
677
+ })
678
+
679
+ perf
680
+ .command('enable')
681
+ .description('Enable perf tracing and clear existing entries')
682
+ .action(async function () {
683
+ await runWithHandler(this as Command, (ctx) => apiRequest(ctx, 'POST', '/perf', { action: 'enable' }))
684
+ })
685
+
686
+ perf
687
+ .command('disable')
688
+ .description('Disable perf tracing')
689
+ .action(async function () {
690
+ await runWithHandler(this as Command, (ctx) => apiRequest(ctx, 'POST', '/perf', { action: 'disable' }))
691
+ })
692
+
693
+ perf
694
+ .command('clear')
695
+ .description('Clear recent perf entries')
696
+ .action(async function () {
697
+ await runWithHandler(this as Command, (ctx) => apiRequest(ctx, 'POST', '/perf', { action: 'clear' }))
698
+ })
699
+
669
700
  const chats = program.command('chats').description('Manage chats')
670
701
  program.command('sessions').description('Manage chats (alias)').action(() => chats.help())
671
702
 
@@ -1381,12 +1412,12 @@ export async function runCli(argv: string[] = process.argv.slice(2)): Promise<nu
1381
1412
 
1382
1413
  // Wait briefly for the hint if the command succeeded
1383
1414
  if (hintPromise && code === 0) {
1384
- await Promise.race([hintPromise, new Promise((r) => setTimeout(r, 2000))])
1415
+ await Promise.race([hintPromise, sleep(2000)])
1385
1416
  }
1386
1417
 
1387
1418
  return code
1388
1419
  } catch (err) {
1389
- const msg = err instanceof Error ? err.message : String(err)
1420
+ const msg = errorMessage(err)
1390
1421
  console.error(msg)
1391
1422
  return 1
1392
1423
  }
package/src/cli/spec.js CHANGED
@@ -103,6 +103,15 @@ const COMMAND_GROUPS = {
103
103
  pick: { description: 'Open native picker (body: {"mode":"file|folder"})', method: 'POST', path: '/dirs/pick' },
104
104
  },
105
105
  },
106
+ perf: {
107
+ description: 'Inspect or control runtime perf tracing',
108
+ commands: {
109
+ status: { description: 'Get current perf tracing status and recent entries', method: 'GET', path: '/perf' },
110
+ enable: { description: 'Enable perf tracing and clear existing entries', method: 'POST', path: '/perf', staticBody: { action: 'enable' } },
111
+ disable: { description: 'Disable perf tracing', method: 'POST', path: '/perf', staticBody: { action: 'disable' } },
112
+ clear: { description: 'Clear recent perf entries', method: 'POST', path: '/perf', staticBody: { action: 'clear' } },
113
+ },
114
+ },
106
115
  documents: {
107
116
  description: 'File uploads/downloads and TTS audio',
108
117
  commands: {
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { useNow } from '@/hooks/use-now'
5
6
  import { useWs } from '@/hooks/use-ws'
6
7
  import type { ActivityEntry } from '@/types'
7
8
 
@@ -22,8 +23,9 @@ const ACTION_COLORS: Record<string, string> = {
22
23
  rejected: 'bg-red-500/15 text-red-400',
23
24
  }
24
25
 
25
- function timeAgo(ts: number) {
26
- const diff = Date.now() - ts
26
+ function timeAgo(ts: number, now: number | null) {
27
+ if (!now) return 'recently'
28
+ const diff = now - ts
27
29
  if (diff < 60_000) return 'just now'
28
30
  if (diff < 3600_000) return `${Math.floor(diff / 60_000)}m ago`
29
31
  if (diff < 86400_000) return `${Math.floor(diff / 3600_000)}h ago`
@@ -33,11 +35,12 @@ function timeAgo(ts: number) {
33
35
  const ENTITY_TYPES = ['', 'agent', 'task', 'connector', 'session', 'webhook', 'schedule'] as const
34
36
 
35
37
  export function ActivityFeed() {
38
+ const now = useNow()
36
39
  const entries = useAppStore((s) => s.activityEntries)
37
40
  const loadActivity = useAppStore((s) => s.loadActivity)
38
41
  const [filterType, setFilterType] = useState('')
39
42
 
40
- useEffect(() => { loadActivity({ entityType: filterType || undefined, limit: 100 }) }, [filterType])
43
+ useEffect(() => { loadActivity({ entityType: filterType || undefined, limit: 100 }) }, [filterType, loadActivity])
41
44
  useWs('activity', () => loadActivity({ entityType: filterType || undefined, limit: 100 }), 10_000)
42
45
 
43
46
  return (
@@ -87,7 +90,7 @@ export function ActivityFeed() {
87
90
  </div>
88
91
  <p className="text-[13px] text-text-2 leading-[1.4] truncate">{entry.summary}</p>
89
92
  </div>
90
- <span className="text-[11px] text-text-3/50 shrink-0 pt-1">{timeAgo(entry.timestamp)}</span>
93
+ <span className="text-[11px] text-text-3/50 shrink-0 pt-1">{timeAgo(entry.timestamp, now)}</span>
91
94
  </div>
92
95
  ))}
93
96
  </div>
@@ -1,10 +1,11 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
3
+ import { useEffect, useRef, useState } from 'react'
4
4
  import type { Agent } from '@/types'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
7
7
  import { useWs } from '@/hooks/use-ws'
8
+ import { useMountedRef } from '@/hooks/use-mounted-ref'
8
9
  import { api } from '@/lib/api-client'
9
10
  import { deleteAgent } from '@/lib/agents'
10
11
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
@@ -19,6 +20,7 @@ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
19
20
  import { useApprovalStore } from '@/stores/use-approval-store'
20
21
  import { AgentAvatar } from './agent-avatar'
21
22
  import { toast } from 'sonner'
23
+ import { errorMessage } from '@/lib/shared-utils'
22
24
 
23
25
  interface Props {
24
26
  agent: Agent
@@ -30,6 +32,7 @@ interface Props {
30
32
  }
31
33
 
32
34
  export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, onSetDefault }: Props) {
35
+ const mountedRef = useMountedRef()
33
36
  const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
34
37
  const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
35
38
  const loadSessions = useAppStore((s) => s.loadSessions)
@@ -46,6 +49,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
46
49
  const approvals = useApprovalStore((s) => s.approvals)
47
50
  const pendingApprovalCount = Object.values(approvals).filter((a) => a.agentId === agent.id).length
48
51
  const [heartbeatPulse, setHeartbeatPulse] = useState(false)
52
+ const heartbeatTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
49
53
  const monthlyBudget = typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0
50
54
  ? agent.monthlyBudget
51
55
  : null
@@ -65,10 +69,25 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
65
69
  const canDelegateToAgents = agent.platformAssignScope === 'all'
66
70
  const agentDisabled = agent.disabled === true
67
71
  useWs(`heartbeat:agent:${agent.id}`, () => {
72
+ if (heartbeatTimerRef.current) {
73
+ clearTimeout(heartbeatTimerRef.current)
74
+ }
68
75
  setHeartbeatPulse(true)
69
- setTimeout(() => setHeartbeatPulse(false), 1500)
76
+ heartbeatTimerRef.current = setTimeout(() => {
77
+ if (mountedRef.current) {
78
+ setHeartbeatPulse(false)
79
+ }
80
+ }, 1500)
70
81
  })
71
82
 
83
+ useEffect(() => {
84
+ return () => {
85
+ if (heartbeatTimerRef.current) {
86
+ clearTimeout(heartbeatTimerRef.current)
87
+ }
88
+ }
89
+ }, [])
90
+
72
91
  const handleClick = () => {
73
92
  setEditingAgentId(agent.id)
74
93
  setAgentSheetOpen(true)
@@ -89,6 +108,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
89
108
  const session = await api<{ id: string }>('POST', `/agents/${agent.id}/thread`, { user: 'default' })
90
109
  if (!session?.id) throw new Error('Agent thread not available')
91
110
  await loadSessions()
111
+ if (!mountedRef.current) return
92
112
  setMessages([])
93
113
  setCurrentSession(session.id)
94
114
  setActiveView('agents')
@@ -96,7 +116,9 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
96
116
  } catch (err) {
97
117
  console.error('Agent task run failed:', err)
98
118
  }
99
- setRunning(false)
119
+ if (mountedRef.current) {
120
+ setRunning(false)
121
+ }
100
122
  }
101
123
 
102
124
  const [cloning, setCloning] = useState(false)
@@ -107,9 +129,11 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
107
129
  await loadAgents()
108
130
  toast.success('Agent duplicated')
109
131
  } catch (err: unknown) {
110
- toast.error(err instanceof Error ? err.message : String(err))
132
+ toast.error(errorMessage(err))
111
133
  } finally {
112
- setCloning(false)
134
+ if (mountedRef.current) {
135
+ setCloning(false)
136
+ }
113
137
  }
114
138
  }
115
139
 
@@ -117,7 +141,9 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
117
141
  await deleteAgent(agent.id)
118
142
  await loadAgents()
119
143
  toast.success('Agent moved to trash')
120
- setConfirmDelete(false)
144
+ if (mountedRef.current) {
145
+ setConfirmDelete(false)
146
+ }
121
147
  }
122
148
 
123
149
  return (