@swarmclawai/swarmclaw 0.7.8 → 0.8.0

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 (251) hide show
  1. package/README.md +12 -15
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +22 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +26 -1
  17. package/src/app/api/external-agents/route.test.ts +165 -0
  18. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  19. package/src/app/api/gateways/[id]/route.ts +2 -0
  20. package/src/app/api/gateways/health-route.test.ts +135 -0
  21. package/src/app/api/gateways/route.ts +2 -0
  22. package/src/app/api/mcp-servers/route.test.ts +130 -0
  23. package/src/app/api/openclaw/deploy/route.ts +38 -5
  24. package/src/app/api/plugins/install/route.ts +46 -6
  25. package/src/app/api/plugins/marketplace/route.ts +48 -15
  26. package/src/app/api/preview-server/route.ts +26 -11
  27. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  28. package/src/app/api/schedules/route.test.ts +86 -0
  29. package/src/app/api/schedules/route.ts +6 -1
  30. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  31. package/src/app/api/setup/check-provider/route.ts +40 -10
  32. package/src/app/api/skills/[id]/route.ts +12 -0
  33. package/src/app/api/skills/import/route.ts +14 -12
  34. package/src/app/api/skills/route.ts +13 -1
  35. package/src/app/api/tasks/[id]/route.ts +10 -1
  36. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  37. package/src/app/api/tasks/import/github/route.ts +337 -0
  38. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  39. package/src/app/api/wallets/[id]/route.ts +79 -33
  40. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  41. package/src/app/api/wallets/route.ts +78 -61
  42. package/src/app/api/webhooks/[id]/route.ts +33 -6
  43. package/src/app/api/webhooks/route.test.ts +272 -0
  44. package/src/cli/index.js +1 -0
  45. package/src/cli/spec.js +1 -0
  46. package/src/components/agents/agent-card.tsx +9 -2
  47. package/src/components/agents/agent-chat-list.tsx +18 -2
  48. package/src/components/agents/agent-list.tsx +1 -0
  49. package/src/components/agents/agent-sheet.tsx +73 -24
  50. package/src/components/agents/inspector-panel.tsx +41 -0
  51. package/src/components/canvas/canvas-panel.tsx +236 -65
  52. package/src/components/chat/chat-card.tsx +36 -13
  53. package/src/components/chat/chat-header.tsx +44 -16
  54. package/src/components/chat/chat-list.tsx +28 -4
  55. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  56. package/src/components/chat/message-bubble.tsx +208 -145
  57. package/src/components/chat/message-list.tsx +48 -19
  58. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  59. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  60. package/src/components/connectors/connector-health.tsx +1 -1
  61. package/src/components/connectors/connector-list.tsx +7 -2
  62. package/src/components/connectors/connector-sheet.tsx +337 -148
  63. package/src/components/gateways/gateway-sheet.tsx +2 -2
  64. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  65. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  66. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  67. package/src/components/plugins/plugin-list.tsx +45 -9
  68. package/src/components/plugins/plugin-sheet.tsx +55 -7
  69. package/src/components/providers/provider-list.tsx +2 -1
  70. package/src/components/providers/provider-sheet.tsx +21 -2
  71. package/src/components/schedules/schedule-card.tsx +25 -1
  72. package/src/components/schedules/schedule-sheet.tsx +44 -2
  73. package/src/components/secrets/secret-sheet.tsx +21 -2
  74. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  75. package/src/components/shared/bottom-sheet.tsx +13 -3
  76. package/src/components/shared/command-palette.tsx +8 -1
  77. package/src/components/shared/confirm-dialog.tsx +19 -4
  78. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  79. package/src/components/shared/connector-platform-icon.tsx +39 -6
  80. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  81. package/src/components/shared/settings/section-capability-policy.tsx +7 -3
  82. package/src/components/skills/skill-list.tsx +25 -0
  83. package/src/components/skills/skill-sheet.tsx +84 -12
  84. package/src/components/tasks/approvals-panel.tsx +191 -95
  85. package/src/components/tasks/task-board.tsx +273 -2
  86. package/src/components/tasks/task-card.tsx +38 -9
  87. package/src/components/ui/dialog.tsx +2 -2
  88. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  89. package/src/components/wallets/wallet-panel.tsx +435 -90
  90. package/src/components/wallets/wallet-section.tsx +198 -48
  91. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  92. package/src/lib/approval-display.ts +20 -0
  93. package/src/lib/canvas-content.ts +198 -0
  94. package/src/lib/chat-artifact-summary.ts +165 -0
  95. package/src/lib/chat-display.test.ts +91 -0
  96. package/src/lib/chat-display.ts +58 -0
  97. package/src/lib/chat-streaming-state.test.ts +47 -1
  98. package/src/lib/chat-streaming-state.ts +42 -0
  99. package/src/lib/ollama-model.ts +10 -0
  100. package/src/lib/openclaw-endpoint.test.ts +8 -0
  101. package/src/lib/openclaw-endpoint.ts +6 -1
  102. package/src/lib/plugin-install-cors.ts +46 -0
  103. package/src/lib/plugin-sources.test.ts +43 -0
  104. package/src/lib/plugin-sources.ts +77 -0
  105. package/src/lib/providers/ollama.ts +16 -6
  106. package/src/lib/providers/openclaw.test.ts +54 -0
  107. package/src/lib/providers/openclaw.ts +127 -11
  108. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  109. package/src/lib/schedule-dedupe.test.ts +66 -1
  110. package/src/lib/schedule-dedupe.ts +169 -12
  111. package/src/lib/schedule-origin.test.ts +20 -0
  112. package/src/lib/schedule-origin.ts +15 -0
  113. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  114. package/src/lib/server/agent-availability.ts +16 -0
  115. package/src/lib/server/agent-runtime-config.ts +12 -4
  116. package/src/lib/server/agent-thread-session.test.ts +51 -0
  117. package/src/lib/server/agent-thread-session.ts +7 -0
  118. package/src/lib/server/approval-match.ts +205 -0
  119. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  120. package/src/lib/server/approvals.ts +214 -1
  121. package/src/lib/server/assistant-control.test.ts +29 -0
  122. package/src/lib/server/assistant-control.ts +23 -0
  123. package/src/lib/server/build-llm.test.ts +79 -0
  124. package/src/lib/server/build-llm.ts +14 -4
  125. package/src/lib/server/canvas-content.test.ts +32 -0
  126. package/src/lib/server/canvas-content.ts +6 -0
  127. package/src/lib/server/capability-router.test.ts +11 -0
  128. package/src/lib/server/capability-router.ts +26 -1
  129. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  130. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  131. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  132. package/src/lib/server/chat-execution.ts +353 -72
  133. package/src/lib/server/clawhub-client.test.ts +14 -8
  134. package/src/lib/server/connectors/manager.test.ts +1147 -0
  135. package/src/lib/server/connectors/manager.ts +362 -63
  136. package/src/lib/server/connectors/pairing.ts +26 -5
  137. package/src/lib/server/connectors/types.ts +2 -0
  138. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  139. package/src/lib/server/connectors/whatsapp.ts +271 -47
  140. package/src/lib/server/context-manager.ts +6 -1
  141. package/src/lib/server/daemon-state.ts +1 -1
  142. package/src/lib/server/data-dir.test.ts +37 -0
  143. package/src/lib/server/data-dir.ts +20 -1
  144. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  145. package/src/lib/server/devserver-launch.test.ts +60 -0
  146. package/src/lib/server/devserver-launch.ts +85 -0
  147. package/src/lib/server/elevenlabs.test.ts +189 -1
  148. package/src/lib/server/elevenlabs.ts +147 -43
  149. package/src/lib/server/ethereum.ts +590 -0
  150. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  151. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  152. package/src/lib/server/eval/agent-regression.ts +383 -11
  153. package/src/lib/server/evm-swap.ts +475 -0
  154. package/src/lib/server/execution-log.ts +1 -0
  155. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  156. package/src/lib/server/heartbeat-service.ts +15 -10
  157. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  158. package/src/lib/server/heartbeat-wake.ts +338 -57
  159. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  160. package/src/lib/server/mcp-client.test.ts +16 -0
  161. package/src/lib/server/mcp-client.ts +25 -0
  162. package/src/lib/server/memory-integration.test.ts +719 -0
  163. package/src/lib/server/memory-policy.test.ts +43 -0
  164. package/src/lib/server/memory-policy.ts +132 -0
  165. package/src/lib/server/memory-tiers.test.ts +60 -0
  166. package/src/lib/server/memory-tiers.ts +16 -0
  167. package/src/lib/server/ollama-runtime.ts +58 -0
  168. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  169. package/src/lib/server/openclaw-deploy.ts +557 -81
  170. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  171. package/src/lib/server/openclaw-gateway.ts +10 -4
  172. package/src/lib/server/openclaw-health.test.ts +35 -0
  173. package/src/lib/server/openclaw-health.ts +215 -47
  174. package/src/lib/server/orchestrator-lg.ts +2 -2
  175. package/src/lib/server/plugins-advanced.test.ts +351 -0
  176. package/src/lib/server/plugins.ts +205 -5
  177. package/src/lib/server/queue-advanced.test.ts +528 -0
  178. package/src/lib/server/queue-followups.test.ts +262 -0
  179. package/src/lib/server/queue-reconcile.test.ts +128 -0
  180. package/src/lib/server/queue.ts +293 -61
  181. package/src/lib/server/scheduler.ts +29 -1
  182. package/src/lib/server/session-note.test.ts +36 -0
  183. package/src/lib/server/session-note.ts +42 -0
  184. package/src/lib/server/session-run-manager.ts +52 -4
  185. package/src/lib/server/session-tools/canvas.ts +14 -12
  186. package/src/lib/server/session-tools/connector.test.ts +138 -0
  187. package/src/lib/server/session-tools/connector.ts +348 -61
  188. package/src/lib/server/session-tools/context.ts +12 -3
  189. package/src/lib/server/session-tools/crud.ts +221 -10
  190. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  191. package/src/lib/server/session-tools/delegate.ts +64 -8
  192. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  193. package/src/lib/server/session-tools/discovery.ts +80 -12
  194. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  195. package/src/lib/server/session-tools/file.ts +43 -4
  196. package/src/lib/server/session-tools/human-loop.ts +35 -5
  197. package/src/lib/server/session-tools/index.ts +44 -9
  198. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  199. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  200. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  201. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  202. package/src/lib/server/session-tools/memory.test.ts +93 -0
  203. package/src/lib/server/session-tools/memory.ts +546 -79
  204. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  205. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  206. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  207. package/src/lib/server/session-tools/schedule.ts +6 -1
  208. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  209. package/src/lib/server/session-tools/shell.ts +22 -3
  210. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  211. package/src/lib/server/session-tools/wallet.ts +1374 -139
  212. package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
  213. package/src/lib/server/session-tools/web.ts +468 -64
  214. package/src/lib/server/skill-discovery.ts +128 -0
  215. package/src/lib/server/skill-eligibility.test.ts +84 -0
  216. package/src/lib/server/skill-eligibility.ts +95 -0
  217. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  218. package/src/lib/server/skill-prompt-budget.ts +125 -0
  219. package/src/lib/server/skills-normalize.test.ts +54 -0
  220. package/src/lib/server/skills-normalize.ts +372 -26
  221. package/src/lib/server/solana.ts +214 -29
  222. package/src/lib/server/storage.ts +65 -36
  223. package/src/lib/server/stream-agent-chat.test.ts +419 -9
  224. package/src/lib/server/stream-agent-chat.ts +887 -83
  225. package/src/lib/server/system-events.ts +1 -1
  226. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  227. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  228. package/src/lib/server/tool-loop-detection.ts +260 -0
  229. package/src/lib/server/tool-planning.ts +4 -2
  230. package/src/lib/server/wallet-execution.test.ts +198 -0
  231. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  232. package/src/lib/server/wallet-portfolio.ts +724 -0
  233. package/src/lib/server/wallet-service.test.ts +57 -0
  234. package/src/lib/server/wallet-service.ts +213 -0
  235. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  236. package/src/lib/server/watch-jobs.ts +17 -2
  237. package/src/lib/server/workspace-context.ts +111 -0
  238. package/src/lib/skill-save-payload.test.ts +39 -0
  239. package/src/lib/skill-save-payload.ts +37 -0
  240. package/src/lib/tasks.ts +28 -0
  241. package/src/lib/tool-event-summary.test.ts +30 -0
  242. package/src/lib/tool-event-summary.ts +37 -0
  243. package/src/lib/validation/schemas.ts +1 -0
  244. package/src/lib/wallet-transactions.test.ts +75 -0
  245. package/src/lib/wallet-transactions.ts +43 -0
  246. package/src/lib/wallet.test.ts +17 -0
  247. package/src/lib/wallet.ts +183 -0
  248. package/src/proxy.test.ts +31 -0
  249. package/src/proxy.ts +34 -2
  250. package/src/stores/use-chat-store.ts +15 -1
  251. package/src/types/index.ts +210 -14
