@swarmclawai/swarmclaw 0.7.7 → 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 (281) hide show
  1. package/README.md +12 -14
  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 +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -1,29 +1,71 @@
1
1
  'use client'
2
2
 
3
- import { useState, useCallback } from 'react'
3
+ import { useCallback, useEffect, useMemo, useState } from 'react'
4
4
  import { api } from '@/lib/api-client'
5
5
  import { copyTextToClipboard } from '@/lib/clipboard'
6
- import type { AgentWallet, WalletChain } from '@/types'
6
+ import type { AgentWallet, WalletAssetBalance, WalletPortfolioSummary, WalletChain } from '@/types'
7
7
  import { toast } from 'sonner'
8
+ import {
9
+ SUPPORTED_WALLET_CHAINS,
10
+ formatWalletAmount,
11
+ getWalletBalanceAtomic,
12
+ getWalletChainMeta,
13
+ getWalletLimitAtomic,
14
+ } from '@/lib/wallet'
15
+
16
+ type SafeWallet = Omit<AgentWallet, 'encryptedPrivateKey'> & {
17
+ balanceAtomic?: string
18
+ balanceLamports?: number
19
+ balanceFormatted?: string
20
+ balanceSymbol?: string
21
+ assets?: WalletAssetBalance[]
22
+ portfolioSummary?: WalletPortfolioSummary
23
+ isActive?: boolean
24
+ }
8
25
 
9
26
  interface WalletSectionProps {
10
27
  agentId: string
11
- wallet: (Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }) | null
28
+ wallets: SafeWallet[]
29
+ activeWalletId: string | null
12
30
  onWalletCreated: () => void
13
31
  }
14
32
 
