@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,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useState, useEffect, useCallback, useMemo } from 'react'
3
+ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
4
4
  import { api } from '@/lib/api-client'
5
5
  import { copyTextToClipboard } from '@/lib/clipboard'
6
6
  import { useAppStore } from '@/stores/use-app-store'
@@ -8,9 +8,47 @@ import { useWs } from '@/hooks/use-ws'
8
8
  import { WalletApprovalDialog } from './wallet-approval-dialog'
9
9
  import { AgentPickerList } from '@/components/shared/agent-picker-list'
10
10
  import { AgentAvatar } from '@/components/agents/agent-avatar'
11
- import type { AgentWallet, WalletTransaction, WalletBalanceSnapshot, Agent } from '@/types'
11
+ import type { AgentWallet, WalletTransaction, WalletBalanceSnapshot, WalletAssetBalance, WalletPortfolioSummary, Agent, WalletChain } from '@/types'
12
+ import {
13
+ SUPPORTED_WALLET_CHAINS,
14
+ formatWalletAmount,
15
+ getWalletAssetSymbol,
16
+ getWalletAtomicAmount,
17
+ getWalletBalanceAtomic,
18
+ getWalletChainMeta,
19
+ getWalletLimitAtomic,
20
+ parseDisplayAmountToAtomic,
21
+ } from '@/lib/wallet'
22
+ import { type WalletTransactionFilter, filterWalletTransactions, getWalletTransactionStatusGroup } from '@/lib/wallet-transactions'
23
+ import { toast } from 'sonner'
24
+
25
+ type SafeWallet = Omit<AgentWallet, 'encryptedPrivateKey'> & {
26
+ balanceAtomic?: string
27
+ balanceLamports?: number
28
+ balanceFormatted?: string
29
+ balanceSymbol?: string
30
+ assets?: WalletAssetBalance[]
31
+ portfolioSummary?: WalletPortfolioSummary
32
+ isActive?: boolean
33
+ }
34
+
35
+ function getAgentWalletIds(agent: Agent | undefined | null): string[] {
36
+ const ids = Array.isArray(agent?.walletIds)
37
+ ? agent.walletIds.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
38
+ : []
39
+ const legacy = typeof agent?.walletId === 'string' && agent.walletId.trim()
40
+ ? [agent.walletId.trim()]
41
+ : []
42
+ return [...new Set([...ids, ...legacy])]
43
+ }
12
44
 
13
- type SafeWallet = Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }
45
+ function getAgentActiveWalletId(agent: Agent | undefined | null, fallbackWallets: SafeWallet[] = []): string | null {
46
+ const walletIds = getAgentWalletIds(agent)
47
+ if (typeof agent?.activeWalletId === 'string' && walletIds.includes(agent.activeWalletId)) return agent.activeWalletId
48
+ if (typeof agent?.walletId === 'string' && walletIds.includes(agent.walletId)) return agent.walletId
49
+ const activeWallet = fallbackWallets.find((wallet) => wallet.isActive)
50
+ return activeWallet?.id || fallbackWallets[0]?.id || walletIds[0] || null
51
+ }
14
52
 
