@swarmclawai/swarmclaw 0.6.4 → 0.6.7

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 (143) hide show
  1. package/README.md +62 -30
  2. package/package.json +10 -1
  3. package/src/app/api/agents/[id]/clone/route.ts +40 -0
  4. package/src/app/api/agents/route.ts +39 -14
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
  6. package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
  7. package/src/app/api/chatrooms/[id]/route.ts +34 -2
  8. package/src/app/api/chatrooms/route.ts +26 -3
  9. package/src/app/api/connectors/[id]/health/route.ts +64 -0
  10. package/src/app/api/connectors/route.ts +17 -2
  11. package/src/app/api/knowledge/route.ts +6 -1
  12. package/src/app/api/openclaw/doctor/route.ts +17 -0
  13. package/src/app/api/schedules/[id]/run/route.ts +3 -0
  14. package/src/app/api/sessions/[id]/chat/route.ts +5 -1
  15. package/src/app/api/sessions/route.ts +11 -2
  16. package/src/app/api/tasks/[id]/route.ts +18 -13
  17. package/src/app/api/tasks/route.ts +44 -1
  18. package/src/app/api/usage/route.ts +16 -7
  19. package/src/app/api/wallets/[id]/approve/route.ts +62 -0
  20. package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
  21. package/src/app/api/wallets/[id]/route.ts +118 -0
  22. package/src/app/api/wallets/[id]/send/route.ts +118 -0
  23. package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
  24. package/src/app/api/wallets/route.ts +74 -0
  25. package/src/app/globals.css +8 -0
  26. package/src/cli/index.js +20 -0
  27. package/src/cli/index.ts +223 -39
  28. package/src/cli/spec.js +14 -0
  29. package/src/components/agents/agent-avatar.tsx +15 -1
  30. package/src/components/agents/agent-card.tsx +38 -6
  31. package/src/components/agents/agent-chat-list.tsx +79 -3
  32. package/src/components/agents/agent-sheet.tsx +191 -26
  33. package/src/components/auth/setup-wizard.tsx +268 -353
  34. package/src/components/chat/chat-area.tsx +24 -9
  35. package/src/components/chat/chat-header.tsx +48 -19
  36. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  37. package/src/components/chat/delegation-banner.test.ts +27 -0
  38. package/src/components/chat/delegation-banner.tsx +109 -23
  39. package/src/components/chat/message-bubble.tsx +17 -16
  40. package/src/components/chat/message-list.tsx +6 -5
  41. package/src/components/chat/streaming-bubble.tsx +3 -2
  42. package/src/components/chat/thinking-indicator.tsx +3 -2
  43. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  44. package/src/components/chatrooms/agent-hover-card.tsx +1 -1
  45. package/src/components/chatrooms/chatroom-input.tsx +1 -1
  46. package/src/components/chatrooms/chatroom-message.tsx +165 -23
  47. package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
  48. package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
  49. package/src/components/chatrooms/chatroom-view.tsx +62 -17
  50. package/src/components/connectors/connector-health.tsx +120 -0
  51. package/src/components/connectors/connector-list.tsx +1 -1
  52. package/src/components/connectors/connector-sheet.tsx +9 -0
  53. package/src/components/home/home-view.tsx +25 -3
  54. package/src/components/input/chat-input.tsx +8 -1
  55. package/src/components/knowledge/knowledge-list.tsx +1 -1
  56. package/src/components/knowledge/knowledge-sheet.tsx +1 -1
  57. package/src/components/layout/app-layout.tsx +35 -4
  58. package/src/components/memory/memory-agent-list.tsx +1 -1
  59. package/src/components/memory/memory-browser.tsx +1 -0
  60. package/src/components/memory/memory-card.tsx +3 -2
  61. package/src/components/memory/memory-detail.tsx +3 -3
  62. package/src/components/memory/memory-sheet.tsx +2 -2
  63. package/src/components/projects/project-detail.tsx +4 -4
  64. package/src/components/schedules/schedule-list.tsx +55 -9
  65. package/src/components/schedules/schedule-sheet.tsx +134 -23
  66. package/src/components/secrets/secret-sheet.tsx +1 -1
  67. package/src/components/secrets/secrets-list.tsx +1 -1
  68. package/src/components/sessions/session-card.tsx +1 -1
  69. package/src/components/shared/agent-picker-list.tsx +1 -1
  70. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  71. package/src/components/shared/command-palette.tsx +237 -0
  72. package/src/components/shared/connector-platform-icon.tsx +1 -0
  73. package/src/components/shared/settings/section-user-preferences.tsx +4 -4
  74. package/src/components/skills/skill-list.tsx +1 -1
  75. package/src/components/skills/skill-sheet.tsx +1 -1
  76. package/src/components/tasks/task-board.tsx +3 -3
  77. package/src/components/tasks/task-card.tsx +22 -2
  78. package/src/components/tasks/task-sheet.tsx +112 -17
  79. package/src/components/usage/metrics-dashboard.tsx +13 -25
  80. package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
  81. package/src/components/wallets/wallet-panel.tsx +616 -0
  82. package/src/components/wallets/wallet-section.tsx +100 -0
  83. package/src/hooks/use-swipe.ts +49 -0
  84. package/src/lib/providers/anthropic.ts +16 -2
  85. package/src/lib/providers/claude-cli.ts +7 -1
  86. package/src/lib/providers/index.ts +7 -0
  87. package/src/lib/providers/ollama.ts +16 -2
  88. package/src/lib/providers/openai.ts +7 -2
  89. package/src/lib/providers/openclaw.ts +6 -1
  90. package/src/lib/providers/provider-defaults.ts +7 -0
  91. package/src/lib/schedule-templates.ts +115 -0
  92. package/src/lib/server/agent-registry.ts +2 -2
  93. package/src/lib/server/alert-dispatch.ts +64 -0
  94. package/src/lib/server/chat-execution.ts +76 -4
  95. package/src/lib/server/chatroom-health.ts +60 -0
  96. package/src/lib/server/chatroom-helpers.test.ts +94 -0
  97. package/src/lib/server/chatroom-helpers.ts +86 -12
  98. package/src/lib/server/chatroom-routing.ts +65 -0
  99. package/src/lib/server/connectors/discord.ts +3 -0
  100. package/src/lib/server/connectors/email.ts +267 -0
  101. package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
  102. package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
  103. package/src/lib/server/connectors/manager.ts +239 -5
  104. package/src/lib/server/connectors/openclaw.ts +3 -0
  105. package/src/lib/server/connectors/slack.ts +6 -0
  106. package/src/lib/server/connectors/telegram.ts +18 -0
  107. package/src/lib/server/connectors/types.ts +2 -0
  108. package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
  109. package/src/lib/server/connectors/whatsapp-text.ts +26 -0
  110. package/src/lib/server/connectors/whatsapp.ts +17 -5
  111. package/src/lib/server/cost.ts +70 -0
  112. package/src/lib/server/create-notification.ts +2 -0
  113. package/src/lib/server/daemon-state.ts +124 -0
  114. package/src/lib/server/dag-validation.ts +115 -0
  115. package/src/lib/server/memory-db.ts +12 -7
  116. package/src/lib/server/openclaw-doctor.ts +48 -0
  117. package/src/lib/server/orchestrator-lg.ts +12 -2
  118. package/src/lib/server/orchestrator.ts +6 -1
  119. package/src/lib/server/queue-followups.test.ts +224 -0
  120. package/src/lib/server/queue.ts +238 -24
  121. package/src/lib/server/scheduler.ts +3 -0
  122. package/src/lib/server/session-run-manager.ts +22 -1
  123. package/src/lib/server/session-tools/chatroom.ts +11 -2
  124. package/src/lib/server/session-tools/context-mgmt.ts +2 -2
  125. package/src/lib/server/session-tools/index.ts +8 -2
  126. package/src/lib/server/session-tools/memory.ts +23 -4
  127. package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
  128. package/src/lib/server/session-tools/shell.ts +1 -1
  129. package/src/lib/server/session-tools/wallet.ts +124 -0
  130. package/src/lib/server/session-tools/web.ts +2 -2
  131. package/src/lib/server/solana.ts +122 -0
  132. package/src/lib/server/storage.ts +158 -6
  133. package/src/lib/server/stream-agent-chat.ts +126 -63
  134. package/src/lib/server/task-mention.test.ts +41 -0
  135. package/src/lib/server/task-mention.ts +3 -2
  136. package/src/lib/setup-defaults.ts +277 -0
  137. package/src/lib/tool-definitions.ts +1 -0
  138. package/src/lib/validation/schemas.ts +69 -0
  139. package/src/lib/view-routes.ts +1 -0
  140. package/src/stores/use-app-store.ts +15 -3
  141. package/src/stores/use-chatroom-store.ts +52 -2
  142. package/src/types/index.ts +98 -2
  143. package/tsconfig.json +2 -1