@@ -0,0 +1,272 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { afterEach } from 'node:test'
3
+
4
+ import { GET as getWebhookHistory } from './[id]/history/route'
5
+ import { handleWebhookPost } from './[id]/route'
6
+ import {
7
+ loadAgents,
8
+ loadSessions,
9
+ loadWebhookLogs,
10
+ loadWebhookRetryQueue,
11
+ loadWebhooks,
12
+ saveAgents,
13
+ saveSessions,
14
+ saveWebhookLogs,
15
+ saveWebhookRetryQueue,
16
+ saveWebhooks,
17
+ } from '@/lib/server/storage'
18
+
19
+ const originalAgents = loadAgents()
20
+ const originalSessions = loadSessions()
21
+ const originalWebhooks = loadWebhooks()
22
+ const originalWebhookLogs = loadWebhookLogs()
23
+ const originalWebhookRetryQueue = loadWebhookRetryQueue()
24
+
25
+ afterEach(() => {
26
+ saveAgents(originalAgents)
27
+ saveSessions(originalSessions)
28
+ saveWebhooks(originalWebhooks)
29
+ saveWebhookLogs(originalWebhookLogs)
30
+ saveWebhookRetryQueue(originalWebhookRetryQueue)
31
+ })
32
+
33
+ function seedAgent(agentId: string) {
34
+ const agents = loadAgents()
35
+ agents[agentId] = {
36
+ id: agentId,
37
+ name: 'Webhook Agent',
38
+ description: 'Test agent for webhook delivery',
39
+ systemPrompt: 'Handle inbound webhooks.',
40
+ provider: 'openai',
41
+ model: 'gpt-4o-mini',
42
+ credentialId: null,
43
+ apiEndpoint: null,
44
+ tools: ['manage_webhooks'],
45
+ createdAt: 1,
46
+ updatedAt: 1,
47
+ }
48
+ saveAgents(agents)
49
+ }
50
+
51
+ function seedWebhook(webhookId: string, overrides: Record<string, unknown> = {}) {
52
+ const webhooks = loadWebhooks()
53
+ webhooks[webhookId] = {
54
+ id: webhookId,
55
+ name: 'Webhook Smoke',
56
+ source: 'custom',
57
+ events: ['build.completed'],
58
+ agentId: 'agent-webhook-smoke',
59
+ secret: 'secret-smoke',
60
+ isEnabled: true,
61
+ createdAt: 1,
62
+ updatedAt: 1,
63
+ ...overrides,
64
+ }
65
+ saveWebhooks(webhooks)
66
+ }
67
+
68
+ test('handleWebhookPost creates a session, records success history, and triggers follow-up wiring', async () => {
69
+ const webhookId = 'wh-success-smoke'
70
+ seedAgent('agent-webhook-smoke')
71
+ seedWebhook(webhookId)
72
+
73
+ const calls = {
74
+ runs: [] as Array<Record<string, unknown>>,
75
+ events: [] as Array<[string, string]>,
76
+ heartbeats: [] as Array<Record<string, unknown>>,
77
+ }
78
+
79
+ const response = await handleWebhookPost(
80
+ new Request(`http://local/api/webhooks/${webhookId}?event=build.completed`, {
81
+ method: 'POST',
82
+ headers: {
83
+ 'content-type': 'application/json',
84
+ 'x-webhook-secret': 'secret-smoke',
85
+ },
86
+ body: JSON.stringify({ event: 'build.completed', payload: { ok: true } }),
87
+ }),
88
+ webhookId,
89
+ {
90
+ enqueueRun(input) {
91
+ calls.runs.push(input as Record<string, unknown>)
92
+ return {
93
+ runId: 'run-success-smoke',
94
+ position: 0,
95
+ promise: Promise.resolve({} as never),
96
+ abort: () => {},
97
+ }
98
+ },
99
+ enqueueEvent(sessionId, text) {
100
+ calls.events.push([sessionId, text])
101
+ },
102
+ requestHeartbeat(opts) {
103
+ calls.heartbeats.push(opts as Record<string, unknown>)
104
+ },
105
+ },
106
+ )
107
+
108
+ assert.equal(response.status, 200)
109
+ const payload = await response.json() as Record<string, unknown>
110
+ assert.equal(payload.ok, true)
111
+ assert.equal(payload.event, 'build.completed')
112
+ assert.equal(payload.runId, 'run-success-smoke')
113
+
114
+ const sessionId = String(payload.sessionId)
115
+ const session = loadSessions()[sessionId]
116
+ assert.ok(session)
117
+ assert.equal(session.name, `webhook:${webhookId}`)
118
+ assert.equal(session.agentId, 'agent-webhook-smoke')
119
+
120
+ assert.equal(calls.runs.length, 1)
121
+ assert.equal(calls.runs[0].sessionId, sessionId)
122
+ assert.equal(calls.runs[0].source, 'webhook')
123
+ assert.equal(calls.runs[0].mode, 'followup')
124
+ assert.match(String(calls.runs[0].message), /Webhook event received\./)
125
+ assert.match(String(calls.runs[0].message), /Event: build\.completed/)
126
+
127
+ assert.deepEqual(calls.events, [[sessionId, 'Webhook received: Webhook Smoke (build.completed)']])
128
+ assert.deepEqual(calls.heartbeats, [{ agentId: 'agent-webhook-smoke', reason: 'webhook' }])
129
+
130
+ const logEntries = Object.values(loadWebhookLogs()) as Array<Record<string, unknown>>
131
+ const successEntry = logEntries.find((entry) => entry.webhookId === webhookId && entry.status === 'success')
132
+ assert.ok(successEntry)
133
+ assert.equal(successEntry?.sessionId, sessionId)
134
+ assert.equal(successEntry?.runId, 'run-success-smoke')
135
+
136
+ const historyResponse = await getWebhookHistory(new Request(`http://local/api/webhooks/${webhookId}/history`), {
137
+ params: Promise.resolve({ id: webhookId }),
138
+ })
139
+ assert.equal(historyResponse.status, 200)
140
+ const history = await historyResponse.json() as Array<Record<string, unknown>>
141
+ assert.equal(history[0]?.status, 'success')
142
+ assert.equal(history[0]?.webhookId, webhookId)
143
+ })
144
+
145
+ test('handleWebhookPost ignores filtered events without dispatching or logging delivery', async () => {
146
+ const webhookId = 'wh-ignored-smoke'
147
+ seedAgent('agent-webhook-smoke')
148
+ seedWebhook(webhookId, { events: ['build.completed'] })
149
+
150
+ let runCalls = 0
151
+ const response = await handleWebhookPost(
152
+ new Request(`http://local/api/webhooks/${webhookId}`, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'content-type': 'application/json',
156
+ 'x-webhook-secret': 'secret-smoke',
157
+ },
158
+ body: JSON.stringify({ event: 'build.started' }),
159
+ }),
160
+ webhookId,
161
+ {
162
+ enqueueRun() {
163
+ runCalls += 1
164
+ return {
165
+ runId: 'should-not-run',
166
+ position: 0,
167
+ promise: Promise.resolve({} as never),
168
+ abort: () => {},
169
+ }
170
+ },
171
+ enqueueEvent() {},
172
+ requestHeartbeat() {},
173
+ },
174
+ )
175
+
176
+ assert.equal(response.status, 200)
177
+ const payload = await response.json() as Record<string, unknown>
178
+ assert.equal(payload.ignored, true)
179
+ assert.equal(payload.event, 'build.started')
180
+ assert.equal(runCalls, 0)
181
+ assert.equal(Object.values(loadSessions()).some((session: any) => session?.name === `webhook:${webhookId}`), false)
182
+ assert.equal(Object.values(loadWebhookLogs()).some((entry: any) => entry?.webhookId === webhookId), false)
183
+ })
184
+
185
+ test('handleWebhookPost rejects disabled webhooks and invalid secrets with error history', async () => {
186
+ const disabledId = 'wh-disabled-smoke'
187
+ seedWebhook(disabledId, { isEnabled: false, secret: '' })
188
+
189
+ const disabledResponse = await handleWebhookPost(
190
+ new Request(`http://local/api/webhooks/${disabledId}`, { method: 'POST' }),
191
+ disabledId,
192
+ {
193
+ enqueueRun() {
194
+ throw new Error('should not dispatch')
195
+ },
196
+ enqueueEvent() {},
197
+ requestHeartbeat() {},
198
+ },
199
+ )
200
+ assert.equal(disabledResponse.status, 409)
201
+
202
+ const invalidSecretId = 'wh-secret-smoke'
203
+ seedAgent('agent-webhook-smoke')
204
+ seedWebhook(invalidSecretId, { secret: 'top-secret' })
205
+
206
+ const invalidSecretResponse = await handleWebhookPost(
207
+ new Request(`http://local/api/webhooks/${invalidSecretId}`, {
208
+ method: 'POST',
209
+ headers: { 'x-webhook-secret': 'wrong-secret' },
210
+ }),
211
+ invalidSecretId,
212
+ {
213
+ enqueueRun() {
214
+ throw new Error('should not dispatch')
215
+ },
216
+ enqueueEvent() {},
217
+ requestHeartbeat() {},
218
+ },
219
+ )
220
+ assert.equal(invalidSecretResponse.status, 401)
221
+
222
+ const errors = Object.values(loadWebhookLogs()) as Array<Record<string, unknown>>
223
+ const disabledEntry = errors.find((entry) => entry.webhookId === disabledId)
224
+ const invalidSecretEntry = errors.find((entry) => entry.webhookId === invalidSecretId)
225
+ assert.equal(disabledEntry?.error, 'Webhook is disabled')
226
+ assert.equal(invalidSecretEntry?.error, 'Invalid webhook secret')
227
+ })
228
+
229
+ test('handleWebhookPost queues retries when run dispatch throws', async () => {
230
+ const webhookId = 'wh-retry-smoke'
231
+ seedAgent('agent-webhook-smoke')
232
+ seedWebhook(webhookId)
233
+
234
+ const heartbeats: Array<Record<string, unknown>> = []
235
+ const response = await handleWebhookPost(
236
+ new Request(`http://local/api/webhooks/${webhookId}`, {
237
+ method: 'POST',
238
+ headers: {
239
+ 'content-type': 'application/json',
240
+ 'x-webhook-secret': 'secret-smoke',
241
+ },
242
+ body: JSON.stringify({ event: 'build.completed', payload: { ok: false } }),
243
+ }),
244
+ webhookId,
245
+ {
246
+ enqueueRun() {
247
+ throw new Error('dispatch exploded')
248
+ },
249
+ enqueueEvent() {},
250
+ requestHeartbeat(opts) {
251
+ heartbeats.push(opts as Record<string, unknown>)
252
+ },
253
+ },
254
+ )
255
+
256
+ assert.equal(response.status, 200)
257
+ const payload = await response.json() as Record<string, unknown>
258
+ assert.equal(payload.retryQueued, true)
259
+ assert.equal(payload.error, 'dispatch exploded')
260
+ assert.equal(heartbeats.length, 0)
261
+
262
+ const retries = Object.values(loadWebhookRetryQueue()) as Array<Record<string, unknown>>
263
+ const retryEntry = retries.find((entry) => entry.webhookId === webhookId)
264
+ assert.ok(retryEntry)
265
+ assert.equal(retryEntry?.attempts, 1)
266
+ assert.equal(retryEntry?.deadLettered, false)
267
+
268
+ const errorLogs = Object.values(loadWebhookLogs()) as Array<Record<string, unknown>>
269
+ const retryLog = errorLogs.find((entry) => entry.webhookId === webhookId)
270
+ assert.ok(retryLog)
271
+ assert.match(String(retryLog?.error), /Dispatch failed, queued for retry: dispatch exploded/)
272
+ })
package/src/cli/index.js CHANGED
@@ -586,6 +586,7 @@ const COMMAND_GROUPS = [
586
586
  cmd('delete', 'DELETE', '/tasks/:id', 'Delete task'),
587
587
  cmd('purge', 'DELETE', '/tasks', 'Bulk delete tasks', { expectsJsonBody: true }),
588
588
  cmd('approve', 'POST', '/tasks/:id/approve', 'Approve or reject a pending tool execution', { expectsJsonBody: true }),
589
+ cmd('import-github', 'POST', '/tasks/import/github', 'Import GitHub issues into tasks', { expectsJsonBody: true }),
589
590
  cmd('metrics', 'GET', '/tasks/metrics', 'Get task board metrics (supports --query range=24h|7d|30d)'),
590
591
  ],
