@swarmclawai/swarmclaw 0.6.4 → 0.6.6

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 (92) hide show
  1. package/README.md +5 -3
  2. package/package.json +5 -1
  3. package/src/app/api/chatrooms/[id]/chat/route.ts +41 -2
  4. package/src/app/api/chatrooms/[id]/route.ts +15 -1
  5. package/src/app/api/chatrooms/route.ts +15 -2
  6. package/src/app/api/schedules/[id]/run/route.ts +3 -0
  7. package/src/app/api/tasks/route.ts +24 -0
  8. package/src/app/api/wallets/[id]/approve/route.ts +62 -0
  9. package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
  10. package/src/app/api/wallets/[id]/route.ts +118 -0
  11. package/src/app/api/wallets/[id]/send/route.ts +118 -0
  12. package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
  13. package/src/app/api/wallets/route.ts +74 -0
  14. package/src/app/globals.css +8 -0
  15. package/src/cli/index.js +15 -0
  16. package/src/cli/spec.js +14 -0
  17. package/src/components/agents/agent-avatar.tsx +15 -1
  18. package/src/components/agents/agent-card.tsx +1 -0
  19. package/src/components/agents/agent-chat-list.tsx +1 -1
  20. package/src/components/agents/agent-sheet.tsx +112 -26
  21. package/src/components/chat/chat-area.tsx +2 -2
  22. package/src/components/chat/chat-header.tsx +48 -19
  23. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  24. package/src/components/chat/delegation-banner.test.ts +27 -0
  25. package/src/components/chat/delegation-banner.tsx +109 -23
  26. package/src/components/chat/message-bubble.tsx +3 -2
  27. package/src/components/chat/message-list.tsx +5 -4
  28. package/src/components/chat/streaming-bubble.tsx +3 -2
  29. package/src/components/chat/thinking-indicator.tsx +3 -2
  30. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  31. package/src/components/chatrooms/agent-hover-card.tsx +1 -1
  32. package/src/components/chatrooms/chatroom-input.tsx +1 -1
  33. package/src/components/chatrooms/chatroom-message.tsx +1 -1
  34. package/src/components/chatrooms/chatroom-sheet.tsx +1 -1
  35. package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
  36. package/src/components/chatrooms/chatroom-view.tsx +1 -1
  37. package/src/components/connectors/connector-list.tsx +1 -1
  38. package/src/components/home/home-view.tsx +2 -1
  39. package/src/components/knowledge/knowledge-list.tsx +1 -1
  40. package/src/components/knowledge/knowledge-sheet.tsx +1 -1
  41. package/src/components/layout/app-layout.tsx +18 -3
  42. package/src/components/memory/memory-agent-list.tsx +1 -1
  43. package/src/components/memory/memory-browser.tsx +1 -0
  44. package/src/components/memory/memory-card.tsx +3 -2
  45. package/src/components/memory/memory-detail.tsx +3 -3
  46. package/src/components/memory/memory-sheet.tsx +2 -2
  47. package/src/components/projects/project-detail.tsx +4 -4
  48. package/src/components/secrets/secret-sheet.tsx +1 -1
  49. package/src/components/secrets/secrets-list.tsx +1 -1
  50. package/src/components/sessions/session-card.tsx +1 -1
  51. package/src/components/shared/agent-picker-list.tsx +1 -1
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/settings/section-user-preferences.tsx +4 -4
  54. package/src/components/skills/skill-list.tsx +1 -1
  55. package/src/components/skills/skill-sheet.tsx +1 -1
  56. package/src/components/tasks/task-board.tsx +3 -3
  57. package/src/components/tasks/task-sheet.tsx +21 -1
  58. package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
  59. package/src/components/wallets/wallet-panel.tsx +616 -0
  60. package/src/components/wallets/wallet-section.tsx +100 -0
  61. package/src/lib/server/agent-registry.ts +2 -2
  62. package/src/lib/server/chat-execution.ts +35 -3
  63. package/src/lib/server/chatroom-health.ts +60 -0
  64. package/src/lib/server/chatroom-helpers.test.ts +94 -0
  65. package/src/lib/server/chatroom-helpers.ts +64 -11
  66. package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
  67. package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
  68. package/src/lib/server/connectors/manager.ts +80 -2
  69. package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
  70. package/src/lib/server/connectors/whatsapp-text.ts +26 -0
  71. package/src/lib/server/connectors/whatsapp.ts +8 -5
  72. package/src/lib/server/orchestrator-lg.ts +12 -2
  73. package/src/lib/server/orchestrator.ts +6 -1
  74. package/src/lib/server/queue-followups.test.ts +224 -0
  75. package/src/lib/server/queue.ts +226 -24
  76. package/src/lib/server/scheduler.ts +3 -0
  77. package/src/lib/server/session-tools/chatroom.ts +11 -2
  78. package/src/lib/server/session-tools/context-mgmt.ts +2 -2
  79. package/src/lib/server/session-tools/index.ts +6 -2
  80. package/src/lib/server/session-tools/memory.ts +1 -1
  81. package/src/lib/server/session-tools/shell.ts +1 -1
  82. package/src/lib/server/session-tools/wallet.ts +124 -0
  83. package/src/lib/server/session-tools/web.ts +2 -2
  84. package/src/lib/server/solana.ts +122 -0
  85. package/src/lib/server/storage.ts +38 -0
  86. package/src/lib/server/stream-agent-chat.ts +126 -63
  87. package/src/lib/server/task-mention.test.ts +41 -0
  88. package/src/lib/server/task-mention.ts +3 -2
  89. package/src/lib/tool-definitions.ts +1 -0
  90. package/src/lib/view-routes.ts +1 -0
  91. package/src/stores/use-app-store.ts +8 -0
  92. package/src/types/index.ts +60 -1
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
3
+ import ReactMarkdown from 'react-markdown'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { AgentAvatar } from '@/components/agents/agent-avatar'
6
6
  import { api } from '@/lib/api-client'
