@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { create } from 'zustand'
4
- import type { Sessions, Session, NetworkInfo, Directory, ProviderInfo, Credentials, Agent, Schedule, AppView, BoardTask, AppSettings, OrchestratorSecret, ProviderConfig, Skill, Connector, Webhook, McpServerConfig, PluginMeta, Project, FleetFilter, ActivityEntry, AppNotification, ApprovalRequest } from '../types'
4
+ import type { Sessions, Session, NetworkInfo, Directory, ProviderInfo, Credentials, Agent, Schedule, AppView, BoardTask, AppSettings, OrchestratorSecret, ProviderConfig, Skill, Connector, Webhook, McpServerConfig, PluginMeta, Project, FleetFilter, ActivityEntry, AppNotification, ApprovalRequest, GatewayProfile, ExternalAgentRuntime } from '../types'
5
5
  import { fetchChats, fetchDirs, fetchProviders, fetchCredentials } from '../lib/chats'
6
6
  import { fetchAgents } from '../lib/agents'
7
7
  import { fetchSchedules } from '../lib/schedules'
@@ -43,9 +43,6 @@ interface AppState {
43
43
  settingsOpen: boolean
44
44
  setSettingsOpen: (open: boolean) => void
45
45
 
46
- newSessionOpen: boolean
47
- setNewSessionOpen: (open: boolean) => void
48
-
49
46
  activeView: AppView
50
47
  setActiveView: (view: AppView) => void
51
48
 
@@ -79,6 +76,10 @@ interface AppState {
79
76
  triggerMemoryRefresh: () => void
80
77
  memoryAgentFilter: string | null
81
78
  setMemoryAgentFilter: (agentId: string | null) => void
79
+ memoryTierFilter: 'all' | 'working' | 'durable' | 'archive'
80
+ setMemoryTierFilter: (tier: 'all' | 'working' | 'durable' | 'archive') => void
81
+ memoryScopeFilter: 'all' | 'global' | 'agent' | 'session' | 'project'
82
+ setMemoryScopeFilter: (scope: 'all' | 'global' | 'agent' | 'session' | 'project') => void
82
83
 
83
84
  appSettings: AppSettings
84
85
  loadSettings: () => Promise<void>
@@ -112,6 +113,13 @@ interface AppState {
112
113
  editingProviderId: string | null
113
114
  setEditingProviderId: (id: string | null) => void
114
115
 
116
+ gatewayProfiles: GatewayProfile[]
117
+ loadGatewayProfiles: () => Promise<void>
118
+ gatewaySheetOpen: boolean
119
+ setGatewaySheetOpen: (open: boolean) => void
120
+ editingGatewayId: string | null
121
+ setEditingGatewayId: (id: string | null) => void
122
+
115
123
  // Skills
116
124
  skills: Record<string, Skill>
117
125
  loadSkills: () => Promise<void>
@@ -215,6 +223,9 @@ interface AppState {
215
223
  walletPanelAgentId: string | null
216
224
  setWalletPanelAgentId: (id: string | null) => void
217
225
 
226
+ externalAgents: ExternalAgentRuntime[]
227
+ loadExternalAgents: () => Promise<void>
228
+
218
229
  }
219
230
 
220
231
  export const useAppStore = create<AppState>((set, get) => ({
@@ -325,9 +336,6 @@ export const useAppStore = create<AppState>((set, get) => ({
325
336
  settingsOpen: false,
326
337
  setSettingsOpen: (open) => set({ settingsOpen: open }),
327
338
 
328
- newSessionOpen: false,
329
- setNewSessionOpen: (open) => set({ newSessionOpen: open }),
330
-
331
339
  activeView: 'home',
332
340
  setActiveView: (view) => set({ activeView: view }),
333
341
 
@@ -400,6 +408,10 @@ export const useAppStore = create<AppState>((set, get) => ({
400
408
  triggerMemoryRefresh: () => set((s) => ({ memoryRefreshKey: s.memoryRefreshKey + 1 })),
401
409
  memoryAgentFilter: null,
402
410
  setMemoryAgentFilter: (agentId) => set({ memoryAgentFilter: agentId }),
411
+ memoryTierFilter: 'all',
412
+ setMemoryTierFilter: (tier) => set({ memoryTierFilter: tier }),
413
+ memoryScopeFilter: 'all',
414
+ setMemoryScopeFilter: (scope) => set({ memoryScopeFilter: scope }),
403
415
 
404
416
  appSettings: {},
405
417
  loadSettings: async () => {
@@ -496,6 +508,20 @@ export const useAppStore = create<AppState>((set, get) => ({
496
508
  editingProviderId: null,
497
509
  setEditingProviderId: (id) => set({ editingProviderId: id }),
498
510
 
511
+ gatewayProfiles: [],
512
+ loadGatewayProfiles: async () => {
513
+ try {
514
+ const gatewayProfiles = await api<GatewayProfile[]>('GET', '/gateways')
515
+ set({ gatewayProfiles })
516
+ } catch {
517
+ // ignore
518
+ }
519
+ },
520
+ gatewaySheetOpen: false,
521
+ setGatewaySheetOpen: (open) => set({ gatewaySheetOpen: open }),
522
+ editingGatewayId: null,
523
+ setEditingGatewayId: (id) => set({ editingGatewayId: id }),
524
+
499
525
  // Skills
500
526
  skills: {},
501
527
  loadSkills: async () => {
@@ -718,4 +744,14 @@ export const useAppStore = create<AppState>((set, get) => ({
718
744
  walletPanelAgentId: null,
719
745
  setWalletPanelAgentId: (id) => set({ walletPanelAgentId: id }),
720
746
 
747
+ externalAgents: [],
748
+ loadExternalAgents: async () => {
749
+ try {
750
+ const externalAgents = await api<ExternalAgentRuntime[]>('GET', '/external-agents')
751
+ set({ externalAgents })
752
+ } catch {
753
+ // ignore
754
+ }
755
+ },
756
+
721
757
  }))
@@ -3,6 +3,7 @@
3
3
  import { create } from 'zustand'
4
4
  import type { Message, DevServerStatus, SSEEvent, ChatTraceBlock } from '../types'
5
5
  import { streamChat } from '../lib/chat'
6
+ import { mergeCompletedAssistantMessage } from '../lib/chat-streaming-state'
6
7
  import { speak } from '../lib/tts'
7
8
  import { getStoredAccessKey } from '../lib/api-client'
8
9
  import { useAppStore } from './use-app-store'
@@ -306,6 +307,11 @@ export const useChatStore = create<ChatState>((set, get) => ({
306
307
  } else if (event.t === 'tool_call') {
307
308
  const id = `tc-${++toolCallCounter}`
308
309
  set((s) => ({
310
+ ...(s.toolEvents[s.toolEvents.length - 1]?.name === (event.toolName || 'unknown')
311
+ && s.toolEvents[s.toolEvents.length - 1]?.input === (event.toolInput || '')
312
+ && s.toolEvents[s.toolEvents.length - 1]?.status === 'running'
313
+ ? {}
314
+ : {
309
315
  streamPhase: 'tool' as const,
310
316
  streamToolName: event.toolName || 'unknown',
311
317
  toolEvents: [...s.toolEvents, {
@@ -314,6 +320,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
314
320
  input: event.toolInput || '',
315
321
  status: 'running',
316
322
  }],
323
+ }),
317
324
  }))
318
325
  } else if (event.t === 'tool_result') {
319
326
  const soundOn = get().soundEnabled
@@ -322,6 +329,22 @@ export const useChatStore = create<ChatState>((set, get) => ({
322
329
  const idx = events.findLastIndex(
323
330
  (e) => e.name === event.toolName && e.status === 'running',
324
331
  )
332
+ if (idx === -1) {
333
+ const last = events[events.length - 1]
334
+ const output = event.toolOutput || ''
335
+ const isError = /^(Error:|error:|ECONNREFUSED|ETIMEDOUT|timeout|failed)/i.test(output.trim())
336
+ || output.includes('ECONNREFUSED')
337
+ || output.includes('ETIMEDOUT')
338
+ || output.includes('Error:')
339
+ if (
340
+ last
341
+ && last.name === event.toolName
342
+ && last.output === output
343
+ && last.status === (isError ? 'error' : 'done')
344
+ ) {
345
+ return { toolEvents: events }
346
+ }
347
+ }
325
348
  if (idx !== -1) {
326
349
  const output = event.toolOutput || ''
327
350
  const isError = /^(Error:|error:|ECONNREFUSED|ETIMEDOUT|timeout|failed)/i.test(output.trim())
@@ -348,7 +371,13 @@ export const useChatStore = create<ChatState>((set, get) => ({
348
371
  } else if (event.t === 'status') {
349
372
  try {
350
373
  const parsed = JSON.parse(event.text || '{}')
351
- set({ agentStatus: parsed })
374
+ if (
375
+ parsed
376
+ && typeof parsed === 'object'
377
+ && ['goal', 'status', 'summary', 'nextAction'].some((key) => key in parsed)
378
+ ) {
379
+ set({ agentStatus: parsed })
380
+ }
352
381
  } catch {
353
382
  // ignore malformed status
354
383
  }
@@ -377,7 +406,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
377
406
  suggestions: suggestions || undefined,
378
407
  }
379
408
  set((s) => ({
380
- messages: [...s.messages, assistantMsg],
409
+ messages: mergeCompletedAssistantMessage(s.messages, assistantMsg),
381
410
  streaming: false,
382
411
  streamingSessionId: null,
383
412
  streamText: '',
@@ -11,13 +11,25 @@ interface ToolEvent {
11
11
  output?: string
12
12
  }
13
13
 
14
- interface StreamingAgent {
14
+ export interface StreamingAgent {
15
15
  text: string
16
16
  name: string
17
17
  error?: string
18
18
  toolEvents: ToolEvent[]
19
19
  }
20
20
 
21
+ interface QueuedChatroomMessage {
22
+ id: string
23
+ chatroomId: string
24
+ text: string
25
+ pendingFiles: PendingFile[]
26
+ replyingTo: ChatroomMessage | null
27
+ }
28
+
29
+ function nextQueuedId() {
30
+ return `queued-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
31
+ }
32
+
21
33
  interface ChatroomState {
22
34
  chatrooms: Record<string, Chatroom>
23
35
  currentChatroomId: string | null
@@ -36,6 +48,10 @@ interface ChatroomState {
36
48
  replyingTo: ChatroomMessage | null
37
49
  setReplyingTo: (msg: ChatroomMessage | null) => void
38
50
 
51
+ queuedMessages: QueuedChatroomMessage[]
52
+ removeQueuedMessage: (id: string) => void
53
+ shiftQueuedMessage: () => QueuedChatroomMessage | undefined
54
+
39
55
  loadChatrooms: () => Promise<void>
40
56
  createChatroom: (data: { name: string; description?: string; agentIds?: string[]; chatMode?: 'sequential' | 'parallel'; autoAddress?: boolean; routingRules?: ChatroomRoutingRule[] }) => Promise<Chatroom>
41
57
  updateChatroom: (id: string, data: Partial<Chatroom>) => Promise<void>
@@ -73,6 +89,15 @@ export const useChatroomStore = create<ChatroomState>((set, get) => ({
73
89
  // Reply-to
74
90
  replyingTo: null,
75
91
  setReplyingTo: (msg) => set({ replyingTo: msg }),
92
+ queuedMessages: [],
93
+ removeQueuedMessage: (id) => set((s) => ({ queuedMessages: s.queuedMessages.filter((item) => item.id !== id) })),
94
+ shiftQueuedMessage: () => {
95
+ const queue = get().queuedMessages
96
+ if (!queue.length) return undefined
97
+ const next = queue[0]
98
+ set({ queuedMessages: queue.slice(1) })
99
+ return next
100
+ },
76
101
 
77
102
  loadChatrooms: async () => {
78
103
  const chatrooms = await api<Record<string, Chatroom>>('GET', '/chatrooms')
@@ -106,9 +131,26 @@ export const useChatroomStore = create<ChatroomState>((set, get) => ({
106
131
 
107
132
  sendMessage: async (text) => {
108
133
  const { currentChatroomId, streaming, pendingFiles, replyingTo } = get()
109
- if (!currentChatroomId || streaming || (!text.trim() && !pendingFiles.length)) return
110
-
111
- set({ streaming: true, streamingAgents: new Map(), pendingFiles: [], replyingTo: null })
134
+ if (!currentChatroomId || (!text.trim() && !pendingFiles.length)) return
135
+ const targetChatroomId = currentChatroomId
136
+
137
+ if (streaming) {
138
+ set((s) => ({
139
+ queuedMessages: [
140
+ ...s.queuedMessages,
141
+ {
142
+ id: nextQueuedId(),
143
+ chatroomId: targetChatroomId,
144
+ text,
145
+ pendingFiles: [...pendingFiles],
146
+ replyingTo,
147
+ },
148
+ ],
149
+ pendingFiles: [],
150
+ replyingTo: null,
151
+ }))
152
+ return
153
+ }
112
154
 
113
155
  const imagePath = pendingFiles.length > 0 && pendingFiles[0].file.type.startsWith('image/')
114
156
  ? pendingFiles[0].path
@@ -116,10 +158,12 @@ export const useChatroomStore = create<ChatroomState>((set, get) => ({
116
158
  const attachedFiles = pendingFiles.length > 0
117
159
  ? pendingFiles.map((f) => f.path)
118
160
  : undefined
161
+ const optimisticText = text.trim() || 'See attached file(s).'
162
+ let started = false
119
163
 
120
164
  const key = getStoredAccessKey()
121
165
  try {
122
- const res = await fetch(`/api/chatrooms/${currentChatroomId}/chat`, {
166
+ const res = await fetch(`/api/chatrooms/${targetChatroomId}/chat`, {
123
167
  method: 'POST',
124
168
  headers: {
125
169
  'Content-Type': 'application/json',
@@ -134,10 +178,43 @@ export const useChatroomStore = create<ChatroomState>((set, get) => ({
134
178
  })
135
179
 
136
180
  if (!res.ok || !res.body) {
137
- set({ streaming: false })
138
181
  return
139
182
  }
140
183
 
184
+ started = true
185
+ set((s) => {
186
+ const existingChatroom = s.chatrooms[targetChatroomId]
187
+ const optimisticMessage: ChatroomMessage = {
188
+ id: `local-${Date.now()}`,
189
+ senderId: 'user',
190
+ senderName: 'You',
191
+ role: 'user',
192
+ text: optimisticText,
193
+ mentions: [],
194
+ reactions: [],
195
+ time: Date.now(),
196
+ ...(imagePath ? { imagePath } : {}),
197
+ ...(attachedFiles ? { attachedFiles } : {}),
198
+ ...(replyingTo ? { replyToId: replyingTo.id } : {}),
199
+ }
200
+ return {
201
+ streaming: true,
202
+ streamingAgents: new Map(),
203
+ pendingFiles: [],
204
+ replyingTo: null,
205
+ chatrooms: existingChatroom
206
+ ? {
207
+ ...s.chatrooms,
208
+ [targetChatroomId]: {
209
+ ...existingChatroom,
210
+ messages: [...existingChatroom.messages, optimisticMessage],
211
+ updatedAt: optimisticMessage.time,
212
+ },
213
+ }
214
+ : s.chatrooms,
215
+ }
216
+ })
217
+
141
218
  const reader = res.body.getReader()
142
219
  const decoder = new TextDecoder()
143
220
  let buf = ''
@@ -220,13 +297,6 @@ export const useChatroomStore = create<ChatroomState>((set, get) => ({
220
297
  return { streamingAgents: agents }
221
298
  })
222
299
  }
223
- try {
224
- const { currentChatroomId: cid } = get()
225
- if (cid) {
226
- const chatroom = await api<Chatroom>('GET', `/chatrooms/${cid}`)
227
- set((s) => ({ chatrooms: { ...s.chatrooms, [cid]: chatroom } }))
228
- }
229
- } catch { /* will catch on next WS push */ }
230
300
  } else if (event.t === 'done') {
231
301
  break
232
302
  }
@@ -236,31 +306,88 @@ export const useChatroomStore = create<ChatroomState>((set, get) => ({
236
306
  }
237
307
  }
238
308
  } finally {
239
- set({ streaming: false, streamingAgents: new Map() })
240
- try {
241
- const { currentChatroomId: cid } = get()
242
- if (cid) {
243
- const chatroom = await api<Chatroom>('GET', `/chatrooms/${cid}`)
244
- set((s) => ({ chatrooms: { ...s.chatrooms, [cid]: chatroom } }))
309
+ if (started) {
310
+ set({ streaming: false, streamingAgents: new Map() })
311
+ try {
312
+ const chatroom = await api<Chatroom>('GET', `/chatrooms/${targetChatroomId}`)
313
+ set((s) => ({ chatrooms: { ...s.chatrooms, [targetChatroomId]: chatroom } }))
314
+ } catch { /* ignore */ }
315
+
316
+ const nextQueued = get().shiftQueuedMessage()
317
+ if (nextQueued) {
318
+ if (get().currentChatroomId !== nextQueued.chatroomId) {
319
+ set((s) => ({ queuedMessages: [nextQueued, ...s.queuedMessages] }))
320
+ return
321
+ }
322
+ set({ pendingFiles: nextQueued.pendingFiles, replyingTo: nextQueued.replyingTo })
323
+ setTimeout(() => {
324
+ void get().sendMessage(nextQueued.text)
325
+ }, 100)
245
326
  }
246
- } catch { /* ignore */ }
327
+ }
247
328
  }
248
329
  },
249
330
 
250
331
  toggleReaction: async (messageId, emoji) => {
251
332
  const { currentChatroomId } = get()
252
333
  if (!currentChatroomId) return
253
- await api('POST', `/chatrooms/${currentChatroomId}/reactions`, { messageId, emoji })
254
- const chatroom = await api<Chatroom>('GET', `/chatrooms/${currentChatroomId}`)
255
- set((s) => ({ chatrooms: { ...s.chatrooms, [currentChatroomId]: chatroom } }))
334
+ const previous = get().chatrooms[currentChatroomId]
335
+ if (previous) {
336
+ set((s) => ({
337
+ chatrooms: {
338
+ ...s.chatrooms,
339
+ [currentChatroomId]: {
340
+ ...previous,
341
+ messages: previous.messages.map((message) => {
342
+ if (message.id !== messageId) return message
343
+ const existing = message.reactions.find((reaction) => reaction.emoji === emoji && reaction.reactorId === 'user')
344
+ return {
345
+ ...message,
346
+ reactions: existing
347
+ ? message.reactions.filter((reaction) => !(reaction.emoji === emoji && reaction.reactorId === 'user'))
348
+ : [...message.reactions, { emoji, reactorId: 'user', time: Date.now() }],
349
+ }
350
+ }),
351
+ },
352
+ },
353
+ }))
354
+ }
355
+ try {
356
+ const chatroom = await api<Chatroom>('POST', `/chatrooms/${currentChatroomId}/reactions`, { messageId, emoji })
357
+ set((s) => ({ chatrooms: { ...s.chatrooms, [currentChatroomId]: chatroom } }))
358
+ } catch {
359
+ if (previous) {
360
+ set((s) => ({ chatrooms: { ...s.chatrooms, [currentChatroomId]: previous } }))
361
+ }
362
+ }
256
363
  },
257
364
 
258
365
  togglePin: async (messageId) => {
259
366
  const { currentChatroomId } = get()
260
367
  if (!currentChatroomId) return
261
- await api('POST', `/chatrooms/${currentChatroomId}/pins`, { messageId })
262
- const chatroom = await api<Chatroom>('GET', `/chatrooms/${currentChatroomId}`)
263
- set((s) => ({ chatrooms: { ...s.chatrooms, [currentChatroomId]: chatroom } }))
368
+ const previous = get().chatrooms[currentChatroomId]
369
+ if (previous) {
370
+ const pinnedMessageIds = previous.pinnedMessageIds || []
371
+ set((s) => ({
372
+ chatrooms: {
373
+ ...s.chatrooms,
374
+ [currentChatroomId]: {
375
+ ...previous,
376
+ pinnedMessageIds: pinnedMessageIds.includes(messageId)
377
+ ? pinnedMessageIds.filter((id) => id !== messageId)
378
+ : [...pinnedMessageIds, messageId],
379
+ },
380
+ },
381
+ }))
382
+ }
383
+ try {
384
+ const chatroom = await api<Chatroom>('POST', `/chatrooms/${currentChatroomId}/pins`, { messageId })
385
+ set((s) => ({ chatrooms: { ...s.chatrooms, [currentChatroomId]: chatroom } }))
386
+ } catch {
387
+ if (previous) {
388
+ set((s) => ({ chatrooms: { ...s.chatrooms, [currentChatroomId]: previous } }))
389
+ }
390
+ }
264
391
  },
265
392
 
266
393
  addMember: async (agentId) => {