@swarmclawai/swarmclaw 1.2.6 → 1.2.9

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 (269) hide show
  1. package/README.md +54 -23
  2. package/next.config.ts +1 -0
  3. package/package.json +4 -3
  4. package/scripts/easy-setup.mjs +1 -1
  5. package/scripts/postinstall.mjs +1 -1
  6. package/skills/swarmclaw.md +115 -0
  7. package/skills/tools/browser.md +131 -0
  8. package/skills/tools/execute.md +98 -0
  9. package/skills/tools/files.md +98 -0
  10. package/skills/tools/memory.md +104 -0
  11. package/skills/tools/platform.md +144 -0
  12. package/skills/tools/skills.md +83 -0
  13. package/src/app/agents/[id]/page.tsx +1 -18
  14. package/src/app/api/agents/thread-route.test.ts +0 -1
  15. package/src/app/api/approvals/route.test.ts +6 -22
  16. package/src/app/api/chats/[id]/messages/route.ts +23 -19
  17. package/src/app/api/chats/messages-route.test.ts +105 -51
  18. package/src/app/api/connectors/route.ts +2 -2
  19. package/src/app/api/mcp-servers/[id]/test/route.ts +3 -2
  20. package/src/app/api/openclaw/deploy/route.ts +2 -0
  21. package/src/app/api/portability/export/route.ts +8 -0
  22. package/src/app/api/portability/import/route.test.ts +80 -0
  23. package/src/app/api/portability/import/route.ts +28 -0
  24. package/src/app/api/settings/route.ts +0 -2
  25. package/src/app/api/setup/doctor/route.ts +4 -4
  26. package/src/app/api/wallets/[id]/route.ts +15 -157
  27. package/src/app/api/wallets/generate/route.ts +22 -0
  28. package/src/app/api/wallets/route.test.ts +147 -0
  29. package/src/app/api/wallets/route.ts +13 -95
  30. package/src/app/autonomy/page.tsx +2 -57
  31. package/src/app/protocols/page.tsx +2 -21
  32. package/src/app/settings/page.tsx +0 -9
  33. package/src/app/wallets/page.tsx +105 -5
  34. package/src/cli/index.js +21 -33
  35. package/src/cli/spec.js +19 -30
  36. package/src/components/agents/agent-chat-list.tsx +23 -1
  37. package/src/components/agents/agent-sheet.tsx +2 -40
  38. package/src/components/agents/inspector-panel.tsx +165 -131
  39. package/src/components/chat/chat-area.tsx +38 -9
  40. package/src/components/chat/chat-card.tsx +0 -31
  41. package/src/components/chat/message-bubble.tsx +1 -108
  42. package/src/components/chat/message-list.tsx +33 -19
  43. package/src/components/connectors/connector-sheet.tsx +25 -1
  44. package/src/components/gateways/gateway-sheet.tsx +5 -2
  45. package/src/components/layout/sidebar-rail.tsx +6 -10
  46. package/src/components/projects/project-detail.tsx +3 -35
  47. package/src/components/projects/tabs/overview-tab.tsx +3 -59
  48. package/src/components/projects/tabs/work-tab.tsx +7 -77
  49. package/src/components/protocols/structured-session-launcher.tsx +1 -22
  50. package/src/components/shared/connector-platform-icon.tsx +1 -0
  51. package/src/components/tasks/task-card.tsx +4 -34
  52. package/src/components/tasks/task-sheet.tsx +6 -36
  53. package/src/components/wallets/wallet-list.tsx +150 -0
  54. package/src/lib/agent-execute-defaults.test.ts +24 -0
  55. package/src/lib/agent-execute-defaults.ts +62 -0
  56. package/src/lib/app/navigation.test.ts +0 -13
  57. package/src/lib/app/navigation.ts +2 -7
  58. package/src/lib/app/view-constants.ts +14 -19
  59. package/src/lib/chat/queued-message-queue.test.ts +134 -1
  60. package/src/lib/chat/queued-message-queue.ts +77 -2
  61. package/src/lib/server/agents/agent-service.ts +5 -0
  62. package/src/lib/server/agents/agent-thread-session.ts +0 -1
  63. package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
  64. package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
  65. package/src/lib/server/agents/delegation-jobs.ts +0 -25
  66. package/src/lib/server/agents/main-agent-loop.ts +1 -49
  67. package/src/lib/server/agents/subagent-runtime.ts +0 -1
  68. package/src/lib/server/approval-match.ts +0 -85
  69. package/src/lib/server/approvals.test.ts +6 -6
  70. package/src/lib/server/approvals.ts +0 -6
  71. package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
  72. package/src/lib/server/builtin-extensions.ts +1 -2
  73. package/src/lib/server/capability-router.test.ts +0 -2
  74. package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +1 -1
  75. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +15 -14
  76. package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
  77. package/src/lib/server/chat-execution/chat-execution-utils.ts +2 -4
  78. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
  79. package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
  80. package/src/lib/server/chat-execution/chat-turn-preparation.ts +81 -64
  81. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -0
  82. package/src/lib/server/chat-execution/continuation-evaluator.ts +8 -0
  83. package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
  84. package/src/lib/server/chat-execution/memory-mutation-tools.ts +1 -1
  85. package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
  86. package/src/lib/server/chat-execution/message-classifier.ts +11 -16
  87. package/src/lib/server/chat-execution/prompt-builder.test.ts +27 -0
  88. package/src/lib/server/chat-execution/prompt-builder.ts +14 -31
  89. package/src/lib/server/chat-execution/prompt-mode.test.ts +24 -0
  90. package/src/lib/server/chat-execution/prompt-mode.ts +5 -1
  91. package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
  92. package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
  93. package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
  94. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +13 -126
  95. package/src/lib/server/chat-execution/stream-agent-chat.ts +46 -21
  96. package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
  97. package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
  98. package/src/lib/server/chatrooms/chatroom-routing.test.ts +4 -0
  99. package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
  100. package/src/lib/server/chats/chat-session-service.ts +3 -5
  101. package/src/lib/server/connectors/connector-inbound.ts +0 -1
  102. package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
  103. package/src/lib/server/connectors/connector-service.ts +39 -9
  104. package/src/lib/server/connectors/discord.ts +2 -2
  105. package/src/lib/server/connectors/matrix.ts +3 -2
  106. package/src/lib/server/connectors/signal.ts +5 -4
  107. package/src/lib/server/connectors/slack.ts +10 -9
  108. package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
  109. package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
  110. package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
  111. package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
  112. package/src/lib/server/connectors/swarmdock-tasks.ts +119 -0
  113. package/src/lib/server/connectors/swarmdock.ts +255 -0
  114. package/src/lib/server/connectors/teams.ts +3 -2
  115. package/src/lib/server/connectors/telegram.ts +4 -4
  116. package/src/lib/server/connectors/whatsapp.ts +2 -2
  117. package/src/lib/server/daemon/controller.ts +7 -0
  118. package/src/lib/server/execution-brief.test.ts +2 -25
  119. package/src/lib/server/execution-brief.ts +12 -35
  120. package/src/lib/server/execution-engine/task-attempt.ts +0 -1
  121. package/src/lib/server/gateways/gateway-profile-service.ts +19 -1
  122. package/src/lib/server/messages/message-repository.test.ts +70 -0
  123. package/src/lib/server/messages/message-repository.ts +11 -6
  124. package/src/lib/server/openclaw/deploy.ts +32 -2
  125. package/src/lib/server/persistence/storage-context.ts +0 -5
  126. package/src/lib/server/plugins-advanced.test.ts +1 -2
  127. package/src/lib/server/portability/export.ts +109 -0
  128. package/src/lib/server/portability/import.ts +159 -0
  129. package/src/lib/server/protocols/protocol-normalization.ts +0 -4
  130. package/src/lib/server/protocols/protocol-queries.ts +0 -6
  131. package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
  132. package/src/lib/server/protocols/protocol-service.ts +0 -1
  133. package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
  134. package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
  135. package/src/lib/server/protocols/protocol-swarm.ts +0 -2
  136. package/src/lib/server/protocols/protocol-types.ts +0 -2
  137. package/src/lib/server/provider-health.ts +1 -10
  138. package/src/lib/server/runtime/daemon-state/core.ts +0 -9
  139. package/src/lib/server/runtime/daemon-state.test.ts +0 -35
  140. package/src/lib/server/runtime/heartbeat-service.ts +3 -23
  141. package/src/lib/server/runtime/process-manager.ts +13 -9
  142. package/src/lib/server/runtime/queue/core.ts +11 -33
  143. package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
  144. package/src/lib/server/runtime/scheduler.ts +0 -13
  145. package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
  146. package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
  147. package/src/lib/server/runtime/session-run-manager/queries.ts +15 -1
  148. package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
  149. package/src/lib/server/runtime/session-run-manager.test.ts +58 -28
  150. package/src/lib/server/sandbox/session-runtime.test.ts +18 -1
  151. package/src/lib/server/sandbox/session-runtime.ts +40 -28
  152. package/src/lib/server/session-tools/autonomy-tools.test.ts +7 -9
  153. package/src/lib/server/session-tools/context.ts +1 -1
  154. package/src/lib/server/session-tools/credential-env.ts +109 -0
  155. package/src/lib/server/session-tools/crud.ts +3 -17
  156. package/src/lib/server/session-tools/delegate.ts +0 -4
  157. package/src/lib/server/session-tools/edit_file.ts +3 -2
  158. package/src/lib/server/session-tools/execute.test.ts +58 -0
  159. package/src/lib/server/session-tools/execute.ts +334 -0
  160. package/src/lib/server/session-tools/files-tool.ts +635 -0
  161. package/src/lib/server/session-tools/index.ts +14 -8
  162. package/src/lib/server/session-tools/memory-tool.ts +242 -0
  163. package/src/lib/server/session-tools/memory.ts +1 -1
  164. package/src/lib/server/session-tools/openclaw-nodes.ts +3 -2
  165. package/src/lib/server/session-tools/openclaw-workspace.ts +3 -2
  166. package/src/lib/server/session-tools/platform-tool.ts +617 -0
  167. package/src/lib/server/session-tools/session-info.ts +3 -2
  168. package/src/lib/server/session-tools/session-tools-wiring.test.ts +3 -4
  169. package/src/lib/server/session-tools/shell.ts +7 -122
  170. package/src/lib/server/session-tools/skills-tool.ts +396 -0
  171. package/src/lib/server/session-tools/team-context.ts +0 -3
  172. package/src/lib/server/session-tools/web.ts +2 -2
  173. package/src/lib/server/storage-normalization.ts +10 -0
  174. package/src/lib/server/storage.ts +18 -45
  175. package/src/lib/server/tasks/task-checkout.ts +59 -0
  176. package/src/lib/server/tasks/task-lifecycle.ts +2 -0
  177. package/src/lib/server/tasks/task-route-service.ts +4 -26
  178. package/src/lib/server/tasks/task-service.ts +0 -7
  179. package/src/lib/server/tool-aliases.ts +2 -2
  180. package/src/lib/server/tool-capability-policy-advanced.test.ts +13 -6
  181. package/src/lib/server/tool-capability-policy.test.ts +2 -1
  182. package/src/lib/server/tool-capability-policy.ts +60 -35
  183. package/src/lib/server/tool-planning.ts +11 -12
  184. package/src/lib/server/universal-tool-access.ts +0 -1
  185. package/src/lib/server/wallets/wallet-crypto.ts +33 -0
  186. package/src/lib/server/wallets/wallet-repository.ts +24 -0
  187. package/src/lib/server/wallets/wallet-service.ts +119 -0
  188. package/src/lib/server/working-state/extraction.ts +8 -42
  189. package/src/lib/server/working-state/normalization.ts +10 -103
  190. package/src/lib/server/working-state/service.ts +12 -21
  191. package/src/lib/setup-defaults.ts +5 -0
  192. package/src/lib/strip-internal-metadata.test.ts +1 -1
  193. package/src/lib/strip-internal-metadata.ts +1 -1
  194. package/src/lib/tool-definitions.ts +1 -1
  195. package/src/lib/validation/schemas.test.ts +16 -0
  196. package/src/lib/validation/schemas.ts +49 -2
  197. package/src/stores/slices/data-slice.ts +5 -1
  198. package/src/stores/slices/ui-slice.ts +0 -4
  199. package/src/stores/use-chat-store.test.ts +231 -0
  200. package/src/stores/use-chat-store.ts +62 -13
  201. package/src/types/agent.ts +264 -0
  202. package/src/types/app-settings.ts +173 -0
  203. package/src/types/approval.ts +25 -0
  204. package/src/types/connector.ts +188 -0
  205. package/src/types/extension.ts +386 -0
  206. package/src/types/index.ts +16 -3555
  207. package/src/types/message.ts +56 -0
  208. package/src/types/misc.ts +737 -0
  209. package/src/types/protocol.ts +420 -0
  210. package/src/types/provider.ts +52 -0
  211. package/src/types/run.ts +180 -0
  212. package/src/types/schedule.ts +59 -0
  213. package/src/types/session.ts +215 -0
  214. package/src/types/skill.ts +157 -0
  215. package/src/types/swarmdock.ts +29 -0
  216. package/src/types/task.ts +144 -0
  217. package/src/types/working-state.ts +204 -0
  218. package/src/views/settings/section-heartbeat.tsx +2 -2
  219. package/src/views/settings/section-runtime-loop.tsx +0 -14
  220. package/src/app/api/canvas/[sessionId]/route.ts +0 -35
  221. package/src/app/api/missions/[id]/actions/route.ts +0 -31
  222. package/src/app/api/missions/[id]/events/route.ts +0 -14
  223. package/src/app/api/missions/[id]/route.ts +0 -10
  224. package/src/app/api/missions/route.test.ts +0 -244
  225. package/src/app/api/missions/route.ts +0 -57
  226. package/src/app/api/wallets/[id]/approve/route.ts +0 -79
  227. package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
  228. package/src/app/api/wallets/[id]/send/route.ts +0 -113
  229. package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
  230. package/src/app/missions/[id]/page.tsx +0 -3
  231. package/src/app/missions/page.tsx +0 -685
  232. package/src/components/canvas/canvas-panel.tsx +0 -267
  233. package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
  234. package/src/components/wallets/wallet-panel.tsx +0 -1010
  235. package/src/components/wallets/wallet-section.tsx +0 -260
  236. package/src/features/missions/queries.ts +0 -23
  237. package/src/lib/canvas-content.test.ts +0 -360
  238. package/src/lib/canvas-content.ts +0 -198
  239. package/src/lib/server/canvas-content.test.ts +0 -32
  240. package/src/lib/server/canvas-content.ts +0 -6
  241. package/src/lib/server/ethereum.ts +0 -591
  242. package/src/lib/server/evm-swap.ts +0 -476
  243. package/src/lib/server/missions/mission-intent.test.ts +0 -63
  244. package/src/lib/server/missions/mission-intent.ts +0 -569
  245. package/src/lib/server/missions/mission-repository.ts +0 -74
  246. package/src/lib/server/missions/mission-service/actions.ts +0 -6
  247. package/src/lib/server/missions/mission-service/bindings.ts +0 -9
  248. package/src/lib/server/missions/mission-service/context.ts +0 -4
  249. package/src/lib/server/missions/mission-service/core.ts +0 -2271
  250. package/src/lib/server/missions/mission-service/queries.ts +0 -12
  251. package/src/lib/server/missions/mission-service/recovery.ts +0 -5
  252. package/src/lib/server/missions/mission-service/ticks.ts +0 -9
  253. package/src/lib/server/missions/mission-service.test.ts +0 -888
  254. package/src/lib/server/missions/mission-service.ts +0 -6
  255. package/src/lib/server/session-tools/canvas.ts +0 -105
  256. package/src/lib/server/session-tools/sandbox.ts +0 -281
  257. package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
  258. package/src/lib/server/session-tools/wallet.ts +0 -1287
  259. package/src/lib/server/solana.ts +0 -327
  260. package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
  261. package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
  262. package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
  263. package/src/lib/server/wallet/wallet-service.test.ts +0 -81
  264. package/src/lib/server/wallet/wallet-service.ts +0 -225
  265. package/src/lib/wallet/wallet-transactions.test.ts +0 -75
  266. package/src/lib/wallet/wallet-transactions.ts +0 -43
  267. package/src/lib/wallet/wallet.test.ts +0 -333
  268. package/src/lib/wallet/wallet.ts +0 -183
  269. package/src/views/settings/section-wallets.tsx +0 -35