15
- export function WalletSection({ agentId, wallet, onWalletCreated }: WalletSectionProps) {
33
+ export function WalletSection({ agentId, wallets, activeWalletId, onWalletCreated }: WalletSectionProps) {
16
34
  const [creating, setCreating] = useState(false)
35
+ const [activatingWalletId, setActivatingWalletId] = useState<string | null>(null)
17
36
  const [error, setError] = useState<string | null>(null)
18
- const [copied, setCopied] = useState(false)
37
+ const [copiedWalletId, setCopiedWalletId] = useState<string | null>(null)
38
+
39
+ const connectedChains = useMemo(() => new Set(wallets.map((wallet) => wallet.chain)), [wallets])
40
+ const availableChains = useMemo(
41
+ () => SUPPORTED_WALLET_CHAINS.filter((chain) => !connectedChains.has(chain)),
42
+ [connectedChains],
43
+ )
44
+ const sortedWallets = useMemo(
45
+ () => [...wallets].sort((a, b) => {
46
+ const aActive = a.id === activeWalletId || a.isActive === true
47
+ const bActive = b.id === activeWalletId || b.isActive === true
48
+ if (aActive !== bActive) return aActive ? -1 : 1
49
+ return a.chain.localeCompare(b.chain)
50
+ }),
51
+ [activeWalletId, wallets],
52
+ )
53
+
54
+ const [chain, setChain] = useState<WalletChain>(availableChains[0] || 'solana')
55
+
56
+ useEffect(() => {
57
+ if (availableChains.length === 0) return
58
+ if (!availableChains.includes(chain)) setChain(availableChains[0])
59
+ }, [availableChains, chain])
19
60
 
20
61
  const createWallet = useCallback(async () => {
62
+ if (!availableChains.includes(chain)) return
21
63
  setCreating(true)
22
64
  setError(null)
23
65
  try {
24
- await api('POST', '/wallets', { agentId, chain: 'solana' as WalletChain })
66
+ await api('POST', '/wallets', { agentId, chain })
25
67
  toast.success('Agent wallet created successfully')
26
- onWalletCreated()
68
+ await onWalletCreated()
27
69
  } catch (err: unknown) {
28
70
  const msg = err instanceof Error ? err.message : String(err)
29
71
  setError(msg)
@@ -31,32 +73,165 @@ export function WalletSection({ agentId, wallet, onWalletCreated }: WalletSectio
31
73
  } finally {
32
74
  setCreating(false)
33
75
  }
34
- }, [agentId, onWalletCreated])
76
+ }, [agentId, availableChains, chain, onWalletCreated])
35
77
 
36
- const copyAddress = useCallback(async () => {
37
- if (!wallet) return
78
+ const copyAddress = useCallback(async (wallet: SafeWallet) => {
38
79
  const copiedValue = await copyTextToClipboard(wallet.publicKey)
39
80
  if (!copiedValue) return
40
- setCopied(true)
41
- setTimeout(() => setCopied(false), 2000)
42
- }, [wallet])
81
+ setCopiedWalletId(wallet.id)
82
+ setTimeout(() => {
83
+ setCopiedWalletId((current) => current === wallet.id ? null : current)
84
+ }, 2000)
85
+ }, [])
86
+
87
+ const setActiveWallet = useCallback(async (walletId: string) => {
88
+ setActivatingWalletId(walletId)
89
+ setError(null)
90
+ try {
91
+ await api('PATCH', `/wallets/${walletId}`, { makeActive: true })
92
+ toast.success('Default wallet updated')
93
+ await onWalletCreated()
94
+ } catch (err: unknown) {
95
+ const msg = err instanceof Error ? err.message : String(err)
96
+ setError(msg)
97
+ toast.error(msg)
98
+ } finally {
99
+ setActivatingWalletId(null)
100
+ }
101
+ }, [onWalletCreated])
43
102
 
44
103
  return (
45
104
  <div className="mb-8">
46
105
  <div className="flex items-center gap-2 mb-3">
47
106
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">
48
- Wallet
107
+ Wallets
49
108
  </label>
50
109
  <span className="px-1.5 py-0.5 rounded-[4px] bg-amber-500/15 text-amber-400 text-[9px] font-600 uppercase tracking-wide">
51
110
  Experimental
52
111
  </span>
53
112
  </div>
54
113
 
55
- {!wallet ? (
56
- <div className="p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50">
114
+ {sortedWallets.length > 0 ? (
115
+ <div className="space-y-3">
116
+ <div className="p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50">
117
+ <div className="flex items-center justify-between gap-3 mb-2">
118
+ <div>
119
+ <div className="text-[12px] font-600 text-text-1">Combined Wallet Summary</div>
120
+ <p className="text-[11px] text-text-3/70 mt-1">
121
+ {sortedWallets.length} wallet{sortedWallets.length === 1 ? '' : 's'} connected. The active wallet is used by default when the agent does not specify a chain explicitly.
122
+ </p>
123
+ </div>
124
+ <div className="text-right">
125
+ <div className="text-[18px] font-600 text-text-1">{sortedWallets.length}</div>
126
+ <div className="text-[10px] uppercase tracking-wide text-text-3/50">Connected</div>
127
+ </div>
128
+ </div>
129
+ <div className="grid gap-2 md:grid-cols-2">
130
+ {sortedWallets.map((wallet) => {
131
+ const walletMeta = getWalletChainMeta(wallet.chain)
132
+ const balanceFormatted = wallet.balanceFormatted || formatWalletAmount(wallet.chain, getWalletBalanceAtomic(wallet), { minFractionDigits: 4, maxFractionDigits: 6 })
133
+ const isActive = wallet.id === activeWalletId || wallet.isActive === true
134
+ return (
135
+ <div key={wallet.id} className="rounded-[10px] border border-white/[0.06] bg-black/10 px-3 py-2">
136
+ <div className="flex items-center gap-2">
137
+ <span className="text-[10px] text-text-3/60 uppercase tracking-wide font-600">{walletMeta.label}</span>
138
+ {isActive && (
139
+ <span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft text-accent-bright text-[9px] font-600 uppercase tracking-wide">
140
+ Active
141
+ </span>
142
+ )}
143
+ </div>
144
+ <div className="mt-1 text-[14px] font-600 text-text-1">{balanceFormatted} {walletMeta.symbol}</div>
145
+ {wallet.portfolioSummary?.nonZeroAssets ? (
146
+ <div className="mt-1 text-[10px] text-text-3/55">
147
+ {wallet.portfolioSummary.nonZeroAssets} asset{wallet.portfolioSummary.nonZeroAssets === 1 ? '' : 's'} tracked
148
+ </div>
149
+ ) : null}
150
+ <div className="mt-1 text-[10px] text-text-3/55 font-mono truncate">{wallet.publicKey}</div>
151
+ </div>
152
+ )
153
+ })}
154
+ </div>
155
+ </div>
156
+
157
+ {sortedWallets.map((wallet) => {
158
+ const walletMeta = getWalletChainMeta(wallet.chain)
159
+ const balanceFormatted = wallet.balanceFormatted || formatWalletAmount(wallet.chain, getWalletBalanceAtomic(wallet), { minFractionDigits: 4, maxFractionDigits: 6 })
160
+ const perTxLimit = formatWalletAmount(wallet.chain, getWalletLimitAtomic(wallet, 'perTx'), { maxFractionDigits: 6 })
161
+ const dailyLimit = formatWalletAmount(wallet.chain, getWalletLimitAtomic(wallet, 'daily'), { maxFractionDigits: 6 })
162
+ const isActive = wallet.id === activeWalletId || wallet.isActive === true
163
+ return (
164
+ <div key={wallet.id} className="p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50 space-y-3">
165
+ <div className="flex items-center gap-2">
166
+ <span className="text-[10px] text-text-3/60 uppercase tracking-wide font-600">
167
+ {walletMeta.label}
168
+ </span>
169
+ {isActive && (
170
+ <span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft text-accent-bright text-[9px] font-600 uppercase tracking-wide">
171
+ Default
172
+ </span>
173
+ )}
174
+ <span className="flex-1" />
175
+ <span className="text-[13px] font-600 text-text-1">
176
+ {balanceFormatted} {walletMeta.symbol}
177
+ </span>
178
+ </div>
179
+ <div className="flex items-center gap-2">
180
+ <code className="text-[11px] text-text-3 bg-black/20 px-2 py-1 rounded-[6px] font-mono truncate flex-1">
181
+ {wallet.publicKey}
182
+ </code>
183
+ <button
184
+ type="button"
185
+ onClick={() => copyAddress(wallet)}
186
+ className="shrink-0 px-2 py-1 rounded-[6px] text-[10px] text-text-3 hover:text-text-2 border border-white/[0.08] bg-surface transition-colors cursor-pointer"
187
+ style={{ fontFamily: 'inherit' }}
188
+ >
189
+ {copiedWalletId === wallet.id ? 'Copied!' : 'Copy'}
190
+ </button>
191
+ {!isActive && (
192
+ <button
193
+ type="button"
194
+ onClick={() => setActiveWallet(wallet.id)}
195
+ disabled={activatingWalletId === wallet.id}
196
+ className="shrink-0 px-2 py-1 rounded-[6px] text-[10px] font-600 text-accent-bright border border-accent-bright/20 bg-accent-soft/10 hover:bg-accent-soft/20 transition-colors cursor-pointer disabled:opacity-50"
197
+ style={{ fontFamily: 'inherit' }}
198
+ >
199
+ {activatingWalletId === wallet.id ? 'Setting...' : 'Set Default'}
200
+ </button>
201
+ )}
202
+ </div>
203
+ <div className="flex flex-wrap items-center gap-3 text-[10px] text-text-3/60">
204
+ <span>Limit: {perTxLimit} {walletMeta.symbol}/tx</span>
205
+ <span>Daily: {dailyLimit} {walletMeta.symbol}</span>
206
+ <span>{wallet.requireApproval ? 'Approval required' : 'Auto-send'}</span>
207
+ {wallet.portfolioSummary?.nonZeroAssets ? (
208
+ <span>{wallet.portfolioSummary.nonZeroAssets} asset{wallet.portfolioSummary.nonZeroAssets === 1 ? '' : 's'} detected</span>
209
+ ) : null}
210
+ </div>
211
+ </div>
212
+ )
213
+ })}
214
+ </div>
215
+ ) : null}
216
+
217
+ {availableChains.length > 0 ? (
218
+ <div className="mt-3 p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50">
57
219
  <p className="text-[12px] text-text-3/70 mb-3">
58
- Create a Solana wallet for this agent to hold funds, pay for services, and trade autonomously.
220
+ {getWalletChainMeta(chain).createDescription}
59
221
  </p>
222
+ <label className="block text-[11px] text-text-3/70 mb-1">Wallet Type</label>
223
+ <select
224
+ value={chain}
225
+ onChange={(event) => setChain(event.target.value as WalletChain)}
226
+ className="w-full mb-3 px-3 py-2 rounded-[8px] border border-white/[0.08] bg-surface text-[12px] text-text-1 outline-none focus:border-accent/40"
227
+ style={{ fontFamily: 'inherit' }}
228
+ >
229
+ {availableChains.map((availableChain) => (
230
+ <option key={availableChain} value={availableChain}>
231
+ {getWalletChainMeta(availableChain).label}
232
+ </option>
233
+ ))}
234
+ </select>
60
235
  <button
61
236
  type="button"
62
237
  onClick={createWallet}
@@ -64,41 +239,16 @@ export function WalletSection({ agentId, wallet, onWalletCreated }: WalletSectio
64
239
  className="px-3 py-1.5 rounded-[8px] bg-accent-soft text-accent-bright text-[11px] font-600 hover:bg-accent-bright/15 transition-all cursor-pointer disabled:opacity-50 border border-accent-bright/20"
65
240
  style={{ fontFamily: 'inherit' }}
66
241
  >
67
- {creating ? 'Creating...' : 'Create Wallet'}
242
+ {creating ? 'Creating...' : `Create ${getWalletChainMeta(chain).label} Wallet`}
68
243
  </button>
69
244
  {error && <p className="text-[11px] text-red-400 mt-2">{error}</p>}
70
245
  </div>
71
246
  ) : (
72
- <div className="p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50 space-y-3">
73
- <div className="flex items-center gap-2">
74
- <span className="text-[10px] text-text-3/60 uppercase tracking-wide font-600">
75
- {wallet.chain}
76
- </span>
77
- <span className="flex-1" />
78
- {typeof wallet.balanceSol === 'number' && (
79
- <span className="text-[13px] font-600 text-text-1">
80
- {wallet.balanceSol.toFixed(4)} SOL
81
- </span>
82
- )}
83
- </div>
84
- <div className="flex items-center gap-2">
85
- <code className="text-[11px] text-text-3 bg-black/20 px-2 py-1 rounded-[6px] font-mono truncate flex-1">
86
- {wallet.publicKey}
87
- </code>
88
- <button
89
- type="button"
90
- onClick={copyAddress}
91
- className="shrink-0 px-2 py-1 rounded-[6px] text-[10px] text-text-3 hover:text-text-2 border border-white/[0.08] bg-surface transition-colors cursor-pointer"
92
- style={{ fontFamily: 'inherit' }}
93
- >
94
- {copied ? 'Copied!' : 'Copy'}
95
- </button>
96
- </div>
97
- <div className="flex items-center gap-3 text-[10px] text-text-3/60">
98
- <span>Limit: {((wallet.spendingLimitLamports ?? 100_000_000) / 1e9).toFixed(2)} SOL/tx</span>
99
- <span>Daily: {((wallet.dailyLimitLamports ?? 1_000_000_000) / 1e9).toFixed(1)} SOL</span>
100
- <span>{wallet.requireApproval ? 'Approval required' : 'Auto-send'}</span>
101
- </div>
247
+ <div className="mt-3 p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50">
248
+ <p className="text-[12px] text-text-3/70">
249
+ This agent already has both supported wallet types connected. Use the default toggle above to choose which wallet autonomous actions use when no chain is specified.
250
+ </p>
251
+ {error && <p className="text-[11px] text-red-400 mt-2">{error}</p>}
102
252
  </div>
103
253
  )}
104
254
  </div>
@@ -3,6 +3,7 @@
3
3
  import { useEffect, useMemo, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { BottomSheet } from '@/components/shared/bottom-sheet'
6
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
6
7
  import { api } from '@/lib/api-client'
7
8
  import { copyTextToClipboard } from '@/lib/clipboard'
8
9
  import type { Webhook, WebhookLogEntry } from '@/types'
@@ -61,6 +62,8 @@ export function WebhookSheet() {
61
62
  const [tab, setTab] = useState<'config' | 'history'>('config')
62
63
  const [history, setHistory] = useState<WebhookLogEntry[]>([])
63
64
  const [historyLoading, setHistoryLoading] = useState(false)
65
+ const [confirmDelete, setConfirmDelete] = useState(false)
66
+ const [deleting, setDeleting] = useState(false)
64
67
 
65
68
  const editing = editingId ? (webhooks[editingId] as Webhook | undefined) : null
66
69
  const endpoint = editing ? webhookUrl(editing.id) : ''
@@ -109,6 +112,8 @@ export function WebhookSheet() {
109
112
  }, [editing, open])
110
113
 
111
114
  const handleClose = () => {
115
+ setConfirmDelete(false)
116
+ setDeleting(false)
112
117
  setOpen(false)
113
118
  setEditingId(null)
114
119
  }
@@ -164,17 +169,21 @@ export function WebhookSheet() {
164
169
  }
165
170
 
166
171
  const handleDelete = async () => {
167
- if (!editing || !confirm('Delete this webhook?')) return
172
+ if (!editing) return
173
+ setDeleting(true)
168
174
  try {
169
175
  const res = await api<DeleteWebhookResponse>('DELETE', `/webhooks/${editing.id}`)
170
176
  if ('error' in res && res.error) throw new Error(res.error)
171
177
  toast.success('Webhook deleted')
172
178
  await loadWebhooks()
179
+ setConfirmDelete(false)
173
180
  handleClose()
174
181
  } catch (err: unknown) {
175
182
  const msg = err instanceof Error ? err.message : 'Failed to delete webhook'
176
183
  setError(msg)
177
184
  toast.error(msg)
185
+ } finally {
186
+ setDeleting(false)
178
187
  }
179
188
  }
180
189
 
@@ -381,7 +390,7 @@ export function WebhookSheet() {
381
390
  <div className="flex gap-3 pt-2">
382
391
  {editing && (
383
392
  <button
384
- onClick={handleDelete}
393
+ onClick={() => setConfirmDelete(true)}
385
394
  className="px-5 py-3 rounded-[14px] border border-danger/30 bg-transparent text-danger text-[14px] font-600 cursor-pointer hover:bg-danger/10 transition-colors"
386
395
  style={{ fontFamily: 'inherit' }}
387
396
  >
@@ -407,6 +416,17 @@ export function WebhookSheet() {
407
416
  </div>
408
417
  </>}
409
418
  </div>
419
+ <ConfirmDialog
420
+ open={confirmDelete}
421
+ title="Delete Webhook?"
422
+ message={editing ? `Delete "${editing.name}"? This will remove the endpoint and its saved webhook configuration.` : 'Delete this webhook?'}
423
+ confirmLabel={deleting ? 'Deleting...' : 'Delete'}
424
+ confirmDisabled={deleting}
425
+ cancelDisabled={deleting}
426
+ danger
427
+ onConfirm={() => { void handleDelete() }}
428
+ onCancel={() => { if (!deleting) setConfirmDelete(false) }}
429
+ />
410
430
  </BottomSheet>
411
431
  )
412
432
  }
@@ -36,6 +36,14 @@ export function getApprovalTitle(approval: ApprovalRequest): string {
36
36
  const pluginId = getApprovalPluginId(approval)
37
37
  return pluginId ? `Enable Plugin: ${pluginId}` : 'Enable Plugin'
38
38
  }
39
+ if (approval.category === 'connector_sender') {
40
+ const data = dataObject(approval)
41
+ const senderName = typeof data.senderName === 'string' ? data.senderName.trim() : ''
42
+ const senderId = typeof data.senderId === 'string' ? data.senderId.trim() : ''
43
+ const connectorName = typeof data.connectorName === 'string' ? data.connectorName.trim() : ''
44
+ const subject = senderName || senderId || 'Unknown sender'
45
+ return connectorName ? `Approve ${subject} on ${connectorName}` : `Approve ${subject}`
46
+ }
39
47
  return approval.title || 'Approval Request'
40
48
  }
41
49
 
@@ -58,5 +66,17 @@ export function getApprovalPayload(approval: ApprovalRequest): Record<string, un
58
66
  }
59
67
  }
60
68
 
69
+ if (approval.category === 'connector_sender') {
70
+ return {
71
+ connectorId: typeof data.connectorId === 'string' ? data.connectorId : null,
72
+ connectorName: typeof data.connectorName === 'string' ? data.connectorName : null,
73
+ platform: typeof data.platform === 'string' ? data.platform : null,
74
+ senderId: typeof data.senderId === 'string' ? data.senderId : null,
75
+ senderName: typeof data.senderName === 'string' ? data.senderName : null,
76
+ channelId: typeof data.channelId === 'string' ? data.channelId : null,
77
+ policy: typeof data.policy === 'string' ? data.policy : null,
78
+ }
79
+ }
80
+
61
81
  return (sanitizeValue(data) || {}) as Record<string, unknown>
62
82
  }
@@ -0,0 +1,198 @@
1
+ import type {
2
+ CanvasActionItem,
3
+ CanvasBlock,
4
+ CanvasCardItem,
5
+ CanvasContent,
6
+ CanvasDocument,
7
+ CanvasMetricItem,
8
+ CanvasTableData,
9
+ } from '@/types'
10
+
11
+ function asObject(value: unknown): Record<string, unknown> | null {
12
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null
13
+ return value as Record<string, unknown>
14
+ }
15
+
16
+ function asTrimmedString(value: unknown, max = 8000): string | null {
17
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value).slice(0, max)
18
+ if (typeof value !== 'string') return null
19
+ const trimmed = value.trim()
20
+ return trimmed ? trimmed.slice(0, max) : null
21
+ }
22
+
23
+ function asStringArray(value: unknown): string[] {
24
+ if (!Array.isArray(value)) return []
25
+ return value
26
+ .map((entry) => asTrimmedString(entry, 240))
27
+ .filter((entry): entry is string => Boolean(entry))
28
+ }
29
+
30
+ function normalizeMetricItems(value: unknown): CanvasMetricItem[] {
31
+ if (!Array.isArray(value)) return []
32
+ const items: CanvasMetricItem[] = []
33
+ for (const entry of value) {
34
+ const row = asObject(entry)
35
+ if (!row) continue
36
+ const label = asTrimmedString(row.label, 120)
37
+ const metricValue = asTrimmedString(row.value, 120)
38
+ if (!label || !metricValue) continue
39
+ items.push({
40
+ label,
41
+ value: metricValue,
42
+ detail: asTrimmedString(row.detail, 240) || undefined,
43
+ tone: row.tone === 'positive' || row.tone === 'negative' || row.tone === 'warning' ? row.tone : 'default',
44
+ })
45
+ if (items.length >= 24) break
46
+ }
47
+ return items
48
+ }
49
+
50
+ function normalizeCardItems(value: unknown): CanvasCardItem[] {
51
+ if (!Array.isArray(value)) return []
52
+ const items: CanvasCardItem[] = []
53
+ for (const entry of value) {
54
+ const row = asObject(entry)
55
+ if (!row) continue
56
+ const title = asTrimmedString(row.title, 180)
57
+ if (!title) continue
58
+ items.push({
59
+ title,
60
+ body: asTrimmedString(row.body, 1600) || undefined,
61
+ meta: asTrimmedString(row.meta, 200) || undefined,
62
+ tone: row.tone === 'positive' || row.tone === 'negative' || row.tone === 'warning' ? row.tone : 'default',
63
+ })
64
+ if (items.length >= 24) break
65
+ }
66
+ return items
67
+ }
68
+
69
+ function normalizeActionItems(value: unknown): CanvasActionItem[] {
70
+ if (!Array.isArray(value)) return []
71
+ const items: CanvasActionItem[] = []
72
+ for (const entry of value) {
73
+ const row = asObject(entry)
74
+ if (!row) continue
75
+ const label = asTrimmedString(row.label, 120)
76
+ if (!label) continue
77
+ items.push({
78
+ label,
79
+ href: asTrimmedString(row.href, 1000) || undefined,
80
+ note: asTrimmedString(row.note, 240) || undefined,
81
+ intent: row.intent === 'primary' || row.intent === 'success' || row.intent === 'danger' ? row.intent : 'secondary',
82
+ })
83
+ if (items.length >= 24) break
84
+ }
85
+ return items
86
+ }
87
+
88
+ function normalizeTable(value: unknown): CanvasTableData | null {
89
+ const row = asObject(value)
90
+ if (!row) return null
91
+ const columns = asStringArray(row.columns).slice(0, 20)
92
+ if (!columns.length) return null
93
+ const rows = Array.isArray(row.rows)
94
+ ? row.rows
95
+ .map((entry) => Array.isArray(entry)
96
+ ? entry.slice(0, columns.length).map((cell) => (
97
+ typeof cell === 'string'
98
+ || typeof cell === 'number'
99
+ || typeof cell === 'boolean'
100
+ || cell === null
101
+ ? cell
102
+ : JSON.stringify(cell)
103
+ ))
104
+ : null)
105
+ .filter((entry): entry is Array<string | number | boolean | null> => Array.isArray(entry))
106
+ .slice(0, 100)
107
+ : []
108
+ return rows.length
109
+ ? {
110
+ columns,
111
+ rows,
112
+ caption: asTrimmedString(row.caption, 240) || undefined,
113
+ }
114
+ : null
115
+ }
116
+
117
+ function normalizeBlock(value: unknown): CanvasBlock | null {
118
+ const row = asObject(value)
119
+ if (!row) return null
120
+ const title = asTrimmedString(row.title, 160) || undefined
121
+ switch (row.type) {
122
+ case 'markdown': {
123
+ const markdown = asTrimmedString(row.markdown, 20_000)
124
+ return markdown ? { type: 'markdown', title, markdown } : null
125
+ }
126
+ case 'metrics': {
127
+ const items = normalizeMetricItems(row.items)
128
+ return items.length ? { type: 'metrics', title, items } : null
129
+ }
130
+ case 'cards': {
131
+ const items = normalizeCardItems(row.items)
132
+ return items.length ? { type: 'cards', title, items } : null
133
+ }
134
+ case 'table': {
135
+ const table = normalizeTable(row.table)
136
+ return table ? { type: 'table', title, table } : null
137
+ }
138
+ case 'code': {
139
+ const code = asTrimmedString(row.code, 20_000)
140
+ return code ? { type: 'code', title, code, language: asTrimmedString(row.language, 60) || undefined } : null
141
+ }
142
+ case 'actions': {
143
+ const items = normalizeActionItems(row.items)
144
+ return items.length ? { type: 'actions', title, items } : null
145
+ }
146
+ default:
147
+ return null
148
+ }
149
+ }
150
+
151
+ export function normalizeCanvasDocument(value: unknown): CanvasDocument | null {
152
+ const row = asObject(value)
153
+ if (!row) return null
154
+ const blocks = Array.isArray(row.blocks)
155
+ ? row.blocks.map((entry) => normalizeBlock(entry)).filter((entry): entry is CanvasBlock => entry !== null).slice(0, 24)
156
+ : []
157
+ if (!blocks.length) return null
158
+ return {
159
+ kind: 'structured',
160
+ title: asTrimmedString(row.title, 180) || undefined,
161
+ subtitle: asTrimmedString(row.subtitle, 320) || undefined,
162
+ theme: row.theme === 'sky' || row.theme === 'emerald' || row.theme === 'amber' || row.theme === 'rose' ? row.theme : 'slate',
163
+ blocks,
164
+ updatedAt: typeof row.updatedAt === 'number' && Number.isFinite(row.updatedAt) ? row.updatedAt : Date.now(),
165
+ }
166
+ }
167
+
168
+ export function isCanvasDocument(value: unknown): value is CanvasDocument {
169
+ return normalizeCanvasDocument(value) !== null
170
+ }
171
+
172
+ export function normalizeCanvasContent(value: unknown): CanvasContent {
173
+ if (typeof value === 'string') return value || null
174
+ if (value === null || value === undefined) return null
175
+ return normalizeCanvasDocument(value)
176
+ }
177
+
178
+ export function summarizeCanvasContent(content: CanvasContent): Record<string, unknown> {
179
+ if (!content) {
180
+ return { kind: 'empty', hasContent: false, contentLength: 0, preview: null }
181
+ }
182
+ if (typeof content === 'string') {
183
+ return {
184
+ kind: 'html',
185
+ hasContent: true,
186
+ contentLength: content.length,
187
+ preview: content.slice(0, 500),
188
+ }
189
+ }
190
+ return {
191
+ kind: 'structured',
192
+ hasContent: true,
193
+ blockCount: content.blocks.length,
194
+ title: content.title || null,
195
+ blockTypes: content.blocks.map((block) => block.type),
196
+ preview: JSON.stringify(content).slice(0, 500),
197
+ }
198
+ }