@@ -10,6 +10,7 @@ type DelegationStatus = 'delegating' | 'checking' | 'completed' | 'failed'
10
10
  interface DelegationBannerProps {
11
11
  agentName: string
12
12
  agentAvatarSeed: string | null
13
+ agentAvatarUrl?: string | null
13
14
  taskPreview: string
14
15
  taskId: string | null
15
16
  status: DelegationStatus
@@ -60,7 +61,7 @@ function statusText(status: DelegationStatus, name: string): string {
60
61
  }
61
62
  }
62
63
 
63
- export function DelegationBanner({ agentName, agentAvatarSeed, taskPreview, taskId, status }: DelegationBannerProps) {
64
+ export function DelegationBanner({ agentName, agentAvatarSeed, agentAvatarUrl, taskPreview, taskId, status }: DelegationBannerProps) {
64
65
  const cfg = STATUS_CONFIG[status]
65
66
 
66
67
  const handleTaskClick = () => {
@@ -83,7 +84,7 @@ export function DelegationBanner({ agentName, agentAvatarSeed, taskPreview, task
83
84
  }}
84
85
  >
85
86
  <div className="shrink-0" style={{ animation: 'delegation-handoff-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.05s both' }}>
86
- <AgentAvatar seed={agentAvatarSeed} name={agentName} size={24} />
87
+ <AgentAvatar seed={agentAvatarSeed} avatarUrl={agentAvatarUrl} name={agentName} size={24} />
87
88
  </div>
88
89
  <StatusIcon status={status} color={cfg.color} />
89
90
  <div className="flex flex-col gap-0.5 min-w-0 flex-1">
@@ -120,6 +121,8 @@ export interface TaskCompletionInfo {
120
121
  /** The agent that executed the task (present on delegated results) */
121
122
  executorName: string | null
122
123
  workingDir: string | null
124
+ reportPath: string | null
125
+ outputFiles: string[]
123
126
  resumeInfo: string | null
124
127
  resultBody: string
125
128
  imageUrl?: string
@@ -140,12 +143,32 @@ export function parseTaskCompletion(text: string): TaskCompletionInfo | null {
140
143
  const sections = bodyStart === -1 ? [] : text.slice(bodyStart + 2).split('\n\n')
141
144
 
142
145
  let workingDir: string | null = null
146
+ let reportPath: string | null = null
147
+ const outputFiles: string[] = []
143
148
  let resumeInfo: string | null = null
144
149
  const resultParts: string[] = []
145
150
 
146
151
  for (const section of sections) {
147
152
  if (section.startsWith('Working directory: ')) {
148
153
  workingDir = section.replace('Working directory: ', '').replace(/^`|`$/g, '')
154
+ } else if (section.startsWith('Output files:')) {
155
+ const fromBackticks = [...section.matchAll(/`([^`\n]+)`/g)].map((m) => (m[1] || '').trim()).filter(Boolean)
156
+ if (fromBackticks.length > 0) {
157
+ for (const fileRef of fromBackticks) {
158
+ if (!outputFiles.includes(fileRef)) outputFiles.push(fileRef)
159
+ }
160
+ } else {
161
+ const fromBullets = section
162
+ .split('\n')
163
+ .slice(1)
164
+ .map((line) => line.replace(/^\s*-\s*/, '').trim())
165
+ .filter(Boolean)
166
+ for (const fileRef of fromBullets) {
167
+ if (!outputFiles.includes(fileRef)) outputFiles.push(fileRef)
168
+ }
169
+ }
170
+ } else if (section.startsWith('Task report: ')) {
171
+ reportPath = section.replace('Task report: ', '').replace(/^`|`$/g, '')
149
172
  } else if (/^(Claude session|Codex thread|OpenCode session|CLI session):/.test(section)) {
150
173
  resumeInfo = section
151
174
  } else if (section.trim()) {
@@ -153,12 +176,21 @@ export function parseTaskCompletion(text: string): TaskCompletionInfo | null {
153
176
  }
154
177
  }
155
178
 
156
- return { status, taskTitle, taskId, executorName, workingDir, resumeInfo, resultBody: resultParts.join('\n\n') }
179
+ return {
180
+ status,
181
+ taskTitle,
182
+ taskId,
183
+ executorName,
184
+ workingDir,
185
+ reportPath,
186
+ outputFiles,
187
+ resumeInfo,
188
+ resultBody: resultParts.join('\n\n'),
189
+ }
157
190
  }
158
191
 
159
192
  export function TaskCompletionCard({ info }: { info: TaskCompletionInfo }) {
160
193
  const isSuccess = info.status === 'completed'
161
- const [expanded, setExpanded] = useState(false)
162
194
 
163
195
  const handleTaskClick = () => {
164
196
  if (!info.taskId) return
@@ -170,10 +202,6 @@ export function TaskCompletionCard({ info }: { info: TaskCompletionInfo }) {
170
202
  })
171
203
  }
172
204
 
173
- // Truncate result for preview
174
- const resultPreview = info.resultBody.length > 200 ? info.resultBody.slice(0, 200) + '...' : info.resultBody
175
- const hasLongResult = info.resultBody.length > 200
176
-
177
205
  return (
178
206
  <div
179
207
  className="rounded-[14px] overflow-hidden"
@@ -278,6 +306,47 @@ export function TaskCompletionCard({ info }: { info: TaskCompletionInfo }) {
278
306
  </div>
279
307
  )}
280
308
 
309
+ {info.outputFiles.length > 0 && (
310
+ <div className="flex flex-col gap-1">
311
+ <span className="text-[11px] text-text-3/55">Output files</span>
312
+ <div className="flex flex-wrap gap-1.5">
313
+ {info.outputFiles.map((fileRef) => {
314
+ const openPath = info.workingDir && !fileRef.startsWith('/') && !fileRef.startsWith('~/')
315
+ ? `${info.workingDir.replace(/\/$/, '')}/${fileRef}`
316
+ : fileRef
317
+ return (
318
+ <button
319
+ key={fileRef}
320
+ type="button"
321
+ onClick={() => { api('POST', '/files/open', { path: openPath }).catch(() => {}) }}
322
+ className="px-2 py-1 rounded-[7px] text-[10px] font-mono bg-white/[0.03] border border-white/[0.08] text-text-3/70 hover:text-text-3 cursor-pointer max-w-full truncate"
323
+ title={`Open ${openPath}`}
324
+ >
325
+ {fileRef}
326
+ </button>
327
+ )
328
+ })}
329
+ </div>
330
+ </div>
331
+ )}
332
+
333
+ {info.reportPath && (
334
+ <div className="flex items-center gap-2">
335
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0 text-text-3/40">
336
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
337
+ <polyline points="14 2 14 8 20 8" />
338
+ </svg>
339
+ <button
340
+ type="button"
341
+ onClick={() => { api('POST', '/files/open', { path: info.reportPath }).catch(() => {}) }}
342
+ className="text-[11px] text-text-3/60 hover:text-text-3 font-mono truncate bg-transparent border-none p-0 cursor-pointer transition-colors"
343
+ title="Open task report"
344
+ >
345
+ {info.reportPath}
346
+ </button>
347
+ </div>
348
+ )}
349
+
281
350
  {/* Image artifact */}
282
351
  {info.imageUrl && (
283
352
  <div className="mt-0.5">
@@ -295,19 +364,35 @@ export function TaskCompletionCard({ info }: { info: TaskCompletionInfo }) {
295
364
  {/* Result body */}
296
365
  {info.resultBody && (
297
366
  <div className="mt-0.5">
298
- <div className="rounded-[10px] bg-white/[0.02] border border-white/[0.04] px-3 py-2.5">
299
- <pre className="text-[12px] leading-[1.6] text-text-3/80 whitespace-pre-wrap break-words m-0 font-mono">
300
- {expanded ? info.resultBody : resultPreview}
301
- </pre>
302
- {hasLongResult && (
303
- <button
304
- type="button"
305
- onClick={() => setExpanded((v) => !v)}
306
- className="mt-2 text-[11px] font-600 text-text-3/50 hover:text-text-3 bg-transparent border-none cursor-pointer p-0 transition-colors"
367
+ <div className="rounded-[10px] bg-white/[0.02] border border-white/[0.04] px-3 py-2.5 max-h-[260px] overflow-y-auto">
368
+ <div className="text-[12px] leading-[1.6] text-text-3/80 break-words">
369
+ <ReactMarkdown
370
+ components={{
371
+ a: ({ href, children }) => (
372
+ <a
373
+ href={href}
374
+ target="_blank"
375
+ rel="noreferrer"
376
+ className="text-emerald-300 hover:text-emerald-200 underline decoration-emerald-300/40"
377
+ >
378
+ {children}
379
+ </a>
380
+ ),
381
+ img: ({ src, alt }) => (
382
+ // eslint-disable-next-line @next/next/no-img-element
383
+ <img
384
+ src={src || ''}
385
+ alt={alt || 'Task artifact'}
386
+ loading="lazy"
387
+ className="max-w-full rounded-[8px] border border-white/[0.08] my-2"
388
+ />
389
+ ),
390
+ p: ({ children }) => <p className="m-0 mb-2">{children}</p>,
391
+ }}
307
392
  >
308
- {expanded ? 'Show less' : 'Show full result'}
309
- </button>
310
- )}
393
+ {info.resultBody}
394
+ </ReactMarkdown>
395
+ </div>
311
396
  </div>
312
397
  </div>
313
398
  )}
@@ -321,12 +406,13 @@ export function TaskCompletionCard({ info }: { info: TaskCompletionInfo }) {
321
406
  interface DelegationSourceBannerProps {
322
407
  delegatorName: string
323
408
  delegatorAvatarSeed: string | null
409
+ delegatorAvatarUrl?: string | null
324
410
  taskTitle: string
325
411
  taskId: string | null
326
412
  description: string
327
413
  }
328
414
 
329
- export function DelegationSourceBanner({ delegatorName, delegatorAvatarSeed, taskTitle, taskId, description }: DelegationSourceBannerProps) {
415
+ export function DelegationSourceBanner({ delegatorName, delegatorAvatarSeed, delegatorAvatarUrl, taskTitle, taskId, description }: DelegationSourceBannerProps) {
330
416
  const handleTaskClick = () => {
331
417
  if (!taskId) return
332
418
  const store = useAppStore.getState()
@@ -343,7 +429,7 @@ export function DelegationSourceBanner({ delegatorName, delegatorAvatarSeed, tas
343
429
  style={{ animation: 'delegation-handoff-in 0.45s cubic-bezier(0.16, 1, 0.3, 1)' }}
344
430
  >
345
431
  <div className="shrink-0 mt-0.5" style={{ animation: 'delegation-handoff-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.05s both' }}>
346
- <AgentAvatar seed={delegatorAvatarSeed} name={delegatorName} size={24} />
432
+ <AgentAvatar seed={delegatorAvatarSeed} avatarUrl={delegatorAvatarUrl} name={delegatorName} size={24} />
347
433
  </div>
348
434
  <div className="flex flex-col gap-1 min-w-0 flex-1">
349
435
  <span className="text-[12px] font-600 text-indigo-400">
@@ -136,6 +136,7 @@ interface Props {
136
136
  message: Message
137
137
  assistantName?: string
138
138
  agentAvatarSeed?: string
139
+ agentAvatarUrl?: string | null
139
140
  agentName?: string
140
141
  isLast?: boolean
141
142
  onRetry?: () => void
@@ -147,7 +148,7 @@ interface Props {
147
148
  momentOverlay?: React.ReactNode
148
149
  }
149
150
 
150
- export const MessageBubble = memo(function MessageBubble({ message, assistantName, agentAvatarSeed, agentName, isLast, onRetry, messageIndex, onToggleBookmark, onEditResend, onFork, onTransferToAgent, momentOverlay }: Props) {
151
+ export const MessageBubble = memo(function MessageBubble({ message, assistantName, agentAvatarSeed, agentAvatarUrl, agentName, isLast, onRetry, messageIndex, onToggleBookmark, onEditResend, onFork, onTransferToAgent, momentOverlay }: Props) {
151
152
  const isUser = message.role === 'user'
152
153
  const isHeartbeat = !isUser && (message.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(message.text || ''))
153
154
  const currentUser = useAppStore((s) => s.currentUser)
@@ -231,7 +232,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
231
232
  {!isUser && (
232
233
  <div className="absolute left-[4px] top-0">
233
234
  <div style={momentOverlay ? { animation: 'avatar-moment-pulse 0.6s ease' } : undefined}>
234
- {agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={28} /> : <AiAvatar size="sm" />}
235
+ {agentName ? <AgentAvatar seed={agentAvatarSeed || null} avatarUrl={agentAvatarUrl} name={agentName} size={28} /> : <AiAvatar size="sm" />}
235
236
  </div>
236
237
  {momentOverlay}
237
238
  </div>
@@ -439,7 +439,7 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
439
439
  )}
440
440
  {filteredMessages.length === 0 && !streaming && (
441
441
  <div className="flex flex-col items-center justify-center gap-3 py-20 text-center" style={{ animation: 'fadeUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) both' }}>
442
- <AgentAvatar seed={agent?.avatarSeed || null} name={agent?.name || 'Agent'} size={48} />
442
+ <AgentAvatar seed={agent?.avatarSeed || null} avatarUrl={agent?.avatarUrl} name={agent?.name || 'Agent'} size={48} />
443
443
  <span className="font-display text-[16px] font-600 text-text-2">{agent?.name || 'Assistant'}</span>
444
444
  <span className="text-[14px] text-text-3/60">
445
445
  {INTRO_GREETINGS[stableHash(agent?.id || session?.id || '') % INTRO_GREETINGS.length]}
@@ -526,6 +526,7 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
526
526
  message={msg}
527
527
  assistantName={assistantName}
528
528
  agentAvatarSeed={agent?.avatarSeed}
529
+ agentAvatarUrl={agent?.avatarUrl}
529
530
  agentName={agent?.name}
530
531
  isLast={isLastAssistant}
531
532
  onRetry={isLastAssistant ? retryLastMessage : undefined}
@@ -540,9 +541,9 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
540
541
  )
541
542
  })}
542
543
  <ApprovalCards agentId={agent?.id} />
543
- {streaming && !displayText && <ThinkingIndicator assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentName={agent?.name} />}
544
- {streaming && displayText && <StreamingBubble text={displayText} assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentName={agent?.name} />}
545
- {appSettings.suggestionsEnabled !== false && !streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
544
+ {streaming && !displayText && <ThinkingIndicator assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentAvatarUrl={agent?.avatarUrl} agentName={agent?.name} />}
545
+ {streaming && displayText && <StreamingBubble text={displayText} assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentAvatarUrl={agent?.avatarUrl} agentName={agent?.name} />}
546
+ {appSettings.suggestionsEnabled === true && !streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
546
547
  <SuggestionsBar lastMessage={filteredMessages[filteredMessages.length - 1]} onSend={sendMessage} />
547
548
  )}
548
549
  </div>
@@ -104,10 +104,11 @@ interface Props {
104
104
  text: string
105
105
  assistantName?: string
106
106
  agentAvatarSeed?: string
107
+ agentAvatarUrl?: string | null
107
108
  agentName?: string
108
109
  }
109
110
 
110
- export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentName }: Props) {
111
+ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentAvatarUrl, agentName }: Props) {
111
112
  const toolEvents = useChatStore((s) => s.toolEvents)
112
113
  const streamPhase = useChatStore((s) => s.streamPhase)
113
114
  const streamToolName = useChatStore((s) => s.streamToolName)
@@ -137,7 +138,7 @@ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentNam
137
138
  style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}
138
139
  >
139
140
  <div className="absolute left-[4px] top-0 relative">
140
- {agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
141
+ {agentName ? <AgentAvatar seed={agentAvatarSeed || null} avatarUrl={agentAvatarUrl} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
141
142
  {currentMoment && (
142
143
  <ActivityMoment
143
144
  key={currentMoment.id}
@@ -10,6 +10,7 @@ import { useChatStore } from '@/stores/use-chat-store'
10
10
  interface Props {
11
11
  assistantName?: string
12
12
  agentAvatarSeed?: string
13
+ agentAvatarUrl?: string | null
13
14
  agentName?: string
14
15
  }
15
16
 
@@ -34,7 +35,7 @@ function ElapsedTimer({ startTime }: { startTime: number }) {
34
35
  )
35
36
  }
36
37
 
37
- export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentName }: Props) {
38
+ export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentAvatarUrl, agentName }: Props) {
38
39
  const streamPhase = useChatStore((s) => s.streamPhase)
39
40
  const streamToolName = useChatStore((s) => s.streamToolName)
40
41
  const thinkingText = useChatStore((s) => s.thinkingText)
@@ -50,7 +51,7 @@ export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentName }:
50
51
  <div className="flex flex-col items-start relative pl-[44px]"
51
52
  style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}>
52
53
  <div className="absolute left-[4px] top-0">
53
- {agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
54
+ {agentName ? <AgentAvatar seed={agentAvatarSeed || null} avatarUrl={agentAvatarUrl} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
54
55
  </div>
55
56
  <div className="flex items-center gap-2.5 mb-2 px-1">
56
57
  <span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
@@ -52,7 +52,7 @@ export function TransferAgentPicker({ excludeIds, filterIds, onSelect, onClose }
52
52
  className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none"
53
53
  style={{ fontFamily: 'inherit' }}
54
54
  >
55
- <AgentAvatar seed={a.avatarSeed} name={a.name} size={20} />
55
+ <AgentAvatar seed={a.avatarSeed} avatarUrl={a.avatarUrl} name={a.name} size={20} />
56
56
  <span className="text-[12px] text-text truncate">{a.name}</span>
57
57
  </button>
58
58
  ))}
@@ -46,7 +46,7 @@ export function AgentHoverCard({ agent, children, status }: Props) {
46
46
  <HoverCardContent align="start" className="w-[280px]">
47
47
  {/* Header: avatar + name + model */}
48
48
  <div className="flex items-center gap-2">
49
- <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={28} status={status} />
49
+ <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={28} status={status} />
50
50
  <div className="min-w-0 flex-1">
51
51
  <div className="text-[13px] font-600 text-text truncate">{agent.name}</div>
52
52
  <div className="label-mono truncate">{agent.model}</div>
@@ -203,7 +203,7 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
203
203
  selectedIndex === i + 1 ? 'bg-white/[0.08]' : 'hover:bg-white/[0.06]'
204
204
  }`}
205
205
  >
206
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={20} />
206
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={20} />
207
207
  <span className="text-[13px] text-text">{agent.name}</span>
208
208
  </button>
209
209
  ))}
@@ -172,7 +172,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
172
172
  className="bg-transparent border-none p-0 cursor-pointer transition-all duration-150 hover:scale-110 hover:-translate-y-0.5"
173
173
  style={momentOverlay ? { animation: 'avatar-moment-pulse 0.6s ease' } : undefined}
174
174
  >
175
- <AgentAvatar seed={agent.avatarSeed || null} name={message.senderName} size={28} status={streamingAgentIds?.has(message.senderId) ? 'busy' : 'online'} />
175
+ <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={message.senderName} size={28} status={streamingAgentIds?.has(message.senderId) ? 'busy' : 'online'} />
176
176
  </button>
177
177
  ) : (
178
178
  <div style={momentOverlay ? { animation: 'avatar-moment-pulse 0.6s ease' } : undefined}>
@@ -178,7 +178,7 @@ export function ChatroomSheet() {
178
178
  selected ? 'bg-accent-soft/40' : 'hover:bg-white/[0.04]'
179
179
  }`}
180
180
  >
181
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={24} />
181
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={24} />
182
182
  <span className="text-[13px] text-text flex-1 truncate">{agent.name}</span>
183
183
  {selected && (
184
184
  <CheckIcon size={14} className="text-accent-bright shrink-0" />
@@ -63,7 +63,7 @@ export function ChatroomTypingBar({ streamingAgents }: Props) {
63
63
  return (
64
64
  <div key={agentId} className="flex gap-2.5 px-4 py-1.5" style={{ animation: 'msg-in 0.2s ease-out both' }}>
65
65
  <div className="shrink-0 mt-0.5 w-7">
66
- <AgentAvatar seed={agent?.avatarSeed || null} name={a.name} size={28} />
66
+ <AgentAvatar seed={agent?.avatarSeed || null} avatarUrl={agent?.avatarUrl} name={a.name} size={28} />
67
67
  </div>
68
68
  <div className="flex-1 min-w-0">
69
69
  <div className="flex items-baseline gap-2 mb-0.5">
@@ -192,7 +192,7 @@ export function ChatroomView() {
192
192
  onClick={() => navigateToAgent(agent.id)}
193
193
  className="relative transition-all duration-200 hover:scale-110 hover:z-10 hover:-translate-y-0.5 cursor-pointer bg-transparent border-none p-0"
194
194
  >
195
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={22} status={streamingAgents.has(agent.id) ? 'busy' : 'online'} />
195
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={22} status={streamingAgents.has(agent.id) ? 'busy' : 'online'} />
196
196
  </button>
197
197
  </TooltipTrigger>
198
198
  <TooltipContent side="bottom" sideOffset={6}>
@@ -206,7 +206,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
206
206
  </>
207
207
  ) : agent ? (
208
208
  <>
209
- <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={24} />
209
+ <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={24} />
210
210
  <div className="flex-1 min-w-0">
211
211
  <span className="text-[12px] font-600 text-text-2 block truncate">{agent.name}</span>
212
212
  <span className="text-[10px] text-text-3/60 block">{agent.provider}/{agent.model}</span>
@@ -353,7 +353,7 @@ export function HomeView() {
353
353
  style={{ fontFamily: 'inherit' }}
354
354
  >
355
355
  <div className="relative">
356
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={36} />
356
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={36} />
357
357
  <div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-surface ${
358
358
  isTyping ? 'bg-accent-bright animate-pulse'
359
359
  : isOnline ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]'
@@ -414,6 +414,7 @@ export function HomeView() {
414
414
  >
415
415
  <AgentAvatar
416
416
  seed={agent?.avatarSeed}
417
+ avatarUrl={agent?.avatarUrl}
417
418
  name={displayName}
418
419
  size={28}
419
420
  />
@@ -183,7 +183,7 @@ export function KnowledgeList() {
183
183
  <div className="flex items-center gap-1.5">
184
184
  <div className="flex items-center -space-x-1.5">
185
185
  {scopedAgents.slice(0, 5).map((agent) => (
186
- <AgentAvatar key={agent.id} seed={agent.avatarSeed} name={agent.name} size={16} className="ring-1 ring-surface" />
186
+ <AgentAvatar key={agent.id} seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={16} className="ring-1 ring-surface" />
187
187
  ))}
188
188
  </div>
189
189
  {scopedAgents.length > 5 && (
@@ -360,7 +360,7 @@ export function KnowledgeSheet() {
360
360
  }`}
361
361
  style={{ fontFamily: 'inherit' }}
362
362
  >
363
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={24} />
363
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={24} />
364
364
  <span className="text-[13px] text-text flex-1 truncate">{agent.name}</span>
365
365
  {selected && (
366
366
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="text-accent-bright shrink-0">
@@ -41,6 +41,7 @@ import { PluginSheet } from '@/components/plugins/plugin-sheet'
41
41
  import { RunList } from '@/components/runs/run-list'
42
42
  import { ActivityFeed } from '@/components/activity/activity-feed'
43
43
  import { MetricsDashboard } from '@/components/usage/metrics-dashboard'
44
+ import { WalletPanel } from '@/components/wallets/wallet-panel'
44
45
  import { ProjectList } from '@/components/projects/project-list'
45
46
  import { ProjectDetail } from '@/components/projects/project-detail'
46
47
  import { ProjectSheet } from '@/components/projects/project-sheet'
@@ -398,6 +399,11 @@ export function AppLayout() {
398
399
  <line x1="18" y1="20" x2="18" y2="10" /><line x1="12" y1="20" x2="12" y2="4" /><line x1="6" y1="20" x2="6" y2="14" />
399
400
  </svg>
400
401
  </NavItem>
402
+ <NavItem view="wallets" label="Wallets" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('wallets')}>
403
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
404
+ <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" />
405
+ </svg>
406
+ </NavItem>
401
407
  <NavItem view="runs" label="Runs" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('runs')}>
402
408
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
403
409
  <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
@@ -519,8 +525,8 @@ export function AppLayout() {
519
525
  </div>
520
526
  )}
521
527
 
522
- {/* Desktop: Side panel */}
523
- {isDesktop && sidebarOpen && (
528
+ {/* Desktop: Side panel (wallets has its own built-in sidebar) */}
529
+ {isDesktop && sidebarOpen && activeView !== 'wallets' && (
524
530
  <div
525
531
  className="w-[280px] shrink-0 bg-raised border-r border-white/[0.04] flex flex-col h-full"
526
532
  style={{ animation: 'panel-in 0.2s cubic-bezier(0.16, 1, 0.3, 1)' }}
@@ -727,6 +733,8 @@ export function AppLayout() {
727
733
  <ActivityFeed />
728
734
  ) : activeView === 'usage' ? (
729
735
  <MetricsDashboard />
736
+ ) : activeView === 'wallets' ? (
737
+ <WalletPanel />
730
738
  ) : activeView === 'chatrooms' ? (
731
739
  <div className="flex-1 flex h-full min-w-0">
732
740
  <div className="w-[280px] shrink-0 border-r border-white/[0.06] flex flex-col">
@@ -886,6 +894,7 @@ const VIEW_DESCRIPTIONS: Record<AppView, string> = {
886
894
  logs: 'Application logs & error tracking',
887
895
  plugins: 'Extend agent capabilities with custom plugins',
888
896
  usage: 'Usage metrics, cost tracking & agent performance',
897
+ wallets: 'Agent crypto wallets — hold funds, send SOL, manage spending',
889
898
  runs: 'Live run monitoring & history',
890
899
  settings: 'Manage providers, API keys & orchestrator engine',
891
900
  projects: 'Group agents, tasks & schedules into projects',
@@ -895,7 +904,7 @@ const VIEW_DESCRIPTIONS: Record<AppView, string> = {
895
904
  const FULL_WIDTH_VIEWS = new Set<AppView>([
896
905
  'home', 'chatrooms', 'schedules', 'secrets', 'providers', 'skills',
897
906
  'connectors', 'webhooks', 'mcp_servers', 'knowledge', 'plugins',
898
- 'usage', 'runs', 'logs', 'settings', 'activity', 'projects',
907
+ 'usage', 'wallets', 'runs', 'logs', 'settings', 'activity', 'projects',
899
908
  ])
900
909
 
901
910
  const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents' | 'home'>, { icon: string; title: string; description: string; features: string[] }> = {
@@ -1007,6 +1016,12 @@ const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents' | 'home'>, { icon: str
1007
1016
  description: 'Audit trail of all entity mutations across the system.',
1008
1017
  features: ['Track agent, task, and connector changes', 'Filter by entity type and action', 'Real-time updates via WebSocket', 'Relative timestamps'],
1009
1018
  },
1019
+ wallets: {
1020
+ icon: 'wallet',
1021
+ title: 'Wallets',
1022
+ description: 'Agent crypto wallets for autonomous financial operations on Solana.',
1023
+ features: ['Create Solana wallets for agents', 'Per-transaction and daily spending limits', 'User approval for transactions', 'Balance tracking and transaction history'],
1024
+ },
1010
1025
  }
1011
1026
 
1012
1027
  function ViewEmptyState({ view }: { view: AppView }) {
@@ -120,7 +120,7 @@ export function MemoryAgentList() {
120
120
  {isActive && (
121
121
  <div className="absolute left-0 top-2 bottom-2 w-[2.5px] rounded-full bg-accent-bright" />
122
122
  )}
123
- <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={28} />
123
+ <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={28} />
124
124
  <span className={`text-[13px] font-600 flex-1 truncate ${isActive ? 'text-accent-bright' : 'text-text-2'}`}>
125
125
  {agent.name}
126
126
  </span>
@@ -163,6 +163,7 @@ export function MemoryBrowser() {
163
163
  active={e.id === selectedMemoryId}
164
164
  agentName={showAgent ? (agent?.name || null) : undefined}
165
165
  agentAvatarSeed={showAgent ? (agent?.avatarSeed || null) : undefined}
166
+ agentAvatarUrl={showAgent ? (agent?.avatarUrl || null) : undefined}
166
167
  onClick={() => setSelectedMemoryId(e.id)}
167
168
  />
168
169
  )
@@ -17,10 +17,11 @@ interface Props {
17
17
  active?: boolean
18
18
  agentName?: string | null
19
19
  agentAvatarSeed?: string | null
20
+ agentAvatarUrl?: string | null
20
21
  onClick: () => void
21
22
  }
22
23
 
23
- export function MemoryCard({ entry, active, agentName, agentAvatarSeed, onClick }: Props) {
24
+ export function MemoryCard({ entry, active, agentName, agentAvatarSeed, agentAvatarUrl, onClick }: Props) {
24
25
  return (
25
26
  <div
26
27
  onClick={onClick}
@@ -73,7 +74,7 @@ export function MemoryCard({ entry, active, agentName, agentAvatarSeed, onClick
73
74
  )}
74
75
  {agentName ? (
75
76
  <div className="flex items-center gap-1.5 mt-1.5">
76
- <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={16} />
77
+ <AgentAvatar seed={agentAvatarSeed || null} avatarUrl={agentAvatarUrl} name={agentName} size={16} />
77
78
  <span className="text-[10px] text-text-3/60 truncate">{agentName}</span>
78
79
  </div>
79
80
  ) : !entry.agentId ? (
@@ -329,7 +329,7 @@ export function MemoryDetail() {
329
329
  : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
330
330
  style={{ fontFamily: 'inherit' }}
331
331
  >
332
- <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={16} />
332
+ <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={16} />
333
333
  <span className="truncate max-w-[100px]">{agent.name}</span>
334
334
  </button>
335
335
  ))}
@@ -360,7 +360,7 @@ export function MemoryDetail() {
360
360
  : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
361
361
  style={{ fontFamily: 'inherit' }}
362
362
  >
363
- <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={16} />
363
+ <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={16} />
364
364
  <span className="truncate max-w-[100px]">{agent.name}</span>
365
365
  </button>
366
366
  )
@@ -406,7 +406,7 @@ export function MemoryDetail() {
406
406
  const a = agents[aid]
407
407
  return (
408
408
  <span key={aid} className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-white/[0.03] text-[11px] text-text-3">
409
- <AgentAvatar seed={a?.avatarSeed || null} name={a?.name || aid} size={16} />
409
+ <AgentAvatar seed={a?.avatarSeed || null} avatarUrl={a?.avatarUrl} name={a?.name || aid} size={16} />
410
410
  {a?.name || aid}
411
411
  </span>
412
412
  )
@@ -104,7 +104,7 @@ export function MemorySheet() {
104
104
  : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
105
105
  style={{ fontFamily: 'inherit' }}
106
106
  >
107
- <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={20} />
107
+ <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={20} />
108
108
  <span className="truncate max-w-[120px]">{agent.name}</span>
109
109
  </button>
110
110
  ))}
@@ -140,7 +140,7 @@ export function MemorySheet() {
140
140
  : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
141
141
  style={{ fontFamily: 'inherit' }}
142
142
  >
143
- <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={20} />
143
+ <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={20} />
144
144
  <span className="truncate max-w-[120px]">{agent.name}</span>
145
145
  </button>
146
146
  )