@@ -41,6 +41,21 @@ const AgentSandboxConfigSchema = z.object({
41
41
  prune: AgentSandboxPruneSchema,
42
42
  }).nullable().optional()
43
43
 
44
+ const AgentExecuteConfigSchema = z.object({
45
+ backend: z.enum(['sandbox', 'host']).optional(),
46
+ network: z.object({
47
+ enabled: z.boolean(),
48
+ allowedUrls: z.array(z.string()).optional(),
49
+ }).optional(),
50
+ runtimes: z.object({
51
+ python: z.boolean().optional(),
52
+ javascript: z.boolean().optional(),
53
+ sqlite: z.boolean().optional(),
54
+ }).optional(),
55
+ timeout: z.number().int().positive().optional(),
56
+ credentials: z.array(z.string()).optional(),
57
+ }).nullable().optional()
58
+
44
59
  const AgentRoutingTargetSchema = z.object({
45
60
  id: z.string().min(1),
46
61
  label: z.string().optional(),
@@ -110,6 +125,7 @@ export const AgentCreateSchema = z.object({
110
125
  avatarSeed: z.string().optional(),
111
126
  avatarUrl: z.string().nullable().optional().default(null),
112
127
  sandboxConfig: AgentSandboxConfigSchema,
128
+ executeConfig: AgentExecuteConfigSchema,
113
129
  autoRecovery: z.boolean().optional().default(false),
114
130
  monthlyBudget: z.number().positive().nullable().optional().default(null),
115
131
  dailyBudget: z.number().positive().nullable().optional().default(null),
@@ -121,7 +137,7 @@ export const ConnectorCreateSchema = z.object({
121
137
  name: z.string().min(1, 'Connector name is required').optional(),
122
138
  platform: z.enum([
123
139
  'discord', 'telegram', 'slack', 'whatsapp', 'openclaw',
124
- 'bluebubbles', 'signal', 'teams', 'googlechat', 'matrix', 'email',
140
+ 'bluebubbles', 'signal', 'teams', 'googlechat', 'matrix', 'email', 'swarmdock',
125
141
  ]),
126
142
  agentId: z.string().nullable().optional().default(null),
127
143
  chatroomId: z.string().nullable().optional().default(null),
@@ -380,7 +396,6 @@ export const ProtocolRunCreateSchema = z.object({
380
396
  participantAgentIds: z.array(z.string()).min(1, 'Select at least one participant').default([]),
381
397
  facilitatorAgentId: z.string().nullable().optional().default(null),
382
398
  observerAgentIds: z.array(z.string()).optional().default([]),
383
- missionId: z.string().nullable().optional().default(null),
384
399
  taskId: z.string().nullable().optional().default(null),
385
400
  sessionId: z.string().nullable().optional().default(null),
386
401
  parentChatroomId: z.string().nullable().optional().default(null),
@@ -457,6 +472,38 @@ export const ProtocolRunActionSchema = z.object({
457
472
  workItemId: z.string().nullable().optional().default(null),
458
473
  })
459
474
 
475
+ const PortableAgentSchema = z.object({
476
+ originalId: z.string().min(1),
477
+ name: z.string().min(1),
478
+ description: z.string(),
479
+ systemPrompt: z.string(),
480
+ provider: z.string().min(1),
481
+ model: z.string().min(1),
482
+ createdAt: z.number().optional(),
483
+ updatedAt: z.number().optional(),
484
+ skillIds: z.array(z.string()).optional(),
485
+ }).passthrough()
486
+
487
+ const PortableSkillSchema = z.object({
488
+ originalId: z.string().min(1),
489
+ name: z.string().min(1),
490
+ content: z.string(),
491
+ }).passthrough()
492
+
493
+ const PortableScheduleSchema = z.object({
494
+ originalId: z.string().min(1),
495
+ originalAgentId: z.string().min(1),
496
+ name: z.string().min(1),
497
+ }).passthrough()
498
+
499
+ export const PortableManifestSchema = z.object({
500
+ formatVersion: z.number().int().nonnegative(),
501
+ exportedAt: z.string().optional(),
502
+ agents: z.array(PortableAgentSchema),
503
+ skills: z.array(PortableSkillSchema),
504
+ schedules: z.array(PortableScheduleSchema),
505
+ })
506
+
460
507
  /** Format ZodError into a 400-friendly payload */
461
508
  export function formatZodError(err: z.ZodError) {
462
509
  return { error: 'Validation failed', issues: err.issues.map((i) => ({ path: i.path.join('.'), message: i.message })) }
@@ -1,6 +1,6 @@
1
1
  import { StateCreator } from 'zustand'
2
2
  import type { AppState } from '../use-app-store'
3
- import type { NetworkInfo, Directory, ProviderInfo, Credentials, Schedule, AppSettings, StoredSecret, ProviderConfig, Skill, Connector, Webhook, McpServerConfig, ExtensionMeta, Project, ActivityEntry, AppNotification, GatewayProfile } from '../../types'
3
+ import type { NetworkInfo, Directory, ProviderInfo, Credentials, Schedule, AppSettings, StoredSecret, ProviderConfig, Skill, Connector, Webhook, McpServerConfig, ExtensionMeta, Project, ActivityEntry, AppNotification, GatewayProfile, SafeWallet } from '../../types'
4
4
  import { api } from '@/lib/app/api-client'
5
5
  import { safeStorageGetJson, safeStorageSet } from '@/lib/app/safe-storage'
6
6
  import { fetchDirs, fetchProviders, fetchCredentials } from '@/lib/chat/chats'
@@ -24,6 +24,8 @@ export interface DataSlice {
24
24
  updateSettings: (patch: Partial<AppSettings>) => Promise<void>
25
25
  secrets: Record<string, StoredSecret>
26
26
  loadSecrets: () => Promise<void>
27
+ wallets: Record<string, SafeWallet>
28
+ loadWallets: () => Promise<void>
27
29
  providerConfigs: ProviderConfig[]
28
30
  loadProviderConfigs: () => Promise<void>
29
31
  gatewayProfiles: GatewayProfile[]
@@ -78,6 +80,8 @@ export const createDataSlice: StateCreator<AppState, [], [], DataSlice> = (set,
78
80
  },
79
81
  secrets: {},
80
82
  loadSecrets: createLoader<AppState>(set, 'secrets', () => api<Record<string, StoredSecret>>('GET', '/secrets'), {}),
83
+ wallets: {},
84
+ loadWallets: createLoader<AppState>(set, 'wallets', () => api<Record<string, SafeWallet>>('GET', '/wallets'), {}),
81
85
  providerConfigs: [],
82
86
  loadProviderConfigs: createLoader<AppState>(set, 'providerConfigs', () => api<ProviderConfig[]>('GET', '/providers/configs'), []),
83
87
  gatewayProfiles: [],
@@ -95,8 +95,6 @@ export interface UiSlice {
95
95
  setFleetFilter: (filter: FleetFilter) => void
96
96
  chatFilter: 'all' | 'active' | 'recent'
97
97
  setChatFilter: (filter: 'all' | 'active' | 'recent') => void
98
- walletPanelAgentId: string | null
99
- setWalletPanelAgentId: (id: string | null) => void
100
98
  heartbeatHistoryOpen: boolean
101
99
  setHeartbeatHistoryOpen: (open: boolean) => void
102
100
  agentPrefill: Partial<Agent> | null
@@ -195,8 +193,6 @@ export const createUiSlice: StateCreator<AppState, [], [], UiSlice> = (set, get)
195
193
  setFleetFilter: (filter) => { safeStorageSet('sc_fleet_filter', filter); set({ fleetFilter: filter }) },
196
194
  chatFilter: 'all' as const,
197
195
  setChatFilter: (filter) => set({ chatFilter: filter }),
198
- walletPanelAgentId: null,
199
- setWalletPanelAgentId: (id) => set({ walletPanelAgentId: id }),
200
196
  heartbeatHistoryOpen: false,
201
197
  setHeartbeatHistoryOpen: (open) => set({ heartbeatHistoryOpen: open }),
202
198
  agentPrefill: null,
@@ -451,6 +451,103 @@ describe('useChatStore control-token hygiene', () => {
451
451
  assert.equal(useAppStore.getState().sessions['session-1']?.currentRunId, 'run-active')
452
452
  })
453
453
 
454
+ it('hydrates the active turn from the backend queue snapshot as a sending placeholder', async () => {
455
+ const now = Date.now()
456
+ const session = makeSession()
457
+ useAppStore.setState({
458
+ agents: { 'agent-1': makeAgent() },
459
+ sessions: { [session.id]: session },
460
+ currentAgentId: 'agent-1',
461
+ })
462
+ useChatStore.setState({
463
+ messages: [],
464
+ pendingFiles: [],
465
+ replyingTo: null,
466
+ toolEvents: [],
467
+ streamText: '',
468
+ displayText: '',
469
+ streaming: false,
470
+ streamingSessionId: null,
471
+ streamSource: null,
472
+ assistantRenderId: null,
473
+ streamPhase: 'thinking',
474
+ streamToolName: '',
475
+ thinkingText: '',
476
+ thinkingStartTime: 0,
477
+ queuedMessages: [],
478
+ agentStatus: null,
479
+ lastUsage: null,
480
+ hasMoreMessages: false,
481
+ loadingMore: false,
482
+ totalMessages: 0,
483
+ })
484
+
485
+ global.fetch = (async (input: RequestInfo | URL) => {
486
+ const url = String(input)
487
+ if (url === '/api/chats/session-1/queue') {
488
+ return jsonResponse({
489
+ sessionId: 'session-1',
490
+ activeRunId: 'run-active',
491
+ activeTurn: {
492
+ runId: 'run-active',
493
+ sessionId: 'session-1',
494
+ text: 'Already running',
495
+ queuedAt: now,
496
+ position: 0,
497
+ },
498
+ queueLength: 1,
499
+ items: [
500
+ { runId: 'run-queued-2', sessionId: 'session-1', text: 'Then refine it', queuedAt: now + 1, position: 1 },
501
+ ],
502
+ })
503
+ }
504
+ throw new Error(`Unexpected fetch: ${url}`)
505
+ }) as unknown as typeof fetch
506
+
507
+ await useChatStore.getState().loadQueuedMessages('session-1')
508
+
509
+ const state = useChatStore.getState()
510
+ assert.deepEqual(
511
+ state.queuedMessages.map((item) => [item.runId, item.sending === true]),
512
+ [['run-active', true], ['run-queued-2', false]],
513
+ )
514
+ assert.equal(useAppStore.getState().sessions['session-1']?.queuedCount, 1)
515
+ assert.equal(useAppStore.getState().sessions['session-1']?.currentRunId, 'run-active')
516
+ })
517
+
518
+ it('clears sending placeholders when a persisted user message with the same runId arrives', () => {
519
+ useChatStore.setState({
520
+ messages: [],
521
+ assistantRenderId: null,
522
+ queuedMessages: [
523
+ { runId: 'run-active', sessionId: 'session-1', text: 'Already running', queuedAt: 9, position: 0, sending: true },
524
+ ],
525
+ toolEvents: [],
526
+ streamText: '',
527
+ displayText: '',
528
+ streaming: false,
529
+ streamingSessionId: null,
530
+ streamSource: null,
531
+ streamPhase: 'thinking',
532
+ streamToolName: '',
533
+ thinkingText: '',
534
+ thinkingStartTime: 0,
535
+ agentStatus: null,
536
+ lastUsage: null,
537
+ hasMoreMessages: false,
538
+ loadingMore: false,
539
+ totalMessages: 0,
540
+ })
541
+
542
+ useChatStore.getState().setMessages([
543
+ { role: 'user', text: 'Already running', time: 10, runId: 'run-active' },
544
+ ])
545
+
546
+ const state = useChatStore.getState()
547
+ assert.equal(state.queuedMessages.length, 0)
548
+ assert.equal(state.messages[0]?.runId, 'run-active')
549
+ })
550
+
454
551
  it('removes optimistic queued items again when the backend enqueue fails', async () => {
455
552
  const session = makeSession()
456
553
  useAppStore.setState({
@@ -533,4 +630,138 @@ describe('useChatStore control-token hygiene', () => {
533
630
  assert.equal(state.assistantRenderId, 'render-1')
534
631
  assert.equal(state.messages[1]?.clientRenderId, 'render-1')
535
632
  })
633
+
634
+ it('preserves transcript totals on local message updates for paginated windows', () => {
635
+ useChatStore.setState({
636
+ messages: [
637
+ { role: 'user', text: 'Ninth', time: 9, bookmarked: false },
638
+ { role: 'assistant', text: 'Tenth', time: 10 },
639
+ ],
640
+ messageStartIndex: 8,
641
+ assistantRenderId: null,
642
+ toolEvents: [],
643
+ streamText: '',
644
+ displayText: '',
645
+ streaming: false,
646
+ streamingSessionId: null,
647
+ streamSource: null,
648
+ streamPhase: 'thinking',
649
+ streamToolName: '',
650
+ thinkingText: '',
651
+ thinkingStartTime: 0,
652
+ queuedMessages: [],
653
+ agentStatus: null,
654
+ lastUsage: null,
655
+ hasMoreMessages: true,
656
+ loadingMore: false,
657
+ totalMessages: 10,
658
+ })
659
+
660
+ useChatStore.getState().setMessages([
661
+ { role: 'user', text: 'Ninth', time: 9, bookmarked: true },
662
+ { role: 'assistant', text: 'Tenth', time: 10 },
663
+ ])
664
+
665
+ const state = useChatStore.getState()
666
+ assert.equal(state.messageStartIndex, 8)
667
+ assert.equal(state.totalMessages, 10)
668
+ assert.equal(state.hasMoreMessages, true)
669
+ assert.equal(state.messages[0]?.bookmarked, true)
670
+ })
671
+
672
+ it('does not timeout sending items before 60 seconds', () => {
673
+ const thirtySecondsAgo = Date.now() - 30_000
674
+ useChatStore.setState({
675
+ messages: [],
676
+ assistantRenderId: null,
677
+ toolEvents: [],
678
+ streamText: '',
679
+ displayText: '',
680
+ streaming: false,
681
+ streamingSessionId: null,
682
+ streamSource: null,
683
+ streamPhase: 'thinking',
684
+ streamToolName: '',
685
+ thinkingText: '',
686
+ thinkingStartTime: 0,
687
+ queuedMessages: [
688
+ { runId: 'run-old', sessionId: 'session-1', text: 'Waiting', queuedAt: thirtySecondsAgo, position: 0, sending: true },
689
+ ],
690
+ agentStatus: null,
691
+ lastUsage: null,
692
+ hasMoreMessages: false,
693
+ loadingMore: false,
694
+ totalMessages: 0,
695
+ })
696
+
697
+ // setMessages with no matching persisted message — item should survive
698
+ useChatStore.getState().setMessages([
699
+ { role: 'user', text: 'Unrelated', time: Date.now() },
700
+ ])
701
+
702
+ const state = useChatStore.getState()
703
+ assert.equal(state.queuedMessages.length, 1)
704
+ assert.equal(state.queuedMessages[0]?.runId, 'run-old')
705
+ })
706
+
707
+ it('tracks messageStartIndex when loading more paginated history', async () => {
708
+ const session = makeSession()
709
+ useAppStore.setState({
710
+ agents: { 'agent-1': makeAgent() },
711
+ sessions: { [session.id]: session },
712
+ currentAgentId: 'agent-1',
713
+ })
714
+ useChatStore.setState({
715
+ messages: [
716
+ { role: 'user', text: 'Ninth', time: 9 },
717
+ { role: 'assistant', text: 'Tenth', time: 10 },
718
+ ],
719
+ messageStartIndex: 8,
720
+ pendingFiles: [],
721
+ replyingTo: null,
722
+ toolEvents: [],
723
+ streamText: '',
724
+ displayText: '',
725
+ streaming: false,
726
+ streamingSessionId: null,
727
+ streamSource: null,
728
+ assistantRenderId: null,
729
+ streamPhase: 'thinking',
730
+ streamToolName: '',
731
+ thinkingText: '',
732
+ thinkingStartTime: 0,
733
+ queuedMessages: [],
734
+ agentStatus: null,
735
+ lastUsage: null,
736
+ hasMoreMessages: true,
737
+ loadingMore: false,
738
+ totalMessages: 10,
739
+ })
740
+
741
+ global.fetch = (async (input: RequestInfo | URL) => {
742
+ const url = String(input)
743
+ if (url === '/api/chats/session-1/messages?limit=100&before=8') {
744
+ return jsonResponse({
745
+ messages: [
746
+ { role: 'user', text: 'Seventh', time: 7 },
747
+ { role: 'assistant', text: 'Eighth', time: 8 },
748
+ ],
749
+ total: 10,
750
+ hasMore: true,
751
+ startIndex: 6,
752
+ })
753
+ }
754
+ throw new Error(`Unexpected fetch: ${url}`)
755
+ }) as unknown as typeof fetch
756
+
757
+ await useChatStore.getState().loadMoreMessages()
758
+
759
+ const state = useChatStore.getState()
760
+ assert.equal(state.messageStartIndex, 6)
761
+ assert.equal(state.totalMessages, 10)
762
+ assert.deepEqual(
763
+ state.messages.map((message) => message.text),
764
+ ['Seventh', 'Eighth', 'Ninth', 'Tenth'],
765
+ )
766
+ })
536
767
  })
@@ -66,7 +66,8 @@ interface ChatState {
66
66
  agentStatus: { goal?: string; status?: string; summary?: string; nextAction?: string } | null
67
67
 
68
68
  messages: Message[]
69
- setMessages: (msgs: Message[]) => void
69
+ messageStartIndex: number
70
+ setMessages: (msgs: Message[], options?: { startIndex?: number; totalMessages?: number }) => void
70
71
 
71
72
  toolEvents: ToolEvent[]
72
73
  clearToolEvents: () => void
@@ -136,6 +137,10 @@ interface ChatState {
136
137
  loadMoreMessages: () => Promise<void>
137
138
  }
138
139
 
140
+ /** Safety-net timeout for "sending" queue items. Normally cleaned up by
141
+ * matchesPersistedQueuedMessage well before this — only fires if matching fails. */
142
+ const SENDING_ITEM_TIMEOUT_MS = 60_000
143
+
139
144
  const CONTROL_TOKEN_PREFIX_RE = /^\s*(?:NO_MESSAGE|HEARTBEAT_OK)(?:(?=[\s.,:;!?()[\]{}"'`-]|$)|(?=[A-Z]))\s*/i
140
145
  const CONTROL_TOKEN_LINE_RE = /(^|\n)\s*(?:NO_MESSAGE|HEARTBEAT_OK)\s*(\n|$)/gi
141
146
 
@@ -165,6 +170,29 @@ function reconcileMessagesForState(
165
170
  return { messages, assistantRenderId: nextAssistantRenderId }
166
171
  }
167
172
 
173
+ function attachedFilesEqual(left: string[] | undefined, right: string[] | undefined): boolean {
174
+ if (!left?.length && !right?.length) return true
175
+ if ((left?.length || 0) !== (right?.length || 0)) return false
176
+ for (let index = 0; index < (left?.length || 0); index += 1) {
177
+ if (left?.[index] !== right?.[index]) return false
178
+ }
179
+ return true
180
+ }
181
+
182
+ function matchesPersistedQueuedMessage(message: Message, queued: QueuedSessionMessage): boolean {
183
+ if (message.role !== 'user') return false
184
+ const messageRunId = typeof message.runId === 'string' && message.runId.trim() ? message.runId : null
185
+ const queuedRunId = typeof queued.runId === 'string' && queued.runId.trim() ? queued.runId : null
186
+ if (messageRunId && queuedRunId) return messageRunId === queuedRunId
187
+ return (
188
+ message.text === queued.text
189
+ && message.replyToId === queued.replyToId
190
+ && message.imagePath === queued.imagePath
191
+ && message.imageUrl === queued.imageUrl
192
+ && attachedFilesEqual(message.attachedFiles, queued.attachedFiles)
193
+ )
194
+ }
195
+
168
196
  function syncSessionQueueState(sessionId: string, params: {
169
197
  queuedCount: number
170
198
  currentRunId?: string | null
@@ -203,13 +231,14 @@ export const useChatStore = create<ChatState>((set, get) => ({
203
231
  displayText: '',
204
232
  agentStatus: null,
205
233
  messages: [],
206
- setMessages: (msgs) => set((s) => {
234
+ messageStartIndex: 0,
235
+ setMessages: (msgs, options) => set((s) => {
207
236
  const next = reconcileMessagesForState(msgs, s.messages, s.assistantRenderId)
208
237
  // Clear "sending" queue items whose text now appears in the message list
209
238
  const queuedMessages = s.queuedMessages.filter((item) => {
210
239
  if (!item.sending) return true
211
- if (next.messages.some((m) => m.role === 'user' && m.text === item.text)) return false
212
- if (Date.now() - item.queuedAt > 15_000) return false
240
+ if (next.messages.some((message) => matchesPersistedQueuedMessage(message, item))) return false
241
+ if (Date.now() - item.queuedAt > SENDING_ITEM_TIMEOUT_MS) return false
213
242
  return true
214
243
  })
215
244
  const patch: Partial<ChatState> = {
@@ -217,9 +246,29 @@ export const useChatStore = create<ChatState>((set, get) => ({
217
246
  assistantRenderId: next.assistantRenderId,
218
247
  queuedMessages,
219
248
  }
249
+ if (typeof options?.startIndex === 'number' && Number.isFinite(options.startIndex)) {
250
+ patch.messageStartIndex = Math.max(0, Math.trunc(options.startIndex))
251
+ } else if (next.messages.length === 0) {
252
+ patch.messageStartIndex = 0
253
+ }
220
254
  if (s.toolEvents.length > 0) patch.toolEvents = []
221
- if (s.hasMoreMessages) patch.hasMoreMessages = false
222
- if (s.totalMessages !== next.messages.length) patch.totalMessages = next.messages.length
255
+ if (next.messages.length === 0) {
256
+ patch.hasMoreMessages = false
257
+ } else if (
258
+ typeof options?.startIndex === 'number'
259
+ && Number.isFinite(options.startIndex)
260
+ && Math.trunc(options.startIndex) === 0
261
+ && typeof options?.totalMessages === 'number'
262
+ && Number.isFinite(options.totalMessages)
263
+ && Math.max(0, Math.trunc(options.totalMessages)) === next.messages.length
264
+ ) {
265
+ patch.hasMoreMessages = false
266
+ }
267
+ if (typeof options?.totalMessages === 'number' && Number.isFinite(options.totalMessages)) {
268
+ patch.totalMessages = Math.max(0, Math.trunc(options.totalMessages))
269
+ } else if (next.messages.length === 0 && s.totalMessages !== 0) {
270
+ patch.totalMessages = 0
271
+ }
223
272
  return patch
224
273
  }),
225
274
  toolEvents: [],
@@ -253,8 +302,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
253
302
  const messages = s.messages
254
303
  const cleaned = next.filter((item) => {
255
304
  if (!item.sending || item.sessionId !== sessionId) return true
256
- if (messages.some((m) => m.role === 'user' && m.text === item.text)) return false
257
- if (Date.now() - item.queuedAt > 15_000) return false
305
+ if (messages.some((message) => matchesPersistedQueuedMessage(message, item))) return false
306
+ if (Date.now() - item.queuedAt > SENDING_ITEM_TIMEOUT_MS) return false
258
307
  return true
259
308
  })
260
309
  return { queuedMessages: cleaned }
@@ -675,7 +724,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
675
724
  })
676
725
  if (msgsRes.ok) {
677
726
  const msgs = await msgsRes.json()
678
- get().setMessages(msgs)
727
+ get().setMessages(msgs, { startIndex: 0, totalMessages: msgs.length })
679
728
  }
680
729
  // Re-send with the new text
681
730
  await get().sendMessage(newText)
@@ -703,7 +752,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
703
752
  })
704
753
  if (msgsRes.ok) {
705
754
  const msgs = await msgsRes.json()
706
- get().setMessages(msgs)
755
+ get().setMessages(msgs, { startIndex: 0, totalMessages: msgs.length })
707
756
  }
708
757
  // Re-send the last user message through the normal SSE flow
709
758
  if (imagePath) {
@@ -817,15 +866,14 @@ export const useChatStore = create<ChatState>((set, get) => ({
817
866
  loadingMore: false,
818
867
  totalMessages: 0,
819
868
  loadMoreMessages: async () => {
820
- const { messages, loadingMore, hasMoreMessages, totalMessages } = get()
869
+ const { loadingMore, hasMoreMessages, messageStartIndex } = get()
821
870
  if (loadingMore || !hasMoreMessages) return
822
871
  const sessionId = selectActiveSessionId(useAppStore.getState())
823
872
  if (!sessionId) return
824
873
  set({ loadingMore: true })
825
874
  try {
826
875
  const key = getStoredAccessKey()
827
- // Find the earliest message's original index (startIndex tracked on initial load)
828
- const currentStartIndex = totalMessages - messages.length
876
+ const currentStartIndex = messageStartIndex
829
877
  const res = await fetch(`/api/chats/${sessionId}/messages?limit=100&before=${currentStartIndex}`, {
830
878
  headers: key ? { 'X-Access-Key': key } : undefined,
831
879
  })
@@ -840,6 +888,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
840
888
  return {
841
889
  messages: next.messages,
842
890
  assistantRenderId: next.assistantRenderId,
891
+ messageStartIndex: data.startIndex,
843
892
  hasMoreMessages: data.hasMore,
844
893
  totalMessages: data.total,
845
894
  loadingMore: false,