15
53
  function SolanaIcon({ size = 12, className = '', shimmer = false }: { size?: number; className?: string; shimmer?: boolean }) {
16
54
  return (
@@ -33,6 +71,45 @@ function SolanaIcon({ size = 12, className = '', shimmer = false }: { size?: num
33
71
  )
34
72
  }
35
73
 
74
+ function EthereumIcon({ size = 12, className = '', shimmer = false }: { size?: number; className?: string; shimmer?: boolean }) {
75
+ return (
76
+ <div className={`relative flex items-center justify-center ${className}`}>
77
+ <svg width={size} height={size} viewBox="0 0 256 417" className="relative z-10" fill="none" xmlns="http://www.w3.org/2000/svg">
78
+ <path d="M127.6 0L124.8 9.5V279.1L127.6 281.9L255.2 208.3L127.6 0Z" fill="#8A92B2" />
79
+ <path d="M127.6 0L0 208.3L127.6 281.9V151.1V0Z" fill="#62688F" />
80
+ <path d="M127.6 306.1L126 308V416.9L127.6 421.6L255.3 232.6L127.6 306.1Z" fill="#8A92B2" />
81
+ <path d="M127.6 421.6V306.1L0 232.6L127.6 421.6Z" fill="#62688F" />
82
+ <path d="M127.6 281.9L255.2 208.3L127.6 151.1V281.9Z" fill="#454A75" />
83
+ <path d="M0 208.3L127.6 281.9V151.1L0 208.3Z" fill="#8A92B2" />
84
+ </svg>
85
+ {shimmer && (
86
+ <div className="absolute inset-0 bg-sky-400/20 blur-md rounded-full animate-pulse" />
87
+ )}
88
+ </div>
89
+ )
90
+ }
91
+
92
+ function ChainIcon({ chain, size = 12, className = '', shimmer = false }: { chain: WalletChain; size?: number; className?: string; shimmer?: boolean }) {
93
+ if (chain === 'ethereum') return <EthereumIcon size={size} className={className} shimmer={shimmer} />
94
+ return <SolanaIcon size={size} className={className} shimmer={shimmer} />
95
+ }
96
+
97
+ function walletBalanceLabel(wallet: SafeWallet): string {
98
+ return wallet.balanceFormatted || formatWalletAmount(wallet.chain, getWalletBalanceAtomic(wallet), { minFractionDigits: 3, maxFractionDigits: 6 })
99
+ }
100
+
101
+ function walletAssetCountLabel(wallet: SafeWallet): string | null {
102
+ const count = wallet.portfolioSummary?.nonZeroAssets
103
+ if (!count) return null
104
+ return `${count} asset${count === 1 ? '' : 's'}`
105
+ }
106
+
107
+ function suggestCreateChain(wallets: SafeWallet[], agentId?: string | null): WalletChain {
108
+ if (!agentId) return 'solana'
109
+ const connectedChains = new Set(wallets.filter((wallet) => wallet.agentId === agentId).map((wallet) => wallet.chain))
110
+ return SUPPORTED_WALLET_CHAINS.find((chain) => !connectedChains.has(chain)) || 'solana'
111
+ }
112
+
36
113
  export function WalletPanel() {
37
114
  const agents = useAppStore((s) => s.agents)
38
115
  const walletPanelAgentId = useAppStore((s) => s.walletPanelAgentId)
@@ -45,7 +122,11 @@ export function WalletPanel() {
45
122
  const [transactions, setTransactions] = useState<WalletTransaction[]>([])
46
123
  const [balanceHistory, setBalanceHistory] = useState<WalletBalanceSnapshot[]>([])
47
124
  const [loading, setLoading] = useState(true)
125
+ const [transactionsLoading, setTransactionsLoading] = useState(false)
48
126
  const [pendingApproval, setPendingApproval] = useState<WalletTransaction | null>(null)
127
+ const [transactionFilter, setTransactionFilter] = useState<WalletTransactionFilter>('all')
128
+ const [transactionQuery, setTransactionQuery] = useState('')
129
+ const detailRequestRef = useRef(0)
49
130
 
50
131
  // Settings edit state
51
132
  const [editingLimits, setEditingLimits] = useState(false)
@@ -55,6 +136,7 @@ export function WalletPanel() {
55
136
  const [saving, setSaving] = useState(false)
56
137
  const [deleting, setDeleting] = useState(false)
57
138
  const [confirmDelete, setConfirmDelete] = useState(false)
139
+ const [settingDefault, setSettingDefault] = useState(false)
58
140
  const [reassigning, setReassigning] = useState(false)
59
141
  const [reassignSaving, setReassignSaving] = useState(false)
60
142
  const [reassignError, setReassignError] = useState('')
@@ -62,6 +144,7 @@ export function WalletPanel() {
62
144
  // Create wallet state
63
145
  const [showCreateForm, setShowCreateForm] = useState(false)
64
146
  const [createAgentId, setCreateAgentId] = useState('')
147
+ const [createChain, setCreateChain] = useState<WalletChain>('solana')
65
148
  const [creating, setCreating] = useState(false)
66
149
  const [createError, setCreateError] = useState('')
67
150
 
@@ -71,7 +154,8 @@ export function WalletPanel() {
71
154
  setWallets(data)
72
155
 
73
156
  if (!walletPanelAgentId && !selectedWalletId && Object.keys(data).length > 0) {
74
- setSelectedWalletId(Object.keys(data)[0])
157
+ const defaultWallet = Object.values(data).find((wallet) => wallet.isActive) || Object.values(data)[0]
158
+ if (defaultWallet) setSelectedWalletId(defaultWallet.id)
75
159
  }
76
160
  } catch { /* ignore */ }
77
161
  setLoading(false)
@@ -79,70 +163,116 @@ export function WalletPanel() {
79
163
  }, [walletPanelAgentId])
80
164
 
81
165
  useEffect(() => { loadWallets() }, [loadWallets])
82
- useWs('wallets', loadWallets, 15000)
83
166
 
167
+ // Sync wallet selection when agent panel changes
84
168
  useEffect(() => {
85
169
  if (!walletPanelAgentId) return
86
- const match = Object.values(wallets).find((wallet) => wallet.agentId === walletPanelAgentId)
170
+ const agentWallets = Object.values(wallets).filter((wallet) => wallet.agentId === walletPanelAgentId)
171
+ const selectedAgentWallet = selectedWalletId
172
+ ? agentWallets.find((wallet) => wallet.id === selectedWalletId) || null
173
+ : null
174
+ if (selectedAgentWallet) {
175
+ setShowCreateForm(false)
176
+ setCreateError('')
177
+ setCreateAgentId(walletPanelAgentId)
178
+ return
179
+ }
180
+ const activeWalletId = getAgentActiveWalletId(agents[walletPanelAgentId] as Agent | undefined, agentWallets)
181
+ const match = agentWallets.find((wallet) => wallet.id === activeWalletId) || agentWallets[0]
87
182
  if (match) {
88
183
  setSelectedWalletId(match.id)
89
184
  setShowCreateForm(false)
90
185
  setCreateError('')
186
+ setCreateAgentId(walletPanelAgentId)
91
187
  return
92
188
  }
93
189
  if (!agents[walletPanelAgentId]) return
94
190
  setSelectedWalletId(null)
95
191
  setShowCreateForm(true)
96
192
  setCreateAgentId(walletPanelAgentId)
193
+ setCreateChain(suggestCreateChain(Object.values(wallets), walletPanelAgentId))
97
194
  setCreateError('')
98
- }, [agents, walletPanelAgentId, wallets])
195
+ }, [agents, selectedWalletId, walletPanelAgentId, wallets])
99
196
 
100
197
  // Load detail when wallet selected
101
198
  const selectedWallet = selectedWalletId ? wallets[selectedWalletId] : null
102
199
 
103
- const loadDetail = useCallback(async () => {
104
- if (!selectedWalletId) return
105
- try {
106
- const [detail, txs, history] = await Promise.all([
107
- api<SafeWallet>('GET', `/wallets/${selectedWalletId}`),
108
- api<WalletTransaction[]>('GET', `/wallets/${selectedWalletId}/transactions`),
109
- api<WalletBalanceSnapshot[]>('GET', `/wallets/${selectedWalletId}/balance-history`),
110
- ])
111
- setWallets((prev) => ({ ...prev, [selectedWalletId]: detail }))
112
- setTransactions(txs)
113
- setBalanceHistory(history)
114
-
115
- // Check for pending approvals
116
- const pending = txs.find((tx) => tx.status === 'pending_approval')
117
- if (pending) setPendingApproval(pending)
118
- } catch { /* ignore */ }
200
+ const loadDetail = useCallback(async (walletId = selectedWalletId) => {
201
+ if (!walletId) return
202
+ const requestId = ++detailRequestRef.current
203
+ setTransactionsLoading(true)
204
+ const [detailResult, txResult, historyResult] = await Promise.allSettled([
205
+ api<SafeWallet>('GET', `/wallets/${walletId}`),
206
+ api<WalletTransaction[]>('GET', `/wallets/${walletId}/transactions`),
207
+ api<WalletBalanceSnapshot[]>('GET', `/wallets/${walletId}/balance-history`),
208
+ ])
209
+ if (detailRequestRef.current !== requestId) return
210
+
211
+ if (detailResult.status === 'fulfilled') {
212
+ setWallets((prev) => ({ ...prev, [walletId]: detailResult.value }))
213
+ }
214
+ if (txResult.status === 'fulfilled') {
215
+ setTransactions(txResult.value)
216
+ const pending = txResult.value.find((tx) => tx.status === 'pending_approval')
217
+ setPendingApproval(pending || null)
218
+ }
219
+ if (historyResult.status === 'fulfilled') {
220
+ setBalanceHistory(historyResult.value)
221
+ }
222
+ setTransactionsLoading(false)
119
223
  }, [selectedWalletId])
120
224
 
121
225
  useEffect(() => { loadDetail() }, [loadDetail])
122
226
 
227
+ const refreshWalletData = useCallback(async () => {
228
+ await loadWallets()
229
+ if (selectedWalletId) {
230
+ await loadDetail(selectedWalletId)
231
+ }
232
+ }, [loadDetail, loadWallets, selectedWalletId])
233
+
234
+ useWs('wallets', refreshWalletData, 15000)
235
+
123
236
  // Initialize limits when wallet selected
124
237
  useEffect(() => {
125
238
  if (selectedWallet) {
126
- setPerTxLimit(String((selectedWallet.spendingLimitLamports ?? 100_000_000) / 1e9))
127
- setDailyLimit(String((selectedWallet.dailyLimitLamports ?? 1_000_000_000) / 1e9))
239
+ setPerTxLimit(formatWalletAmount(selectedWallet.chain, getWalletLimitAtomic(selectedWallet, 'perTx'), { maxFractionDigits: 6 }))
240
+ setDailyLimit(formatWalletAmount(selectedWallet.chain, getWalletLimitAtomic(selectedWallet, 'daily'), { maxFractionDigits: 6 }))
128
241
  setRequireApproval(selectedWallet.requireApproval)
129
242
  }
130
243
  }, [selectedWallet])
131
244
 
132
245
  const saveLimits = useCallback(async () => {
133
- if (!selectedWalletId) return
246
+ if (!selectedWalletId || !selectedWallet) return
134
247
  setSaving(true)
135
248
  try {
249
+ const spendingLimitAtomic = parseDisplayAmountToAtomic(perTxLimit || '0', getWalletChainMeta(selectedWallet.chain).decimals)
250
+ const dailyLimitAtomic = parseDisplayAmountToAtomic(dailyLimit || '0', getWalletChainMeta(selectedWallet.chain).decimals)
136
251
  await api('PATCH', `/wallets/${selectedWalletId}`, {
137
- spendingLimitLamports: Math.round(parseFloat(perTxLimit || '0.1') * 1e9),
138
- dailyLimitLamports: Math.round(parseFloat(dailyLimit || '1') * 1e9),
252
+ spendingLimitAtomic,
253
+ dailyLimitAtomic,
139
254
  requireApproval,
140
255
  })
141
256
  setEditingLimits(false)
142
257
  loadDetail()
143
- } catch { /* ignore */ }
258
+ } catch (err: unknown) {
259
+ toast.error(err instanceof Error ? err.message : String(err))
260
+ }
144
261
  setSaving(false)
145
- }, [selectedWalletId, perTxLimit, dailyLimit, requireApproval, loadDetail])
262
+ }, [selectedWalletId, selectedWallet, perTxLimit, dailyLimit, requireApproval, loadDetail])
263
+
264
+ const setDefaultWallet = useCallback(async () => {
265
+ if (!selectedWalletId) return
266
+ setSettingDefault(true)
267
+ try {
268
+ await api('PATCH', `/wallets/${selectedWalletId}`, { makeActive: true })
269
+ toast.success('Default wallet updated')
270
+ loadWallets()
271
+ } catch (err: unknown) {
272
+ toast.error(err instanceof Error ? err.message : String(err))
273
+ }
274
+ setSettingDefault(false)
275
+ }, [selectedWalletId, loadWallets])
146
276
 
147
277
  const handleDelete = useCallback(async () => {
148
278
  if (!selectedWalletId) return
@@ -165,25 +295,42 @@ export function WalletPanel() {
165
295
  setTimeout(() => setCopied(false), 2000)
166
296
  }, [selectedWallet])
167
297
 
168
- const agentsWithoutWallets = useMemo(() => {
169
- const walletAgentIds = new Set(Object.values(wallets).map((w) => w.agentId))
170
- return Object.values(agents).filter((a) => !walletAgentIds.has(a.id)) as Agent[]
298
+ const agentsMissingSelectedChain = useMemo(() => {
299
+ return Object.values(agents).filter((agent) => !Object.values(wallets).some((wallet) => wallet.agentId === agent.id && wallet.chain === createChain)) as Agent[]
300
+ }, [agents, createChain, wallets])
301
+
302
+ const canCreateMoreWallets = useMemo(() => {
303
+ return Object.values(agents).some((agent) =>
304
+ SUPPORTED_WALLET_CHAINS.some((chain) => !Object.values(wallets).some((wallet) => wallet.agentId === agent.id && wallet.chain === chain)),
305
+ )
171
306
  }, [agents, wallets])
172
307
 
308
+ useEffect(() => {
309
+ if (!createAgentId) return
310
+ if (agentsMissingSelectedChain.some((agent) => agent.id === createAgentId)) return
311
+ setCreateAgentId('')
312
+ }, [agentsMissingSelectedChain, createAgentId])
313
+
173
314
  const createWallet = useCallback(async () => {
174
315
  if (!createAgentId) return
175
316
  setCreating(true)
176
317
  setCreateError('')
177
318
  try {
178
- await api('POST', '/wallets', { agentId: createAgentId })
319
+ await api('POST', '/wallets', { agentId: createAgentId, chain: createChain })
179
320
  setShowCreateForm(false)
180
321
  setCreateAgentId('')
322
+ setCreateChain('solana')
181
323
  loadWallets()
182
324
  } catch (err: unknown) {
183
325
  setCreateError(err instanceof Error ? err.message : String(err))
184
326
  }
185
327
  setCreating(false)
186
- }, [createAgentId, loadWallets])
328
+ }, [createAgentId, createChain, loadWallets])
329
+
330
+ const filteredTransactions = useMemo(
331
+ () => filterWalletTransactions(transactions, { filter: transactionFilter, query: transactionQuery }),
332
+ [transactionFilter, transactionQuery, transactions],
333
+ )
187
334
 
188
335
  if (loading) {
189
336
  return (
@@ -193,7 +340,31 @@ export function WalletPanel() {
193
340
  )
194
341
  }
195
342
 
196
- const walletList = Object.values(wallets)
343
+ const walletList = Object.values(wallets).sort((a, b) => {
344
+ const aAgent = agents[a.agentId] as Agent | undefined
345
+ const bAgent = agents[b.agentId] as Agent | undefined
346
+ const aActive = a.isActive === true || getAgentActiveWalletId(aAgent, [a]) === a.id
347
+ const bActive = b.isActive === true || getAgentActiveWalletId(bAgent, [b]) === b.id
348
+ if (a.agentId === b.agentId && aActive !== bActive) return aActive ? -1 : 1
349
+ const agentCompare = (aAgent?.name || a.agentId).localeCompare(bAgent?.name || b.agentId)
350
+ if (agentCompare !== 0) return agentCompare
351
+ return a.chain.localeCompare(b.chain)
352
+ })
353
+ const selectedWalletMeta = selectedWallet ? getWalletChainMeta(selectedWallet.chain) : null
354
+ const selectedWalletSymbol = selectedWallet ? getWalletAssetSymbol(selectedWallet.chain) : null
355
+ const selectedWalletBalance = selectedWallet ? walletBalanceLabel(selectedWallet) : null
356
+ const selectedWalletAssets = (selectedWallet?.assets || []).filter((asset) => BigInt(asset.balanceAtomic) > BigInt(0))
357
+ const selectedAgent = selectedWallet ? agents[selectedWallet.agentId] as Agent | undefined : undefined
358
+ const selectedAgentWallets = selectedWallet
359
+ ? walletList.filter((wallet) => wallet.agentId === selectedWallet.agentId)
360
+ : []
361
+ const selectedAgentActiveWalletId = getAgentActiveWalletId(selectedAgent, selectedAgentWallets)
362
+ const reassignCandidates = selectedWallet
363
+ ? (Object.values(agents).filter((agent) => (
364
+ agent.id !== selectedWallet.agentId
365
+ && !walletList.some((wallet) => wallet.agentId === agent.id && wallet.chain === selectedWallet.chain)
366
+ )) as Agent[])
367
+ : []
197
368
 
198
369
  if (walletList.length === 0) {
199
370
  return (
@@ -203,14 +374,23 @@ export function WalletPanel() {
203
374
  <rect x="2" y="6" width="20" height="14" rx="2" /><path d="M22 10H18a2 2 0 0 0 0 4h4" /><path d="M6 6V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2" />
204
375
  </svg>
205
376
  <h3 className="font-display text-[14px] font-600 text-text-2 mb-2">No wallets yet</h3>
206
- {agentsWithoutWallets.length > 0 ? (
377
+ {agentsMissingSelectedChain.length > 0 ? (
207
378
  <div className="mt-4 space-y-3">
208
379
  <AgentPickerList
209
- agents={agentsWithoutWallets}
380
+ agents={agentsMissingSelectedChain}
210
381
  selected={createAgentId}
211
382
  onSelect={(id) => setCreateAgentId(id === createAgentId ? '' : id)}
212
383
  maxHeight={180}
213
384
  />
385
+ <select
386
+ value={createChain}
387
+ onChange={(e) => setCreateChain(e.target.value as WalletChain)}
388
+ className="w-full px-3 py-2 rounded-[8px] border border-white/[0.08] bg-surface text-[12px] text-text-1 outline-none focus:border-accent/40"
389
+ style={{ fontFamily: 'inherit' }}
390
+ >
391
+ <option value="solana">Solana</option>
392
+ <option value="ethereum">Ethereum (EVM)</option>
393
+ </select>
214
394
  <button
215
395
  type="button"
216
396
  onClick={createWallet}
@@ -224,7 +404,7 @@ export function WalletPanel() {
224
404
  </div>
225
405
  ) : (
226
406
  <p className="text-[12px] text-text-3/60">
227
- All agents already have wallets.
407
+ Every agent already has a {getWalletChainMeta(createChain).label} wallet.
228
408
  </p>
229
409
  )}
230
410
  </div>
@@ -240,10 +420,15 @@ export function WalletPanel() {
240
420
  <h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em] flex-1">Wallets</h2>
241
421
  <button
242
422
  type="button"
243
- onClick={() => { setShowCreateForm(!showCreateForm); setCreateAgentId(''); setCreateError('') }}
244
- disabled={agentsWithoutWallets.length === 0}
423
+ onClick={() => {
424
+ setShowCreateForm(!showCreateForm)
425
+ setCreateAgentId(walletPanelAgentId || '')
426
+ setCreateChain(suggestCreateChain(walletList, walletPanelAgentId))
427
+ setCreateError('')
428
+ }}
429
+ disabled={!canCreateMoreWallets}
245
430
  className="w-6 h-6 rounded-[6px] flex items-center justify-center text-text-3/50 hover:text-text-2 hover:bg-white/[0.06] transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-default"
246
- title={agentsWithoutWallets.length === 0 ? 'All agents have wallets' : 'Create wallet'}
431
+ title={canCreateMoreWallets ? 'Create wallet' : 'Every agent already has both wallet types'}
247
432
  >
248
433
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
249
434
  <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
@@ -255,11 +440,20 @@ export function WalletPanel() {
255
440
  <div className="mx-1 mb-2 p-2.5 rounded-[8px] border border-accent/20 bg-accent-soft/10 space-y-2"
256
441
  style={{ animation: 'spring-in 0.4s var(--ease-spring)' }}>
257
442
  <AgentPickerList
258
- agents={agentsWithoutWallets}
443
+ agents={agentsMissingSelectedChain}
259
444
  selected={createAgentId}
260
445
  onSelect={(id) => setCreateAgentId(id === createAgentId ? '' : id)}
261
446
  maxHeight={160}
262
447
  />
448
+ <select
449
+ value={createChain}
450
+ onChange={(e) => setCreateChain(e.target.value as WalletChain)}
451
+ className="w-full px-2 py-1.5 rounded-[6px] border border-white/[0.08] bg-surface text-[10px] text-text-1 outline-none focus:border-accent/40"
452
+ style={{ fontFamily: 'inherit' }}
453
+ >
454
+ <option value="solana">Solana</option>
455
+ <option value="ethereum">Ethereum (EVM)</option>
456
+ </select>
263
457
  <div className="flex gap-1.5">
264
458
  <button
265
459
  type="button"
@@ -284,6 +478,7 @@ export function WalletPanel() {
284
478
  )}
285
479
  {walletList.map((w, idx) => {
286
480
  const a = agents[w.agentId] as Agent | undefined
481
+ const isActive = w.isActive === true || getAgentActiveWalletId(a, walletList.filter((wallet) => wallet.agentId === w.agentId)) === w.id
287
482
  return (
288
483
  <button
289
484
  key={w.id}
@@ -298,13 +493,19 @@ export function WalletPanel() {
298
493
  >
299
494
  <AgentAvatar seed={a?.avatarSeed || null} avatarUrl={a?.avatarUrl} name={a?.name || '?'} size={28} />
300
495
  <div className="flex-1 min-w-0">
301
- <div className="text-[12px] font-600 truncate">{a?.name || w.agentId}</div>
496
+ <div className="flex items-center gap-1.5 min-w-0">
497
+ <div className="text-[12px] font-600 truncate">{a?.name || w.agentId}</div>
498
+ {isActive && (
499
+ <span className="shrink-0 px-1 py-0.5 rounded-[999px] bg-accent-soft/40 text-accent-bright text-[8px] font-700 uppercase tracking-wide">
500
+ Default
501
+ </span>
502
+ )}
503
+ </div>
302
504
  <div className="text-[10px] text-text-3/50 font-mono truncate mt-0.5 flex items-center gap-1">
303
- {w.chain === 'solana' && <SolanaIcon size={9} className="shrink-0 opacity-50" />}
505
+ <ChainIcon chain={w.chain} size={9} className="shrink-0 opacity-50" />
304
506
  <span className="truncate">{w.publicKey.slice(0, 8)}...{w.publicKey.slice(-4)}</span>
305
- {typeof w.balanceSol === 'number' && (
306
- <span className="text-text-3/40">{w.balanceSol.toFixed(3)} SOL</span>
307
- )}
507
+ <span className="text-text-3/40">{walletBalanceLabel(w)} {getWalletAssetSymbol(w.chain)}</span>
508
+ {walletAssetCountLabel(w) && <span className="text-text-3/35">{walletAssetCountLabel(w)}</span>}
308
509
  </div>
309
510
  </div>
310
511
  </button>
@@ -328,6 +529,49 @@ export function WalletPanel() {
328
529
  </p>
329
530
  </div>
330
531
 
532
+ {selectedAgentWallets.length > 1 && (
533
+ <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50"
534
+ style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.03s both' }}>
535
+ <div className="flex items-center justify-between gap-3 mb-3">
536
+ <div>
537
+ <div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600">Combined Wallet Stats</div>
538
+ <p className="text-[12px] text-text-3/70 mt-1">
539
+ {selectedAgentWallets.length} wallets connected for this agent. Pick a wallet in the sidebar for chain-specific history and controls.
540
+ </p>
541
+ </div>
542
+ <div className="text-right">
543
+ <div className="text-[18px] font-600 text-text-1">{selectedAgentWallets.length}</div>
544
+ <div className="text-[10px] uppercase tracking-wide text-text-3/50">Wallets</div>
545
+ </div>
546
+ </div>
547
+ <div className="grid gap-2 md:grid-cols-2">
548
+ {selectedAgentWallets.map((wallet) => (
549
+ <div key={wallet.id} className="rounded-[10px] border border-white/[0.06] bg-black/10 px-3 py-2">
550
+ <div className="flex items-center gap-2">
551
+ <span className="text-[10px] text-text-3/60 uppercase tracking-wide font-600">
552
+ {getWalletChainMeta(wallet.chain).label}
553
+ </span>
554
+ {(wallet.id === selectedAgentActiveWalletId || wallet.isActive) && (
555
+ <span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft/40 text-accent-bright text-[8px] font-700 uppercase tracking-wide">
556
+ Default
557
+ </span>
558
+ )}
559
+ </div>
560
+ <div className="mt-1 text-[14px] font-600 text-text-1">
561
+ {walletBalanceLabel(wallet)} {getWalletAssetSymbol(wallet.chain)}
562
+ </div>
563
+ {wallet.portfolioSummary?.nonZeroAssets ? (
564
+ <div className="mt-1 text-[10px] text-text-3/55">
565
+ {wallet.portfolioSummary.nonZeroAssets} detected asset{wallet.portfolioSummary.nonZeroAssets === 1 ? '' : 's'}
566
+ </div>
567
+ ) : null}
568
+ <div className="mt-1 text-[10px] text-text-3/55 font-mono truncate">{wallet.publicKey}</div>
569
+ </div>
570
+ ))}
571
+ </div>
572
+ </div>
573
+ )}
574
+
331
575
  {/* Agent & Address */}
332
576
  <div style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.05s both' }}>
333
577
  <div className="flex items-center gap-2 mb-2">
@@ -349,9 +593,14 @@ export function WalletPanel() {
349
593
  )
350
594
  })()}
351
595
  <span className="inline-flex items-center gap-1 text-[11px] text-text-3/40 uppercase tracking-wide font-600">
352
- {selectedWallet.chain === 'solana' && <SolanaIcon size={11} />}
596
+ <ChainIcon chain={selectedWallet.chain} size={11} />
353
597
  {selectedWallet.chain}
354
598
  </span>
599
+ {(selectedWallet.id === selectedAgentActiveWalletId || selectedWallet.isActive) && (
600
+ <span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft/40 text-accent-bright text-[9px] font-700 uppercase tracking-wide">
601
+ Default
602
+ </span>
603
+ )}
355
604
  <button
356
605
  type="button"
357
606
  onClick={() => { setReassigning(!reassigning); setReassignError('') }}
@@ -360,27 +609,44 @@ export function WalletPanel() {
360
609
  >
361
610
  {reassigning ? 'Cancel' : 'Reassign'}
362
611
  </button>
612
+ {selectedWallet.id !== selectedAgentActiveWalletId && !selectedWallet.isActive && (
613
+ <button
614
+ type="button"
615
+ onClick={setDefaultWallet}
616
+ disabled={settingDefault}
617
+ className="text-[10px] text-accent-bright hover:text-white transition-colors cursor-pointer bg-transparent border border-accent-bright/20 px-1.5 py-0.5 rounded-[5px] hover:bg-accent/20 disabled:opacity-50"
618
+ style={{ fontFamily: 'inherit' }}
619
+ >
620
+ {settingDefault ? 'Setting...' : 'Set Default'}
621
+ </button>
622
+ )}
363
623
  </div>
364
624
  {reassigning && (
365
625
  <div className="mb-2 space-y-2" style={{ animation: 'spring-in 0.4s var(--ease-spring)' }}>
366
626
  <p className="text-[11px] text-text-3/60">Select a new agent to control this wallet:</p>
367
- <AgentPickerList
368
- agents={agentsWithoutWallets}
369
- selected=""
370
- onSelect={async (agentId) => {
371
- setReassignSaving(true)
372
- setReassignError('')
373
- try {
374
- await api('PATCH', `/wallets/${selectedWallet.id}`, { agentId })
375
- setReassigning(false)
376
- loadWallets()
377
- } catch (err: unknown) {
378
- setReassignError(err instanceof Error ? err.message : String(err) || 'Reassign failed')
379
- }
380
- setReassignSaving(false)
381
- }}
382
- maxHeight={160}
383
- />
627
+ {reassignCandidates.length > 0 ? (
628
+ <AgentPickerList
629
+ agents={reassignCandidates}
630
+ selected=""
631
+ onSelect={async (agentId) => {
632
+ setReassignSaving(true)
633
+ setReassignError('')
634
+ try {
635
+ await api('PATCH', `/wallets/${selectedWallet.id}`, { agentId })
636
+ setReassigning(false)
637
+ loadWallets()
638
+ } catch (err: unknown) {
639
+ setReassignError(err instanceof Error ? err.message : String(err) || 'Reassign failed')
640
+ }
641
+ setReassignSaving(false)
642
+ }}
643
+ maxHeight={160}
644
+ />
645
+ ) : (
646
+ <p className="text-[10px] text-text-3/50">
647
+ No other agents can take this {selectedWallet.chain} wallet right now.
648
+ </p>
649
+ )}
384
650
  {reassignSaving && <p className="text-[10px] text-text-3/50">Reassigning...</p>}
385
651
  {reassignError && <p className="text-[10px] text-red-400">{reassignError}</p>}
386
652
  </div>
@@ -406,21 +672,61 @@ export function WalletPanel() {
406
672
  <div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-2">Balance</div>
407
673
  <div className="flex items-baseline gap-3">
408
674
  <div className="text-[28px] font-600 text-text-1 tracking-tight">
409
- {(selectedWallet.balanceSol ?? 0).toFixed(4)} <span className="text-[14px] text-text-3/60 font-mono">SOL</span>
675
+ {selectedWalletBalance} <span className="text-[14px] text-text-3/60 font-mono">{selectedWalletSymbol}</span>
410
676
  </div>
411
- {selectedWallet.chain === 'solana' && (
412
- <SolanaIcon size={16} shimmer className="opacity-80" />
413
- )}
677
+ <ChainIcon chain={selectedWallet.chain} size={16} shimmer className="opacity-80" />
678
+ </div>
679
+ <div className="mt-2 text-[11px] text-text-3/60">
680
+ {selectedWallet.portfolioSummary?.nonZeroAssets
681
+ ? `${selectedWallet.portfolioSummary.nonZeroAssets} funded asset${selectedWallet.portfolioSummary.nonZeroAssets === 1 ? '' : 's'} across ${Math.max(selectedWallet.portfolioSummary.networkCount, 1)} network${selectedWallet.portfolioSummary.networkCount === 1 ? '' : 's'}`
682
+ : 'No funded assets detected yet.'}
414
683
  </div>
415
684
  </div>
416
685
 
686
+ <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50"
687
+ style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.13s both' }}>
688
+ <div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-3">Detected Assets</div>
689
+ {selectedWalletAssets.length === 0 ? (
690
+ <p className="text-[12px] text-text-3/55">No funded token or native balances detected yet.</p>
691
+ ) : (
692
+ <div className="space-y-2">
693
+ {selectedWalletAssets.map((asset) => (
694
+ <div key={asset.id} className="flex items-center justify-between gap-3 rounded-[10px] border border-white/[0.06] bg-black/10 px-3 py-2">
695
+ <div className="min-w-0">
696
+ <div className="flex items-center gap-2 min-w-0">
697
+ <span className="text-[12px] font-600 text-text-1 truncate">{asset.symbol}</span>
698
+ <span className="text-[10px] text-text-3/55 uppercase tracking-wide">{asset.networkLabel}</span>
699
+ {asset.isNative && (
700
+ <span className="px-1.5 py-0.5 rounded-[999px] bg-accent-soft/30 text-accent-bright text-[8px] font-700 uppercase tracking-wide">
701
+ Gas
702
+ </span>
703
+ )}
704
+ </div>
705
+ <div className="text-[10px] text-text-3/55 truncate">{asset.name || asset.symbol}</div>
706
+ </div>
707
+ <div className="text-right shrink-0">
708
+ <div className="text-[12px] font-600 text-text-1">{asset.balanceDisplay || asset.balanceFormatted || asset.balanceAtomic}</div>
709
+ {asset.contractAddress && (
710
+ <div className="text-[10px] text-text-3/45 font-mono">{asset.contractAddress.slice(0, 6)}...{asset.contractAddress.slice(-4)}</div>
711
+ )}
712
+ {asset.tokenMint && (
713
+ <div className="text-[10px] text-text-3/45 font-mono">{asset.tokenMint.slice(0, 6)}...{asset.tokenMint.slice(-4)}</div>
714
+ )}
715
+ </div>
716
+ </div>
717
+ ))}
718
+ </div>
719
+ )}
720
+ </div>
721
+
417
722
  {/* Funding help */}
418
723
  <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50"
419
- style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.15s both' }}>
724
+ style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.16s both' }}>
420
725
  <div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-2">How to Fund This Wallet</div>
421
726
  <div className="space-y-2 text-[12px] text-text-3/70 leading-relaxed">
422
- <p>Send SOL to the wallet address above from any Solana wallet (Phantom, Solflare, an exchange, etc.). Copy the address and use it as the recipient.</p>
423
- <p>This wallet is on <strong className="text-text-2 font-600">Solana mainnet</strong>. Make sure you&apos;re sending real SOL on the Solana mainnet network.</p>
727
+ {selectedWalletMeta?.fundingInstructions.map((line) => (
728
+ <p key={line}>{line}</p>
729
+ ))}
424
730
  <p className="text-text-3/50 text-[11px]">The private key is AES-256 encrypted in your local database (<code className="text-[10px] bg-black/20 px-1 py-0.5 rounded">data/swarmclaw.db</code>). It is never exposed via the API. To export it, query the <code className="text-[10px] bg-black/20 px-1 py-0.5 rounded">wallets</code> table directly and decrypt using your <code className="text-[10px] bg-black/20 px-1 py-0.5 rounded">CREDENTIAL_SECRET</code>.</p>
425
731
  </div>
426
732
  </div>
@@ -432,13 +738,15 @@ export function WalletPanel() {
432
738
  <div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-3">Balance Over Time</div>
433
739
  <div className="h-[120px] flex items-end gap-[2px]">
434
740
  {(() => {
435
- const max = Math.max(...balanceHistory.map((s) => s.balanceLamports), 1)
436
- return balanceHistory.slice(-60).map((s, i) => (
741
+ const recentHistory = balanceHistory.slice(-60)
742
+ const balances = recentHistory.map((snapshot) => Number.parseFloat(formatWalletAmount(selectedWallet.chain, getWalletBalanceAtomic(snapshot), { maxFractionDigits: 6 })) || 0)
743
+ const max = Math.max(...balances, 1)
744
+ return recentHistory.map((s, i) => (
437
745
  <div
438
746
  key={s.id || i}
439
747
  className="flex-1 bg-accent/40 rounded-t-[2px] min-w-[3px] transition-all hover:bg-accent hover:scale-y-110"
440
- style={{ height: `${Math.max(2, (s.balanceLamports / max) * 100)}%`, transitionDelay: `${i * 10}ms` }}
441
- title={`${(s.balanceLamports / 1e9).toFixed(4)} SOL — ${new Date(s.timestamp).toLocaleString()}`}
748
+ style={{ height: `${Math.max(2, ((balances[i] || 0) / max) * 100)}%`, transitionDelay: `${i * 10}ms` }}
749
+ title={`${formatWalletAmount(selectedWallet.chain, getWalletBalanceAtomic(s), { minFractionDigits: 4, maxFractionDigits: 6 })} ${selectedWalletSymbol} — ${new Date(s.timestamp).toLocaleString()}`}
442
750
  />
443
751
  ))
444
752
  })()}
@@ -466,7 +774,7 @@ export function WalletPanel() {
466
774
  {editingLimits ? (
467
775
  <div className="space-y-3" style={{ animation: 'fade-in 0.3s ease' }}>
468
776
  <div>
469
- <label className="block text-[11px] text-text-3/70 mb-1">Per-transaction limit (SOL)</label>
777
+ <label className="block text-[11px] text-text-3/70 mb-1">Per-transaction limit ({selectedWalletSymbol})</label>
470
778
  <input
471
779
  type="number"
472
780
  step="0.01"
@@ -477,7 +785,7 @@ export function WalletPanel() {
477
785
  />
478
786
  </div>
479
787
  <div>
480
- <label className="block text-[11px] text-text-3/70 mb-1">Daily limit (SOL)</label>
788
+ <label className="block text-[11px] text-text-3/70 mb-1">Daily limit ({selectedWalletSymbol})</label>
481
789
  <input
482
790
  type="number"
483
791
  step="0.1"
@@ -521,11 +829,11 @@ export function WalletPanel() {
521
829
  <div className="space-y-2 text-[12px]">
522
830
  <div className="flex justify-between">
523
831
  <span className="text-text-3/70">Per-transaction</span>
524
- <span className="text-text-2">{((selectedWallet.spendingLimitLamports ?? 100_000_000) / 1e9).toFixed(2)} SOL</span>
832
+ <span className="text-text-2">{formatWalletAmount(selectedWallet.chain, getWalletLimitAtomic(selectedWallet, 'perTx'), { maxFractionDigits: 6 })} {selectedWalletSymbol}</span>
525
833
  </div>
526
834
  <div className="flex justify-between">
527
835
  <span className="text-text-3/70">Daily rolling</span>
528
- <span className="text-text-2">{((selectedWallet.dailyLimitLamports ?? 1_000_000_000) / 1e9).toFixed(1)} SOL</span>
836
+ <span className="text-text-2">{formatWalletAmount(selectedWallet.chain, getWalletLimitAtomic(selectedWallet, 'daily'), { maxFractionDigits: 6 })} {selectedWalletSymbol}</span>
529
837
  </div>
530
838
  <div className="flex justify-between">
531
839
  <span className="text-text-3/70">Approval</span>
@@ -537,12 +845,45 @@ export function WalletPanel() {
537
845
 
538
846
  {/* Transaction history */}
539
847
  <div style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.3s both' }}>
540
- <div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-3">Transactions</div>
541
- {transactions.length === 0 ? (
848
+ <div className="flex items-center justify-between gap-3 mb-3">
849
+ <div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600">Transactions</div>
850
+ <div className="text-[10px] text-text-3/45">
851
+ {filteredTransactions.length}{filteredTransactions.length !== transactions.length ? ` / ${transactions.length}` : ''} shown
852
+ </div>
853
+ </div>
854
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center mb-3">
855
+ <input
856
+ type="text"
857
+ value={transactionQuery}
858
+ onChange={(e) => setTransactionQuery(e.target.value)}
859
+ placeholder="Search memo, hash, address..."
860
+ className="flex-1 px-3 py-2 rounded-[8px] border border-white/[0.08] bg-surface text-[12px] text-text-1 outline-none focus:border-accent/40"
861
+ style={{ fontFamily: 'inherit' }}
862
+ />
863
+ <select
864
+ value={transactionFilter}
865
+ onChange={(e) => setTransactionFilter(e.target.value as WalletTransactionFilter)}
866
+ className="px-3 py-2 rounded-[8px] border border-white/[0.08] bg-surface text-[12px] text-text-1 outline-none focus:border-accent/40"
867
+ style={{ fontFamily: 'inherit' }}
868
+ >
869
+ <option value="all">All</option>
870
+ <option value="confirmed">Confirmed</option>
871
+ <option value="pending">Pending</option>
872
+ <option value="failed">Failed</option>
873
+ <option value="send">Sends</option>
874
+ <option value="receive">Receives</option>
875
+ <option value="swap">Swaps</option>
876
+ </select>
877
+ </div>
878
+ {transactionsLoading && transactions.length === 0 ? (
879
+ <p className="text-[12px] text-text-3/50">Loading transactions...</p>
880
+ ) : transactions.length === 0 ? (
542
881
  <p className="text-[12px] text-text-3/50">No transactions yet.</p>
882
+ ) : filteredTransactions.length === 0 ? (
883
+ <p className="text-[12px] text-text-3/50">No matching transactions.</p>
543
884
  ) : (
544
- <div className="space-y-2">
545
- {transactions.map((tx, idx) => (
885
+ <div className="max-h-[420px] overflow-y-auto pr-1 space-y-2">
886
+ {filteredTransactions.map((tx, idx) => (
546
887
  <div key={tx.id} className="flex items-center gap-3 p-3 rounded-[10px] border border-white/[0.06] bg-surface-2/30 transition-all hover:bg-surface-2/50"
547
888
  style={{ animation: 'fade-up 0.4s var(--ease-spring) both', animationDelay: `${0.35 + idx * 0.03}s` }}>
548
889
  <div className={`w-6 h-6 rounded-full flex items-center justify-center text-[12px] ${
@@ -555,22 +896,26 @@ export function WalletPanel() {
555
896
  <div className="flex-1 min-w-0">
556
897
  <div className="flex items-center gap-2">
557
898
  <span className="text-[12px] font-600 text-text-1">
558
- {tx.type === 'send' ? '-' : '+'}{(tx.amountLamports / 1e9).toFixed(4)} SOL
899
+ {tx.type === 'send' ? '-' : '+'}{formatWalletAmount(tx.chain, getWalletAtomicAmount(tx), { minFractionDigits: 4, maxFractionDigits: 6 })} {getWalletAssetSymbol(tx.chain)}
559
900
  </span>
560
901
  <span className={`px-1.5 py-0.5 rounded-[4px] text-[9px] font-600 uppercase ${
561
- tx.status === 'confirmed' ? 'bg-green-500/15 text-green-400' :
562
- tx.status === 'pending_approval' ? 'bg-amber-500/15 text-amber-400 animate-pulse' :
563
- tx.status === 'failed' ? 'bg-red-500/15 text-red-400' :
564
- tx.status === 'denied' ? 'bg-red-500/15 text-red-400' :
902
+ getWalletTransactionStatusGroup(tx.status) === 'confirmed' ? 'bg-green-500/15 text-green-400' :
903
+ getWalletTransactionStatusGroup(tx.status) === 'pending' ? 'bg-amber-500/15 text-amber-400 animate-pulse' :
565
904
  'bg-blue-500/15 text-blue-400'
566
905
  }`}>
567
906
  {tx.status.replace('_', ' ')}
568
907
  </span>
908
+ <span className="px-1.5 py-0.5 rounded-[4px] bg-white/[0.05] text-[9px] font-600 uppercase text-text-3/70">
909
+ {tx.type}
910
+ </span>
569
911
  </div>
570
912
  <div className="text-[10px] text-text-3/50 font-mono truncate mt-0.5">
571
913
  {tx.type === 'send' ? `To: ${tx.toAddress.slice(0, 8)}...${tx.toAddress.slice(-4)}` : `From: ${tx.fromAddress.slice(0, 8)}...${tx.fromAddress.slice(-4)}`}
572
914
  </div>
573
915
  {tx.memo && <div className="text-[10px] text-text-3/60 mt-0.5 truncate">{tx.memo}</div>}
916
+ <div className="text-[10px] text-text-3/40 font-mono truncate mt-0.5">
917
+ {tx.signature.slice(0, 10)}...{tx.signature.slice(-6)}
918
+ </div>
574
919
  </div>
575
920
  <div className="text-[10px] text-text-3/40 shrink-0">
576
921
  {new Date(tx.timestamp).toLocaleDateString()}