@@ -0,0 +1,616 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useCallback, useMemo } from 'react'
4
+ import { api } from '@/lib/api-client'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+ import { useWs } from '@/hooks/use-ws'
7
+ import { WalletApprovalDialog } from './wallet-approval-dialog'
8
+ import { AgentPickerList } from '@/components/shared/agent-picker-list'
9
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
10
+ import type { AgentWallet, WalletTransaction, WalletBalanceSnapshot, Agent } from '@/types'
11
+
12
+ type SafeWallet = Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }
13
+
14
+ function SolanaIcon({ size = 12, className = '' }: { size?: number; className?: string }) {
15
+ return (
16
+ <svg width={size} height={size} viewBox="0 0 128 128" className={className}>
17
+ <defs>
18
+ <linearGradient id="sol-grad" x1="0%" y1="0%" x2="100%" y2="100%">
19
+ <stop offset="0%" stopColor="#00FFA3" />
20
+ <stop offset="100%" stopColor="#DC1FFF" />
21
+ </linearGradient>
22
+ </defs>
23
+ <path d="M25.5 100.5a4.3 4.3 0 0 1 3-1.3h93.2a2.2 2.2 0 0 1 1.5 3.7l-17.7 17.8a4.3 4.3 0 0 1-3 1.3H9.3a2.2 2.2 0 0 1-1.5-3.7l17.7-17.8z" fill="url(#sol-grad)" />
24
+ <path d="M25.5 7.3a4.4 4.4 0 0 1 3-1.3h93.2a2.2 2.2 0 0 1 1.5 3.7L105.5 27.5a4.3 4.3 0 0 1-3 1.3H9.3a2.2 2.2 0 0 1-1.5-3.7L25.5 7.3z" fill="url(#sol-grad)" />
25
+ <path d="M105.5 53.7a4.3 4.3 0 0 0-3-1.3H9.3a2.2 2.2 0 0 0-1.5 3.7l17.7 17.8a4.3 4.3 0 0 0 3 1.3h93.2a2.2 2.2 0 0 0 1.5-3.7L105.5 53.7z" fill="url(#sol-grad)" />
26
+ </svg>
27
+ )
28
+ }
29
+
30
+ export function WalletPanel() {
31
+ const agents = useAppStore((s) => s.agents)
32
+ const walletPanelAgentId = useAppStore((s) => s.walletPanelAgentId)
33
+ const setWalletPanelAgentId = useAppStore((s) => s.setWalletPanelAgentId)
34
+ const setActiveView = useAppStore((s) => s.setActiveView)
35
+ const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
36
+
37
+ const [wallets, setWallets] = useState<Record<string, SafeWallet>>({})
38
+ const [selectedWalletId, setSelectedWalletId] = useState<string | null>(null)
39
+ const [transactions, setTransactions] = useState<WalletTransaction[]>([])
40
+ const [balanceHistory, setBalanceHistory] = useState<WalletBalanceSnapshot[]>([])
41
+ const [loading, setLoading] = useState(true)
42
+ const [pendingApproval, setPendingApproval] = useState<WalletTransaction | null>(null)
43
+
44
+ // Settings edit state
45
+ const [editingLimits, setEditingLimits] = useState(false)
46
+ const [perTxLimit, setPerTxLimit] = useState('')
47
+ const [dailyLimit, setDailyLimit] = useState('')
48
+ const [requireApproval, setRequireApproval] = useState(true)
49
+ const [saving, setSaving] = useState(false)
50
+ const [deleting, setDeleting] = useState(false)
51
+ const [confirmDelete, setConfirmDelete] = useState(false)
52
+ const [reassigning, setReassigning] = useState(false)
53
+ const [reassignSaving, setReassignSaving] = useState(false)
54
+ const [reassignError, setReassignError] = useState('')
55
+
56
+ // Create wallet state
57
+ const [showCreateForm, setShowCreateForm] = useState(false)
58
+ const [createAgentId, setCreateAgentId] = useState('')
59
+ const [creating, setCreating] = useState(false)
60
+ const [createError, setCreateError] = useState('')
61
+
62
+ const loadWallets = useCallback(async () => {
63
+ try {
64
+ const data = await api<Record<string, SafeWallet>>('GET', '/wallets')
65
+ setWallets(data)
66
+
67
+ // Auto-select wallet for target agent
68
+ if (walletPanelAgentId) {
69
+ const match = Object.values(data).find((w) => w.agentId === walletPanelAgentId)
70
+ if (match) setSelectedWalletId(match.id)
71
+ } else if (!selectedWalletId && Object.keys(data).length > 0) {
72
+ setSelectedWalletId(Object.keys(data)[0])
73
+ }
74
+ } catch { /* ignore */ }
75
+ setLoading(false)
76
+ // eslint-disable-next-line react-hooks/exhaustive-deps
77
+ }, [walletPanelAgentId])
78
+
79
+ useEffect(() => { loadWallets() }, [loadWallets])
80
+ useWs('wallets', loadWallets, 15000)
81
+
82
+ // Load detail when wallet selected
83
+ const selectedWallet = selectedWalletId ? wallets[selectedWalletId] : null
84
+
85
+ const loadDetail = useCallback(async () => {
86
+ if (!selectedWalletId) return
87
+ try {
88
+ const [detail, txs, history] = await Promise.all([
89
+ api<SafeWallet>('GET', `/wallets/${selectedWalletId}`),
90
+ api<WalletTransaction[]>('GET', `/wallets/${selectedWalletId}/transactions`),
91
+ api<WalletBalanceSnapshot[]>('GET', `/wallets/${selectedWalletId}/balance-history`),
92
+ ])
93
+ setWallets((prev) => ({ ...prev, [selectedWalletId]: detail }))
94
+ setTransactions(txs)
95
+ setBalanceHistory(history)
96
+
97
+ // Check for pending approvals
98
+ const pending = txs.find((tx) => tx.status === 'pending_approval')
99
+ if (pending) setPendingApproval(pending)
100
+ } catch { /* ignore */ }
101
+ }, [selectedWalletId])
102
+
103
+ useEffect(() => { loadDetail() }, [loadDetail])
104
+
105
+ // Initialize limits when wallet selected
106
+ useEffect(() => {
107
+ if (selectedWallet) {
108
+ setPerTxLimit(String((selectedWallet.spendingLimitLamports ?? 100_000_000) / 1e9))
109
+ setDailyLimit(String((selectedWallet.dailyLimitLamports ?? 1_000_000_000) / 1e9))
110
+ setRequireApproval(selectedWallet.requireApproval)
111
+ }
112
+ }, [selectedWallet])
113
+
114
+ const saveLimits = useCallback(async () => {
115
+ if (!selectedWalletId) return
116
+ setSaving(true)
117
+ try {
118
+ await api('PATCH', `/wallets/${selectedWalletId}`, {
119
+ spendingLimitLamports: Math.round(parseFloat(perTxLimit || '0.1') * 1e9),
120
+ dailyLimitLamports: Math.round(parseFloat(dailyLimit || '1') * 1e9),
121
+ requireApproval,
122
+ })
123
+ setEditingLimits(false)
124
+ loadDetail()
125
+ } catch { /* ignore */ }
126
+ setSaving(false)
127
+ }, [selectedWalletId, perTxLimit, dailyLimit, requireApproval, loadDetail])
128
+
129
+ const handleDelete = useCallback(async () => {
130
+ if (!selectedWalletId) return
131
+ setDeleting(true)
132
+ try {
133
+ await api('DELETE', `/wallets/${selectedWalletId}`)
134
+ setSelectedWalletId(null)
135
+ setConfirmDelete(false)
136
+ loadWallets()
137
+ } catch { /* ignore */ }
138
+ setDeleting(false)
139
+ }, [selectedWalletId, loadWallets])
140
+
141
+ const [copied, setCopied] = useState(false)
142
+ const copyAddress = useCallback(() => {
143
+ if (!selectedWallet) return
144
+ navigator.clipboard.writeText(selectedWallet.publicKey)
145
+ setCopied(true)
146
+ setTimeout(() => setCopied(false), 2000)
147
+ }, [selectedWallet])
148
+
149
+ const agentsWithoutWallets = useMemo(() => {
150
+ const walletAgentIds = new Set(Object.values(wallets).map((w) => w.agentId))
151
+ return Object.values(agents).filter((a) => !walletAgentIds.has(a.id)) as Agent[]
152
+ }, [agents, wallets])
153
+
154
+ const createWallet = useCallback(async () => {
155
+ if (!createAgentId) return
156
+ setCreating(true)
157
+ setCreateError('')
158
+ try {
159
+ await api('POST', '/wallets', { agentId: createAgentId })
160
+ setShowCreateForm(false)
161
+ setCreateAgentId('')
162
+ loadWallets()
163
+ } catch (err: unknown) {
164
+ setCreateError(err instanceof Error ? err.message : String(err))
165
+ }
166
+ setCreating(false)
167
+ }, [createAgentId, loadWallets])
168
+
169
+ if (loading) {
170
+ return (
171
+ <div className="flex-1 flex items-center justify-center">
172
+ <div className="animate-spin w-5 h-5 border-2 border-accent border-t-transparent rounded-full" />
173
+ </div>
174
+ )
175
+ }
176
+
177
+ const walletList = Object.values(wallets)
178
+
179
+ if (walletList.length === 0) {
180
+ return (
181
+ <div className="flex-1 flex items-center justify-center">
182
+ <div className="text-center max-w-sm">
183
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="mx-auto mb-4 text-text-3/30">
184
+ <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" />
185
+ </svg>
186
+ <h3 className="font-display text-[14px] font-600 text-text-2 mb-2">No wallets yet</h3>
187
+ {agentsWithoutWallets.length > 0 ? (
188
+ <div className="mt-4 space-y-3">
189
+ <AgentPickerList
190
+ agents={agentsWithoutWallets}
191
+ selected={createAgentId}
192
+ onSelect={(id) => setCreateAgentId(id === createAgentId ? '' : id)}
193
+ maxHeight={180}
194
+ />
195
+ <button
196
+ type="button"
197
+ onClick={createWallet}
198
+ disabled={!createAgentId || creating}
199
+ className="w-full px-3 py-2 rounded-[8px] bg-accent text-white text-[12px] font-600 hover:brightness-110 cursor-pointer disabled:opacity-50 transition-colors"
200
+ style={{ fontFamily: 'inherit' }}
201
+ >
202
+ {creating ? 'Creating...' : 'Create Wallet'}
203
+ </button>
204
+ {createError && <p className="text-[11px] text-red-400">{createError}</p>}
205
+ </div>
206
+ ) : (
207
+ <p className="text-[12px] text-text-3/60">
208
+ All agents already have wallets.
209
+ </p>
210
+ )}
211
+ </div>
212
+ </div>
213
+ )
214
+ }
215
+
216
+ return (
217
+ <div className="flex-1 flex h-full min-w-0">
218
+ {/* Sidebar — wallet list */}
219
+ <div className="w-[240px] shrink-0 border-r border-white/[0.06] flex flex-col">
220
+ <div className="flex items-center px-4 pt-4 pb-2 shrink-0">
221
+ <h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em] flex-1">Wallets</h2>
222
+ <button
223
+ type="button"
224
+ onClick={() => { setShowCreateForm(!showCreateForm); setCreateAgentId(''); setCreateError('') }}
225
+ disabled={agentsWithoutWallets.length === 0}
226
+ 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"
227
+ title={agentsWithoutWallets.length === 0 ? 'All agents have wallets' : 'Create wallet'}
228
+ >
229
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
230
+ <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
231
+ </svg>
232
+ </button>
233
+ </div>
234
+ <div className="flex-1 overflow-y-auto px-2 pb-4">
235
+ {showCreateForm && (
236
+ <div className="mx-1 mb-2 p-2.5 rounded-[8px] border border-accent/20 bg-accent-soft/10 space-y-2">
237
+ <AgentPickerList
238
+ agents={agentsWithoutWallets}
239
+ selected={createAgentId}
240
+ onSelect={(id) => setCreateAgentId(id === createAgentId ? '' : id)}
241
+ maxHeight={160}
242
+ />
243
+ <div className="flex gap-1.5">
244
+ <button
245
+ type="button"
246
+ onClick={createWallet}
247
+ disabled={!createAgentId || creating}
248
+ className="flex-1 px-2 py-1.5 rounded-[6px] bg-accent text-white text-[10px] font-600 hover:brightness-110 cursor-pointer disabled:opacity-50 transition-colors"
249
+ style={{ fontFamily: 'inherit' }}
250
+ >
251
+ {creating ? 'Creating...' : 'Create'}
252
+ </button>
253
+ <button
254
+ type="button"
255
+ onClick={() => { setShowCreateForm(false); setCreateError('') }}
256
+ className="px-2 py-1.5 rounded-[6px] border border-white/[0.08] text-text-3 text-[10px] hover:text-text-2 cursor-pointer transition-colors"
257
+ style={{ fontFamily: 'inherit' }}
258
+ >
259
+ Cancel
260
+ </button>
261
+ </div>
262
+ {createError && <p className="text-[10px] text-red-400">{createError}</p>}
263
+ </div>
264
+ )}
265
+ {walletList.map((w) => {
266
+ const a = agents[w.agentId] as Agent | undefined
267
+ return (
268
+ <button
269
+ key={w.id}
270
+ onClick={() => { setSelectedWalletId(w.id); setWalletPanelAgentId(w.agentId) }}
271
+ className={`w-full text-left px-3 py-2.5 rounded-[8px] mb-1 transition-colors cursor-pointer flex items-center gap-2.5 ${
272
+ selectedWalletId === w.id ? 'bg-accent-soft/30 text-text-1' : 'text-text-3 hover:bg-white/[0.04]'
273
+ }`}
274
+ >
275
+ <AgentAvatar seed={a?.avatarSeed || null} avatarUrl={a?.avatarUrl} name={a?.name || '?'} size={28} />
276
+ <div className="flex-1 min-w-0">
277
+ <div className="text-[12px] font-600 truncate">{a?.name || w.agentId}</div>
278
+ <div className="text-[10px] text-text-3/50 font-mono truncate mt-0.5 flex items-center gap-1">
279
+ {w.chain === 'solana' && <SolanaIcon size={9} className="shrink-0 opacity-50" />}
280
+ <span className="truncate">{w.publicKey.slice(0, 8)}...{w.publicKey.slice(-4)}</span>
281
+ {typeof w.balanceSol === 'number' && (
282
+ <span className="text-text-3/40">{w.balanceSol.toFixed(3)} SOL</span>
283
+ )}
284
+ </div>
285
+ </div>
286
+ </button>
287
+ )
288
+ })}
289
+ </div>
290
+ </div>
291
+
292
+ {/* Main detail area */}
293
+ {selectedWallet ? (
294
+ <div className="flex-1 overflow-y-auto p-6 space-y-6">
295
+ {/* Warning banner */}
296
+ <div className="flex items-start gap-3 p-3 rounded-[10px] bg-amber-500/10 border border-amber-500/20">
297
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-amber-400 shrink-0 mt-0.5">
298
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
299
+ <line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
300
+ </svg>
301
+ <p className="text-[11px] text-amber-300/80 leading-relaxed">
302
+ Agent Wallets is experimental. Crypto transactions are irreversible. Do not store more than you can afford to lose.
303
+ </p>
304
+ </div>
305
+
306
+ {/* Agent & Address */}
307
+ <div>
308
+ <div className="flex items-center gap-2 mb-2">
309
+ {(() => {
310
+ const a = agents[selectedWallet.agentId] as Agent | undefined
311
+ return a ? (
312
+ <button
313
+ type="button"
314
+ onClick={() => { setEditingAgentId(a.id); setActiveView('agents') }}
315
+ className="flex items-center gap-2 bg-transparent border-none p-0 cursor-pointer group"
316
+ style={{ fontFamily: 'inherit' }}
317
+ title="Open agent settings"
318
+ >
319
+ <AgentAvatar seed={a.avatarSeed || null} avatarUrl={a.avatarUrl} name={a.name} size={24} />
320
+ <span className="text-[13px] font-600 text-text-2 group-hover:text-accent-bright transition-colors">{a.name}</span>
321
+ </button>
322
+ ) : (
323
+ <span className="text-[13px] font-600 text-text-2">{selectedWallet.agentId}</span>
324
+ )
325
+ })()}
326
+ <span className="inline-flex items-center gap-1 text-[11px] text-text-3/40 uppercase tracking-wide font-600">
327
+ {selectedWallet.chain === 'solana' && <SolanaIcon size={11} />}
328
+ {selectedWallet.chain}
329
+ </span>
330
+ <button
331
+ type="button"
332
+ onClick={() => { setReassigning(!reassigning); setReassignError('') }}
333
+ className="text-[10px] text-text-3/40 hover:text-accent-bright transition-colors cursor-pointer bg-transparent border-none px-1.5 py-0.5 rounded-[5px] hover:bg-white/[0.04]"
334
+ style={{ fontFamily: 'inherit' }}
335
+ >
336
+ {reassigning ? 'Cancel' : 'Reassign'}
337
+ </button>
338
+ </div>
339
+ {reassigning && (
340
+ <div className="mb-2 space-y-2">
341
+ <p className="text-[11px] text-text-3/60">Select a new agent to control this wallet:</p>
342
+ <AgentPickerList
343
+ agents={agentsWithoutWallets}
344
+ selected=""
345
+ onSelect={async (agentId) => {
346
+ setReassignSaving(true)
347
+ setReassignError('')
348
+ try {
349
+ await api('PATCH', `/wallets/${selectedWallet.id}`, { agentId })
350
+ setReassigning(false)
351
+ loadWallets()
352
+ } catch (err: unknown) {
353
+ setReassignError(err instanceof Error ? err.message : String(err))
354
+ }
355
+ setReassignSaving(false)
356
+ }}
357
+ maxHeight={160}
358
+ />
359
+ {reassignSaving && <p className="text-[10px] text-text-3/50">Reassigning...</p>}
360
+ {reassignError && <p className="text-[10px] text-red-400">{reassignError}</p>}
361
+ </div>
362
+ )}
363
+ <div className="flex items-center gap-2">
364
+ <code className="text-[13px] text-text-2 font-mono bg-black/20 px-3 py-2 rounded-[8px] flex-1 truncate">
365
+ {selectedWallet.publicKey}
366
+ </code>
367
+ <button
368
+ type="button"
369
+ onClick={copyAddress}
370
+ className="shrink-0 px-3 py-2 rounded-[8px] text-[11px] text-text-3 hover:text-text-2 border border-white/[0.08] bg-surface transition-colors cursor-pointer"
371
+ style={{ fontFamily: 'inherit' }}
372
+ >
373
+ {copied ? 'Copied!' : 'Copy'}
374
+ </button>
375
+ </div>
376
+ </div>
377
+
378
+ {/* Balance card */}
379
+ <div className="p-5 rounded-[14px] border border-white/[0.06] bg-surface-2/50">
380
+ <div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-2">Balance</div>
381
+ <div className="text-[28px] font-600 text-text-1 tracking-tight">
382
+ {(selectedWallet.balanceSol ?? 0).toFixed(4)} <span className="text-[14px] text-text-3/60">SOL</span>
383
+ </div>
384
+ </div>
385
+
386
+ {/* Funding help */}
387
+ <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50">
388
+ <div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-2">How to Fund This Wallet</div>
389
+ <div className="space-y-2 text-[12px] text-text-3/70 leading-relaxed">
390
+ <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>
391
+ <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>
392
+ <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>
393
+ </div>
394
+ </div>
395
+
396
+ {/* Balance history chart (simple) */}
397
+ {balanceHistory.length > 1 && (
398
+ <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50">
399
+ <div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-3">Balance Over Time</div>
400
+ <div className="h-[120px] flex items-end gap-[2px]">
401
+ {(() => {
402
+ const max = Math.max(...balanceHistory.map((s) => s.balanceLamports), 1)
403
+ return balanceHistory.slice(-60).map((s, i) => (
404
+ <div
405
+ key={s.id || i}
406
+ className="flex-1 bg-accent/40 rounded-t-[2px] min-w-[3px]"
407
+ style={{ height: `${Math.max(2, (s.balanceLamports / max) * 100)}%` }}
408
+ title={`${(s.balanceLamports / 1e9).toFixed(4)} SOL — ${new Date(s.timestamp).toLocaleString()}`}
409
+ />
410
+ ))
411
+ })()}
412
+ </div>
413
+ </div>
414
+ )}
415
+
416
+ {/* Spending config */}
417
+ <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface-2/50">
418
+ <div className="flex items-center justify-between mb-3">
419
+ <div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600">Spending Limits</div>
420
+ {!editingLimits && (
421
+ <button
422
+ type="button"
423
+ onClick={() => setEditingLimits(true)}
424
+ className="text-[11px] text-accent-bright hover:underline cursor-pointer"
425
+ style={{ fontFamily: 'inherit' }}
426
+ >
427
+ Edit
428
+ </button>
429
+ )}
430
+ </div>
431
+
432
+ {editingLimits ? (
433
+ <div className="space-y-3">
434
+ <div>
435
+ <label className="block text-[11px] text-text-3/70 mb-1">Per-transaction limit (SOL)</label>
436
+ <input
437
+ type="number"
438
+ step="0.01"
439
+ value={perTxLimit}
440
+ onChange={(e) => setPerTxLimit(e.target.value)}
441
+ 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"
442
+ style={{ fontFamily: 'inherit' }}
443
+ />
444
+ </div>
445
+ <div>
446
+ <label className="block text-[11px] text-text-3/70 mb-1">Daily limit (SOL)</label>
447
+ <input
448
+ type="number"
449
+ step="0.1"
450
+ value={dailyLimit}
451
+ onChange={(e) => setDailyLimit(e.target.value)}
452
+ 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"
453
+ style={{ fontFamily: 'inherit' }}
454
+ />
455
+ </div>
456
+ <div className="flex items-center gap-2">
457
+ <button
458
+ type="button"
459
+ onClick={() => setRequireApproval(!requireApproval)}
460
+ className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${requireApproval ? 'bg-accent' : 'bg-white/[0.12]'}`}
461
+ >
462
+ <span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${requireApproval ? 'translate-x-[18px]' : ''}`} />
463
+ </button>
464
+ <span className="text-[11px] text-text-3">Require approval for sends</span>
465
+ </div>
466
+ <div className="flex gap-2 pt-1">
467
+ <button
468
+ type="button"
469
+ onClick={saveLimits}
470
+ disabled={saving}
471
+ className="px-3 py-1.5 rounded-[8px] bg-accent text-white text-[11px] font-600 hover:brightness-110 cursor-pointer disabled:opacity-50"
472
+ style={{ fontFamily: 'inherit' }}
473
+ >
474
+ {saving ? 'Saving...' : 'Save'}
475
+ </button>
476
+ <button
477
+ type="button"
478
+ onClick={() => setEditingLimits(false)}
479
+ className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] text-text-3 text-[11px] hover:text-text-2 cursor-pointer"
480
+ style={{ fontFamily: 'inherit' }}
481
+ >
482
+ Cancel
483
+ </button>
484
+ </div>
485
+ </div>
486
+ ) : (
487
+ <div className="space-y-2 text-[12px]">
488
+ <div className="flex justify-between">
489
+ <span className="text-text-3/70">Per-transaction</span>
490
+ <span className="text-text-2">{((selectedWallet.spendingLimitLamports ?? 100_000_000) / 1e9).toFixed(2)} SOL</span>
491
+ </div>
492
+ <div className="flex justify-between">
493
+ <span className="text-text-3/70">Daily rolling</span>
494
+ <span className="text-text-2">{((selectedWallet.dailyLimitLamports ?? 1_000_000_000) / 1e9).toFixed(1)} SOL</span>
495
+ </div>
496
+ <div className="flex justify-between">
497
+ <span className="text-text-3/70">Approval</span>
498
+ <span className="text-text-2">{selectedWallet.requireApproval ? 'Required' : 'Auto-send'}</span>
499
+ </div>
500
+ </div>
501
+ )}
502
+ </div>
503
+
504
+ {/* Transaction history */}
505
+ <div>
506
+ <div className="text-[11px] text-text-3/60 uppercase tracking-wide font-600 mb-3">Transactions</div>
507
+ {transactions.length === 0 ? (
508
+ <p className="text-[12px] text-text-3/50">No transactions yet.</p>
509
+ ) : (
510
+ <div className="space-y-2">
511
+ {transactions.map((tx) => (
512
+ <div key={tx.id} className="flex items-center gap-3 p-3 rounded-[10px] border border-white/[0.06] bg-surface-2/30">
513
+ <div className={`w-6 h-6 rounded-full flex items-center justify-center text-[12px] ${
514
+ tx.type === 'send' ? 'bg-red-500/15 text-red-400' :
515
+ tx.type === 'receive' ? 'bg-green-500/15 text-green-400' :
516
+ 'bg-blue-500/15 text-blue-400'
517
+ }`}>
518
+ {tx.type === 'send' ? '\u2191' : tx.type === 'receive' ? '\u2193' : '\u21C4'}
519
+ </div>
520
+ <div className="flex-1 min-w-0">
521
+ <div className="flex items-center gap-2">
522
+ <span className="text-[12px] font-600 text-text-1">
523
+ {tx.type === 'send' ? '-' : '+'}{(tx.amountLamports / 1e9).toFixed(4)} SOL
524
+ </span>
525
+ <span className={`px-1.5 py-0.5 rounded-[4px] text-[9px] font-600 uppercase ${
526
+ tx.status === 'confirmed' ? 'bg-green-500/15 text-green-400' :
527
+ tx.status === 'pending_approval' ? 'bg-amber-500/15 text-amber-400' :
528
+ tx.status === 'failed' ? 'bg-red-500/15 text-red-400' :
529
+ tx.status === 'denied' ? 'bg-red-500/15 text-red-400' :
530
+ 'bg-blue-500/15 text-blue-400'
531
+ }`}>
532
+ {tx.status.replace('_', ' ')}
533
+ </span>
534
+ </div>
535
+ <div className="text-[10px] text-text-3/50 font-mono truncate mt-0.5">
536
+ {tx.type === 'send' ? `To: ${tx.toAddress.slice(0, 8)}...${tx.toAddress.slice(-4)}` : `From: ${tx.fromAddress.slice(0, 8)}...${tx.fromAddress.slice(-4)}`}
537
+ </div>
538
+ {tx.memo && <div className="text-[10px] text-text-3/60 mt-0.5 truncate">{tx.memo}</div>}
539
+ </div>
540
+ <div className="text-[10px] text-text-3/40 shrink-0">
541
+ {new Date(tx.timestamp).toLocaleDateString()}
542
+ </div>
543
+ {tx.status === 'pending_approval' && (
544
+ <button
545
+ type="button"
546
+ onClick={() => setPendingApproval(tx)}
547
+ className="shrink-0 px-2 py-1 rounded-[6px] bg-amber-500/15 text-amber-400 text-[10px] font-600 hover:bg-amber-500/25 cursor-pointer transition-colors"
548
+ style={{ fontFamily: 'inherit' }}
549
+ >
550
+ Review
551
+ </button>
552
+ )}
553
+ </div>
554
+ ))}
555
+ </div>
556
+ )}
557
+ </div>
558
+
559
+ {/* Danger zone */}
560
+ <div className="p-4 rounded-[14px] border border-red-500/15 bg-red-500/5">
561
+ <div className="text-[11px] text-red-400/80 uppercase tracking-wide font-600 mb-2">Danger Zone</div>
562
+ {confirmDelete ? (
563
+ <div className="space-y-2">
564
+ <p className="text-[11px] text-text-3/70">
565
+ This will permanently delete the wallet and its private key. Any remaining balance will be inaccessible. This cannot be undone.
566
+ </p>
567
+ <div className="flex gap-2">
568
+ <button
569
+ type="button"
570
+ onClick={handleDelete}
571
+ disabled={deleting}
572
+ className="px-3 py-1.5 rounded-[8px] bg-red-500 text-white text-[11px] font-600 hover:brightness-110 cursor-pointer disabled:opacity-50"
573
+ style={{ fontFamily: 'inherit' }}
574
+ >
575
+ {deleting ? 'Deleting...' : 'Confirm Delete'}
576
+ </button>
577
+ <button
578
+ type="button"
579
+ onClick={() => setConfirmDelete(false)}
580
+ className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] text-text-3 text-[11px] hover:text-text-2 cursor-pointer"
581
+ style={{ fontFamily: 'inherit' }}
582
+ >
583
+ Cancel
584
+ </button>
585
+ </div>
586
+ </div>
587
+ ) : (
588
+ <button
589
+ type="button"
590
+ onClick={() => setConfirmDelete(true)}
591
+ className="px-3 py-1.5 rounded-[8px] border border-red-500/30 text-red-400 text-[11px] font-600 hover:bg-red-500/10 cursor-pointer transition-colors"
592
+ style={{ fontFamily: 'inherit' }}
593
+ >
594
+ Delete Wallet
595
+ </button>
596
+ )}
597
+ </div>
598
+ </div>
599
+ ) : (
600
+ <div className="flex-1 flex items-center justify-center">
601
+ <p className="text-[12px] text-text-3/50">Select a wallet to view details</p>
602
+ </div>
603
+ )}
604
+
605
+ {/* Approval dialog */}
606
+ {pendingApproval && selectedWallet && (
607
+ <WalletApprovalDialog
608
+ transaction={pendingApproval}
609
+ walletAddress={selectedWallet.publicKey}
610
+ onClose={() => setPendingApproval(null)}
611
+ onResolved={loadDetail}
612
+ />
613
+ )}
614
+ </div>
615
+ )
616
+ }