591
592
  },
package/src/cli/spec.js CHANGED
@@ -447,6 +447,7 @@ const COMMAND_GROUPS = {
447
447
  delete: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
448
448
  archive: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
449
449
  approve: { description: 'Approve or reject a pending tool execution', method: 'POST', path: '/tasks/:id/approve', params: ['id'] },
450
+ 'import-github': { description: 'Import GitHub issues into tasks', method: 'POST', path: '/tasks/import/github' },
450
451
  metrics: { description: 'Get task board metrics (supports --query range=24h|7d|30d)', method: 'GET', path: '/tasks/metrics' },
451
452
  },
452
453
  },
@@ -63,6 +63,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
63
63
  },
64
64
  ].filter((entry) => entry.budget !== null)
65
65
  const canDelegateToAgents = agent.platformAssignScope === 'all'
66
+ const agentDisabled = agent.disabled === true
66
67
  useWs(`heartbeat:agent:${agent.id}`, () => {
67
68
  setHeartbeatPulse(true)
68
69
  setTimeout(() => setHeartbeatPulse(false), 1500)
@@ -125,6 +126,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
125
126
  onClick={handleClick}
126
127
  className={`group relative py-3.5 px-4 cursor-pointer rounded-[14px]
127
128
  transition-all duration-200 active:scale-[0.98]
129
+ ${agentDisabled ? 'opacity-70' : ''}
128
130
  ${isSelected
129
131
  ? 'bg-white/[0.04] border border-white/[0.08]'
130
132
  : 'bg-transparent border border-transparent hover:bg-white/[0.05] hover:border-white/[0.08]'}`}
@@ -197,6 +199,11 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
197
199
  {pendingApprovalCount} {pendingApprovalCount === 1 ? 'approval' : 'approvals'}
198
200
  </span>
199
201
  )}
202
+ {agentDisabled && (
203
+ <span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-amber-300 bg-amber-400/[0.08] border border-amber-400/15 px-2 py-0.5 rounded-[6px]">
204
+ disabled
205
+ </span>
206
+ )}
200
207
  {isDefault && (
201
208
  <span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-accent-bright bg-accent-soft px-2 py-0.5 rounded-[6px]">
202
209
  default
@@ -205,12 +212,12 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
205
212
  {canDelegateToAgents && (
206
213
  <button
207
214
  onClick={handleRunClick}
208
- disabled={running}
215
+ disabled={running || agentDisabled}
209
216
  className="shrink-0 text-[10px] font-600 uppercase tracking-wider px-2.5 py-1 rounded-[6px] cursor-pointer
210
217
  transition-all border-none bg-accent-bright/20 text-accent-bright hover:bg-accent-bright/30 disabled:opacity-40"
211
218
  style={{ fontFamily: 'inherit' }}
212
219
  >
213
- {running ? '...' : 'Run'}
220
+ {agentDisabled ? 'Off' : running ? '...' : 'Run'}
214
221
  </button>
215
222
  )}
216
223
  {canDelegateToAgents && (
@@ -167,6 +167,10 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
167
167
  }, [filteredAgents.map((a) => a.id).join(',')])
168
168
 
169
169
  const handleSelect = async (agent: Agent) => {
170
+ if (agent.disabled === true && !agent.threadSessionId) {
171
+ toast.error(`${agent.name} is disabled. Re-enable it to start a new chat.`)
172
+ return
173
+ }
170
174
  await setCurrentAgent(agent.id)
171
175
  // Load messages for the thread
172
176
  const state = useAppStore.getState()
@@ -274,7 +278,8 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
274
278
  const lastMsg = threadSession?.messages?.at(-1)
275
279
  const heartbeatOn = defaultAgent.heartbeatEnabled === true && (defaultAgent.plugins?.length ?? 0) > 0
276
280
  const recentlyActive = (threadSession?.lastActiveAt ?? 0) > Date.now() - 30 * 60 * 1000
277
- const isWorking = runningAgentIds.has(defaultAgent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive || chatroomActiveAgentIds.has(defaultAgent.id)
281
+ const isDisabled = defaultAgent.disabled === true
282
+ const isWorking = !isDisabled && (runningAgentIds.has(defaultAgent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive || chatroomActiveAgentIds.has(defaultAgent.id))
278
283
  const isTyping = streamingSessionId === defaultAgent.threadSessionId
279
284
  const preview = lastMsg?.text?.slice(0, 100)?.replace(/\n/g, ' ') || 'Your primary shortcut chat.'
280
285
  const isActive = currentAgentId === defaultAgent.id
@@ -313,6 +318,11 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
313
318
  <span className="font-display text-[14px] font-700 truncate text-text tracking-[-0.01em]">
314
319
  {defaultAgent.name}
315
320
  </span>
321
+ {isDisabled && (
322
+ <span className="px-1.5 py-0.5 rounded-[6px] bg-amber-400/[0.08] text-amber-300 text-[9px] font-700 uppercase tracking-[0.08em]">
323
+ Disabled
324
+ </span>
325
+ )}
316
326
  <span className="px-1.5 py-0.5 rounded-[6px] bg-accent-bright/12 text-accent-bright text-[9px] font-700 uppercase tracking-[0.08em]">
317
327
  Shortcut
318
328
  </span>
@@ -359,7 +369,8 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
359
369
  const isActive = currentAgentId === agent.id
360
370
  const heartbeatOn = agent.heartbeatEnabled === true && (agent.plugins?.length ?? 0) > 0
361
371
  const recentlyActive = (threadSession?.lastActiveAt ?? 0) > Date.now() - 30 * 60 * 1000
362
- const isWorking = runningAgentIds.has(agent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive || chatroomActiveAgentIds.has(agent.id)
372
+ const isDisabled = agent.disabled === true
373
+ const isWorking = !isDisabled && (runningAgentIds.has(agent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive || chatroomActiveAgentIds.has(agent.id))
363
374
  const isTyping = streamingSessionId === agent.threadSessionId
364
375
  const preview = lastMsg?.text?.slice(0, 80)?.replace(/\n/g, ' ') || ''
365
376
 
@@ -395,6 +406,11 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
395
406
  <span className="font-display text-[13.5px] font-600 truncate flex-1 tracking-[-0.01em]">
396
407
  {agent.name}
397
408
  </span>
409
+ {isDisabled && (
410
+ <span className="px-1.5 py-0.5 rounded-[6px] bg-amber-400/[0.08] text-amber-300 text-[9px] font-700 uppercase tracking-[0.08em] shrink-0">
411
+ Disabled
412
+ </span>
413
+ )}
398
414
  {appSettings.defaultAgentId === agent.id && (
399
415
  <span className="px-1.5 py-0.5 rounded-[6px] bg-accent-bright/10 text-accent-bright text-[9px] font-700 uppercase tracking-[0.08em] shrink-0">
400
416
  Default
@@ -70,6 +70,7 @@ export function AgentList({ inSidebar }: Props) {
70
70
  const ids = new Set<string>()
71
71
  const recentThreshold = now - 30 * 60 * 1000
72
72
  for (const a of Object.values(agents)) {
73
+ if (a.disabled === true) continue
73
74
  if (a.heartbeatEnabled === true && (a.plugins?.length ?? 0) > 0) { ids.add(a.id); continue }
74
75
  // Check if any session for this agent was active in the last 30 minutes
75
76
  for (const s of Object.values(sessions)) {
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useRef, useState } from 'react'
3
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { createAgent, updateAgent, deleteAgent } from '@/lib/agents'
6
6
  import { api } from '@/lib/api-client'
@@ -18,11 +18,19 @@ import { copyTextToClipboard } from '@/lib/clipboard'
18
18
  import { SectionLabel } from '@/components/shared/section-label'
19
19
  import { SoulLibraryPicker } from './soul-library-picker'
20
20
  import { HintTip } from '@/components/shared/hint-tip'
21
+ import { isOllamaCloudModel } from '@/lib/ollama-model'
21
22
 
22
23
  const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
23
24
  const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
24
25
 
25
26
  type AgentSheetSectionId = 'overview' | 'instructions' | 'model' | 'tools'
27
+ type SafeAgentWallet = Omit<AgentWallet, 'encryptedPrivateKey'> & {
28
+ balanceAtomic?: string
29
+ balanceLamports?: number
30
+ balanceFormatted?: string
31
+ balanceSymbol?: string
32
+ isActive?: boolean
33
+ }
26
34
 
27
35
  function SectionCard({
28
36
  title,
@@ -190,6 +198,7 @@ export function AgentSheet() {
190
198
  const [memoryScopeMode, setMemoryScopeMode] = useState<'auto' | 'all' | 'global' | 'agent' | 'session' | 'project'>('auto')
191
199
  const [memoryTierPreference, setMemoryTierPreference] = useState<'working' | 'durable' | 'archive' | 'blended'>('blended')
192
200
  const [autoRecovery, setAutoRecovery] = useState(false)
201
+ const [disabled, setDisabled] = useState(false)
193
202
  const [voiceId, setVoiceId] = useState('')
194
203
  const [heartbeatEnabled, setHeartbeatEnabled] = useState(false)
195
204
  const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('') // '' = default (30m)
@@ -211,7 +220,7 @@ export function AgentSheet() {
211
220
  const [dailyBudget, setDailyBudget] = useState('')
212
221
  const [monthlyBudget, setMonthlyBudget] = useState('')
213
222
  const [budgetAction, setBudgetAction] = useState<'warn' | 'block'>('warn')
214
- const [agentWallet, setAgentWallet] = useState<(Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }) | null>(null)
223
+ const [agentWallets, setAgentWallets] = useState<SafeAgentWallet[]>([])
215
224
  const [addingKey, setAddingKey] = useState(false)
216
225
  const [newKeyName, setNewKeyName] = useState('')
217
226
  const [newKeyValue, setNewKeyValue] = useState('')
@@ -245,6 +254,21 @@ export function AgentSheet() {
245
254
  e.target.value = ''
246
255
  }
247
256
 
257
+ const loadAgentWallets = useCallback(async (agentId: string) => {
258
+ try {
259
+ const wallets = await api<Record<string, SafeAgentWallet>>('GET', `/wallets?agentId=${encodeURIComponent(agentId)}`)
260
+ const matches = Object.values(wallets)
261
+ .filter((wallet) => wallet.agentId === agentId)
262
+ .sort((a, b) => {
263
+ if ((a.isActive ? 1 : 0) !== (b.isActive ? 1 : 0)) return a.isActive ? -1 : 1
264
+ return a.chain.localeCompare(b.chain)
265
+ })
266
+ setAgentWallets(matches)
267
+ } catch {
268
+ setAgentWallets([])
269
+ }
270
+ }, [])
271
+
248
272
  const currentProvider = providers.find((p) => p.id === provider)
249
273
  const providerCredentials = Object.values(credentials).filter((c) => c.provider === provider)
250
274
  const openclawCredentials = Object.values(credentials).filter((c) => c.provider === 'openclaw')
@@ -307,7 +331,11 @@ export function AgentSheet() {
307
331
  setFallbackCredentialIds(editing.fallbackCredentialIds || [])
308
332
  setCapabilities(Array.isArray(editing.capabilities) ? editing.capabilities : [])
309
333
  setCapInput('')
310
- setOllamaMode(editing.credentialId && editing.provider === 'ollama' ? 'cloud' : 'local')
334
+ setOllamaMode(
335
+ editing.provider === 'ollama' && (Boolean(editing.credentialId) || isOllamaCloudModel(editing.model))
336
+ ? 'cloud'
337
+ : 'local'
338
+ )
311
339
  setOpenclawEnabled(editing.provider === 'openclaw')
312
340
  setProjectId(editing.projectId)
313
341
  setAvatarSeed(editing.avatarSeed || crypto.randomUUID().slice(0, 8))
@@ -316,6 +344,7 @@ export function AgentSheet() {
316
344
  setMemoryScopeMode(editing.memoryScopeMode || 'auto')
317
345
  setMemoryTierPreference(editing.memoryTierPreference || 'blended')
318
346
  setAutoRecovery(editing.autoRecovery || false)
347
+ setDisabled(editing.disabled === true)
319
348
  setVoiceId(editing.elevenLabsVoiceId || '')
320
349
  setHeartbeatEnabled(editing.heartbeatEnabled || false)
321
350
  setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
@@ -341,14 +370,7 @@ export function AgentSheet() {
341
370
  setDailyBudget(typeof editing.dailyBudget === 'number' && editing.dailyBudget > 0 ? String(editing.dailyBudget) : '')
342
371
  setMonthlyBudget(typeof editing.monthlyBudget === 'number' && editing.monthlyBudget > 0 ? String(editing.monthlyBudget) : '')
343
372
  setBudgetAction(editing.budgetAction || 'warn')
344
- // Load wallet if agent has one
345
- if (editing.walletId) {
346
- api<Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }>('GET', `/wallets/${editing.walletId}`)
347
- .then(setAgentWallet)
348
- .catch(() => setAgentWallet(null))
349
- } else {
350
- setAgentWallet(null)
351
- }
373
+ void loadAgentWallets(editing.id)
352
374
  } else {
353
375
  setName('')
354
376
  setDescription('')
@@ -383,6 +405,7 @@ export function AgentSheet() {
383
405
  setMemoryScopeMode('auto')
384
406
  setMemoryTierPreference('blended')
385
407
  setAutoRecovery(false)
408
+ setDisabled(false)
386
409
  setVoiceId('')
387
410
  setHeartbeatEnabled(false)
388
411
  setHeartbeatIntervalSec('')
@@ -404,6 +427,7 @@ export function AgentSheet() {
404
427
  setDailyBudget('')
405
428
  setMonthlyBudget('')
406
429
  setBudgetAction('warn')
430
+ setAgentWallets([])
407
431
  }
408
432
  }
409
433
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -571,6 +595,7 @@ export function AgentSheet() {
571
595
  memoryScopeMode,
572
596
  memoryTierPreference,
573
597
  autoRecovery,
598
+ disabled,
574
599
  elevenLabsVoiceId: voiceId.trim() || null,
575
600
  heartbeatEnabled,
576
601
  heartbeatInterval: heartbeatIntervalSec ? formatHbDuration(Number(heartbeatIntervalSec)) : null,
@@ -744,9 +769,18 @@ export function AgentSheet() {
744
769
  <BottomSheet open={open} onClose={onClose} wide>
745
770
  <div className="mb-10 flex items-start justify-between">
746
771
  <div>
747
- <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
748
- {editing ? 'Edit Agent' : 'New Agent'}
749
- </h2>
772
+ <div className="mb-2 flex flex-wrap items-center gap-2">
773
+ <h2 className="font-display text-[28px] font-700 tracking-[-0.03em]">
774
+ {editing ? 'Edit Agent' : 'New Agent'}
775
+ </h2>
776
+ <span className={`rounded-[999px] px-2.5 py-1 text-[10px] font-700 uppercase tracking-[0.1em] ${
777
+ disabled
778
+ ? 'border border-amber-400/20 bg-amber-400/[0.08] text-amber-300'
779
+ : 'border border-emerald-400/20 bg-emerald-400/[0.08] text-emerald-300'
780
+ }`}>
781
+ {disabled ? 'Disabled' : 'Enabled'}
782
+ </span>
783
+ </div>
750
784
  <p className="text-[14px] text-text-3">Define an AI agent and optional multi-agent delegation behavior</p>
751
785
  </div>
752
786
  <div className="flex items-center gap-3 mt-1.5">
@@ -1029,6 +1063,28 @@ export function AgentSheet() {
1029
1063
  <p className="text-[11px] text-text-3/70 mt-1.5">Use working for fast recent context, durable for facts/preferences, and archive for long-lived history.</p>
1030
1064
  </div>
1031
1065
 
1066
+ {/* Auto-Recovery */}
1067
+ <div className="mb-8">
1068
+ <div className="flex items-center justify-between mb-1.5">
1069
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">
1070
+ Agent Availability
1071
+ <HintTip text="Disabled agents stay visible, but cannot take chats, heartbeats, schedules, or queued work until re-enabled." />
1072
+ </label>
1073
+ <div
1074
+ onClick={() => setDisabled(!disabled)}
1075
+ className={`w-9 h-5 rounded-full transition-all relative cursor-pointer ${disabled ? 'bg-amber-400' : 'bg-accent-bright'}`}
1076
+ >
1077
+ <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all ${disabled ? 'left-0.5' : 'left-[18px]'}`} />
1078
+ </div>
1079
+ </div>
1080
+ <p className="text-[11px] text-text-3/70">
1081
+ {disabled
1082
+ ? 'This agent is paused. Existing chats remain viewable, but new work is blocked until you switch it back on.'
1083
+ : 'This agent is active and can accept chats, heartbeats, schedules, and queued work.'}
1084
+ {' '}Gateway and remote runtime shutdown stay managed separately in Providers and OpenClaw Deploy.
1085
+ </p>
1086
+ </div>
1087
+
1032
1088
  {/* Auto-Recovery */}
1033
1089
  <div className="mb-8">
1034
1090
  <div className="flex items-center justify-between mb-1.5">
@@ -1261,18 +1317,11 @@ export function AgentSheet() {
1261
1317
  {editingId && (
1262
1318
  <WalletSection
1263
1319
  agentId={editingId}
1264
- wallet={agentWallet}
1320
+ wallets={agentWallets}
1321
+ activeWalletId={editing?.activeWalletId || editing?.walletId || agentWallets.find((wallet) => wallet.isActive)?.id || null}
1265
1322
  onWalletCreated={async () => {
1266
1323
  await loadAgents()
1267
- // Fetch the wallet for this agent
1268
- try {
1269
- const wallets = await api<Record<string, Omit<AgentWallet, 'encryptedPrivateKey'>>>('GET', '/wallets')
1270
- const match = Object.values(wallets).find((w) => w.agentId === editingId)
1271
- if (match) {
1272
- const detail = await api<Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }>('GET', `/wallets/${match.id}`)
1273
- setAgentWallet(detail)
1274
- }
1275
- } catch { /* ignore */ }
1324
+ await loadAgentWallets(editingId)
1276
1325
  }}
1277
1326
  />
1278
1327
  )}