@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
@@ -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">
@@ -10,13 +10,35 @@ import { ChatroomTypingBar } from './chatroom-typing-bar'
10
10
  import { AgentAvatar } from '@/components/agents/agent-avatar'
11
11
  import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
12
12
  import { HeartbeatMoment, ActivityMoment, isNotableTool } from '@/components/chat/activity-moment'
13
- import type { Chatroom, ChatroomMessage, Agent } from '@/types'
13
+ import type { Chatroom, ChatroomMessage, ChatroomMember, Agent } from '@/types'
14
14
 
15
15
  function navigateToAgent(agentId: string) {
16
16
  useAppStore.getState().setActiveView('agents')
17
17
  useAppStore.getState().setCurrentAgent(agentId)
18
18
  }
19
19
 
20
+ function getRoleBadge(role: string) {
21
+ if (role === 'admin') return { label: 'Admin', className: 'bg-purple-500/20 text-purple-400 border-purple-500/30' }
22
+ if (role === 'moderator') return { label: 'Mod', className: 'bg-blue-500/20 text-blue-400 border-blue-500/30' }
23
+ return null
24
+ }
25
+
26
+ function getMemberFromChatroom(chatroom: Chatroom, agentId: string): ChatroomMember | undefined {
27
+ if (chatroom.members?.length) return chatroom.members.find((m) => m.agentId === agentId)
28
+ return undefined
29
+ }
30
+
31
+ function getMemberRole(chatroom: Chatroom, agentId: string): string {
32
+ const member = getMemberFromChatroom(chatroom, agentId)
33
+ return member?.role || 'member'
34
+ }
35
+
36
+ function isAgentMuted(chatroom: Chatroom, agentId: string): boolean {
37
+ const member = getMemberFromChatroom(chatroom, agentId)
38
+ if (!member?.mutedUntil) return false
39
+ return new Date(member.mutedUntil).getTime() > Date.now()
40
+ }
41
+
20
42
  type MomentType = { kind: 'heartbeat' } | { kind: 'tool'; name: string; input: string }
21
43
 
22
44
  /** Subscribe to a single agent heartbeat topic — one hook call per agent */
@@ -65,6 +87,10 @@ export function ChatroomView() {
65
87
  const loadChatrooms = useChatroomStore((s) => s.loadChatrooms)
66
88
  const setChatroomSheetOpen = useChatroomStore((s) => s.setChatroomSheetOpen)
67
89
  const setEditingChatroomId = useChatroomStore((s) => s.setEditingChatroomId)
90
+ const deleteMessage = useChatroomStore((s) => s.deleteMessage)
91
+ const muteAgent = useChatroomStore((s) => s.muteAgent)
92
+ const unmuteAgent = useChatroomStore((s) => s.unmuteAgent)
93
+ const setMemberRole = useChatroomStore((s) => s.setMemberRole)
68
94
  const agents = useAppStore((s) => s.agents) as Record<string, Agent>
69
95
  const scrollRef = useRef<HTMLDivElement>(null)
70
96
  const [pinsExpanded, setPinsExpanded] = useState(false)
@@ -183,23 +209,37 @@ export function ChatroomView() {
183
209
  {chatroom.description ? ` · ${chatroom.description}` : ''}
184
210
  </p>
185
211
  </div>
186
- {/* Member avatars */}
212
+ {/* Member avatars with role badges */}
187
213
  <div className="flex -space-x-1.5 shrink-0">
188
- {memberAgents.slice(0, 5).map((agent) => (
189
- <Tooltip key={agent.id}>
190
- <TooltipTrigger asChild>
191
- <button
192
- onClick={() => navigateToAgent(agent.id)}
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
- >
195
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={22} status={streamingAgents.has(agent.id) ? 'busy' : 'online'} />
196
- </button>
197
- </TooltipTrigger>
198
- <TooltipContent side="bottom" sideOffset={6}>
199
- {agent.name}
200
- </TooltipContent>
201
- </Tooltip>
202
- ))}
214
+ {memberAgents.slice(0, 5).map((agent) => {
215
+ const role = getMemberRole(chatroom, agent.id)
216
+ const badge = getRoleBadge(role)
217
+ const muted = isAgentMuted(chatroom, agent.id)
218
+ return (
219
+ <Tooltip key={agent.id}>
220
+ <TooltipTrigger asChild>
221
+ <button
222
+ onClick={() => navigateToAgent(agent.id)}
223
+ 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 ${muted ? 'opacity-40' : ''}`}
224
+ >
225
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={22} status={streamingAgents.has(agent.id) ? 'busy' : 'online'} />
226
+ {badge && (
227
+ <span className={`absolute -bottom-1 -right-1 text-[7px] font-700 px-0.5 rounded border ${badge.className}`}>
228
+ {badge.label[0]}
229
+ </span>
230
+ )}
231
+ </button>
232
+ </TooltipTrigger>
233
+ <TooltipContent side="bottom" sideOffset={6}>
234
+ <div className="flex items-center gap-1.5">
235
+ <span>{agent.name}</span>
236
+ {badge && <span className={`text-[9px] font-600 px-1 py-0.5 rounded border ${badge.className}`}>{badge.label}</span>}
237
+ {muted && <span className="text-[9px] text-red-400">Muted</span>}
238
+ </div>
239
+ </TooltipContent>
240
+ </Tooltip>
241
+ )
242
+ })}
203
243
  {memberAgents.length > 5 && (
204
244
  <div className="w-[22px] h-[22px] rounded-full bg-white/[0.08] flex items-center justify-center text-[9px] text-text-3">
205
245
  +{memberAgents.length - 5}
@@ -320,6 +360,11 @@ export function ChatroomView() {
320
360
  onReply={(m: ChatroomMessage) => setReplyingTo(m)}
321
361
  onTogglePin={togglePin}
322
362
  onTransfer={handleTransfer}
363
+ onDeleteMessage={(messageId, targetAgentId) => deleteMessage(messageId, targetAgentId)}
364
+ onMuteAgent={(agentId) => muteAgent(agentId)}
365
+ onUnmuteAgent={(agentId) => unmuteAgent(agentId)}
366
+ onSetRole={(agentId, role) => setMemberRole(agentId, role)}
367
+ chatroom={chatroom}
323
368
  pinnedMessageIds={pinnedIds}
324
369
  streamingAgentIds={streamingAgentIds}
325
370
  messages={chatroom.messages}
@@ -0,0 +1,120 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { api } from '@/lib/api-client'
5
+ import type { ConnectorHealthEvent, ConnectorHealthEventType } from '@/types'
6
+
7
+ interface HealthResponse {
8
+ events: ConnectorHealthEvent[]
9
+ uptimePercent: number
10
+ }
11
+
12
+ const EVENT_CONFIG: Record<ConnectorHealthEventType, { color: string; label: string }> = {
13
+ started: { color: 'bg-green-400', label: 'Started' },
14
+ reconnected: { color: 'bg-green-400', label: 'Reconnected' },
15
+ stopped: { color: 'bg-white/30', label: 'Stopped' },
16
+ error: { color: 'bg-red-400', label: 'Error' },
17
+ disconnected: { color: 'bg-amber-400', label: 'Disconnected' },
18
+ }
19
+
20
+ function formatTimestamp(ts: string): string {
21
+ const d = new Date(ts)
22
+ const now = new Date()
23
+ const diffMs = now.getTime() - d.getTime()
24
+ const diffMin = Math.floor(diffMs / 60_000)
25
+ const diffHr = Math.floor(diffMs / 3_600_000)
26
+ const diffDay = Math.floor(diffMs / 86_400_000)
27
+
28
+ if (diffMin < 1) return 'just now'
29
+ if (diffMin < 60) return `${diffMin}m ago`
30
+ if (diffHr < 24) return `${diffHr}h ago`
31
+ if (diffDay < 7) return `${diffDay}d ago`
32
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
33
+ }
34
+
35
+ function uptimeBadgeColor(pct: number): string {
36
+ if (pct >= 99) return 'bg-green-500/15 text-green-400 border-green-500/20'
37
+ if (pct >= 95) return 'bg-amber-500/15 text-amber-400 border-amber-500/20'
38
+ return 'bg-red-500/15 text-red-400 border-red-500/20'
39
+ }
40
+
41
+ export function ConnectorHealth({ connectorId }: { connectorId: string }) {
42
+ const [data, setData] = useState<HealthResponse | null>(null)
43
+ const [loading, setLoading] = useState(true)
44
+
45
+ useEffect(() => {
46
+ let cancelled = false
47
+ async function load() {
48
+ setLoading(true)
49
+ try {
50
+ const resp = await api<HealthResponse>('GET', `/connectors/${connectorId}/health`)
51
+ if (!cancelled) setData(resp)
52
+ } catch {
53
+ // ignore fetch errors
54
+ } finally {
55
+ if (!cancelled) setLoading(false)
56
+ }
57
+ }
58
+ load()
59
+ return () => { cancelled = true }
60
+ }, [connectorId])
61
+
62
+ if (loading) {
63
+ return (
64
+ <div className="p-4 rounded-[14px] border border-white/[0.06] bg-white/[0.01]">
65
+ <div className="text-[13px] text-text-3 animate-pulse">Loading health data...</div>
66
+ </div>
67
+ )
68
+ }
69
+
70
+ if (!data || data.events.length === 0) {
71
+ return (
72
+ <div className="p-4 rounded-[14px] border border-white/[0.06] bg-white/[0.01]">
73
+ <div className="text-[13px] text-text-3">No health events recorded yet.</div>
74
+ </div>
75
+ )
76
+ }
77
+
78
+ // Show most recent events first (up to 50)
79
+ const recentEvents = [...data.events].reverse().slice(0, 50)
80
+
81
+ return (
82
+ <div className="p-4 rounded-[14px] border border-white/[0.06] bg-white/[0.01] space-y-4">
83
+ {/* Uptime badge */}
84
+ <div className="flex items-center justify-between">
85
+ <div className="text-[13px] font-600 text-text-2">Health Timeline</div>
86
+ <span className={`px-3 py-1 rounded-[8px] text-[12px] font-600 border ${uptimeBadgeColor(data.uptimePercent)}`}>
87
+ {data.uptimePercent}% uptime
88
+ </span>
89
+ </div>
90
+
91
+ {/* Timeline */}
92
+ <div className="relative pl-5">
93
+ {/* Vertical line */}
94
+ <div className="absolute left-[7px] top-2 bottom-2 w-px bg-white/[0.08]" />
95
+
96
+ <div className="space-y-3">
97
+ {recentEvents.map((ev) => {
98
+ const cfg = EVENT_CONFIG[ev.event] ?? { color: 'bg-white/30', label: ev.event }
99
+ return (
100
+ <div key={ev.id} className="relative flex items-start gap-3">
101
+ {/* Dot */}
102
+ <div className={`absolute left-[-13px] top-[6px] w-[10px] h-[10px] rounded-full ${cfg.color} ring-2 ring-surface shrink-0`} />
103
+ {/* Content */}
104
+ <div className="min-w-0 flex-1">
105
+ <div className="flex items-center gap-2">
106
+ <span className="text-[13px] font-600 text-text-2">{cfg.label}</span>
107
+ <span className="text-[11px] text-text-3">{formatTimestamp(ev.timestamp)}</span>
108
+ </div>
109
+ {ev.message && (
110
+ <p className="text-[12px] text-text-3/70 mt-0.5 leading-[1.4] break-words">{ev.message}</p>
111
+ )}
112
+ </div>
113
+ </div>
114
+ )
115
+ })}
116
+ </div>
117
+ </div>
118
+ </div>
119
+ )
120
+ }
@@ -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>
@@ -13,6 +13,7 @@ import { ChatroomPickerList } from '@/components/shared/chatroom-picker-list'
13
13
  import { SheetFooter } from '@/components/shared/sheet-footer'
14
14
  import { SectionLabel } from '@/components/shared/section-label'
15
15
  import { useChatroomStore } from '@/stores/use-chatroom-store'
16
+ import { ConnectorHealth } from '@/components/connectors/connector-health'
16
17
 
17
18
  /** Auto-detect URLs in text and make them clickable links that open in a new tab */
18
19
  function linkify(text: string) {
@@ -655,6 +656,7 @@ export function ConnectorSheet() {
655
656
  <span key={i} className="flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] bg-accent-soft/50 border border-accent-bright/20 text-[12px] font-mono text-accent-bright">
656
657
  {tag}
657
658
  <button
659
+ aria-label={`Remove ${tag}`}
658
660
  onClick={() => {
659
661
  const next = tags.filter((_, j) => j !== i).join(',')
660
662
  setConfig({ ...config, [field.key]: next })
@@ -860,6 +862,13 @@ export function ConnectorSheet() {
860
862
  </div>
861
863
  )}
862
864
 
865
+ {/* Health timeline (existing connectors only) */}
866
+ {editing && (
867
+ <div className="mb-6">
868
+ <ConnectorHealth connectorId={editing.id} />
869
+ </div>
870
+ )}
871
+
863
872
  {/* Actions */}
864
873
  <SheetFooter
865
874
  onCancel={() => { setOpen(false); setEditingId(null) }}
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useMemo, useState } from 'react'
4
+ import { AreaChart, Area, ResponsiveContainer } from 'recharts'
4
5
  import { useAppStore } from '@/stores/use-app-store'
5
6
  import { useChatStore } from '@/stores/use-chat-store'
6
7
  import { AgentAvatar } from '@/components/agents/agent-avatar'
@@ -85,6 +86,7 @@ export function HomeView() {
85
86
  const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
86
87
  const setMessages = useChatStore((s) => s.setMessages)
87
88
  const [todayCost, setTodayCost] = useState(0)
89
+ const [costTrend, setCostTrend] = useState<{ cost: number }[]>([])
88
90
 
89
91
  const allAgents = Object.values(agents).filter((a) => !a.trashedAt)
90
92
  const pinnedAgents = allAgents.filter((a) => a.pinned)
@@ -144,10 +146,11 @@ export function HomeView() {
144
146
  void loadSchedules()
145
147
  void loadNotifications()
146
148
  void loadConnectors()
147
- api<{ records: Array<{ estimatedCost: number }> }>('GET', '/usage?range=24h')
149
+ api<{ records: Array<{ estimatedCost: number }>; timeSeries: Array<{ cost: number }> }>('GET', '/usage?range=7d')
148
150
  .then((data) => {
149
151
  const total = (data.records || []).reduce((s, r) => s + (r.estimatedCost || 0), 0)
150
152
  setTodayCost(total)
153
+ setCostTrend((data.timeSeries || []).map((pt) => ({ cost: pt.cost })))
151
154
  })
152
155
  .catch(() => {})
153
156
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -196,13 +199,31 @@ export function HomeView() {
196
199
  </div>
197
200
 
198
201
  {/* Quick Stats */}
199
- <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-10">
202
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
200
203
  <StatCard label="Agents" value={String(agentCount)} />
201
204
  <StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} />
202
205
  <StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} />
203
206
  <StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} />
204
207
  </div>
205
208
 
209
+ {/* Cost trend sparkline */}
210
+ {costTrend.length > 1 && (
211
+ <div className="mb-10 px-1">
212
+ <p className="text-[10px] text-text-3/50 uppercase tracking-wider mb-1">7-day cost trend</p>
213
+ <ResponsiveContainer width="100%" height={60}>
214
+ <AreaChart data={costTrend} margin={{ top: 2, right: 0, bottom: 0, left: 0 }}>
215
+ <defs>
216
+ <linearGradient id="costGrad" x1="0" y1="0" x2="0" y2="1">
217
+ <stop offset="0%" stopColor="#818CF8" stopOpacity={0.3} />
218
+ <stop offset="100%" stopColor="#818CF8" stopOpacity={0} />
219
+ </linearGradient>
220
+ </defs>
221
+ <Area type="monotone" dataKey="cost" stroke="#818CF8" strokeWidth={1.5} fill="url(#costGrad)" dot={false} />
222
+ </AreaChart>
223
+ </ResponsiveContainer>
224
+ </div>
225
+ )}
226
+
206
227
  {/* Notifications banner */}
207
228
  {unreadNotifications.length > 0 && (
208
229
  <section className="mb-8">
@@ -353,7 +374,7 @@ export function HomeView() {
353
374
  style={{ fontFamily: 'inherit' }}
354
375
  >
355
376
  <div className="relative">
356
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={36} />
377
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={36} />
357
378
  <div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-surface ${
358
379
  isTyping ? 'bg-accent-bright animate-pulse'
359
380
  : isOnline ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]'
@@ -414,6 +435,7 @@ export function HomeView() {
414
435
  >
415
436
  <AgentAvatar
416
437
  seed={agent?.avatarSeed}
438
+ avatarUrl={agent?.avatarUrl}
417
439
  name={displayName}
418
440
  size={28}
419
441
  />
@@ -8,6 +8,7 @@ import { useAutoResize } from '@/hooks/use-auto-resize'
8
8
  import { useSpeechRecognition } from '@/hooks/use-speech-recognition'
9
9
  import { FilePreview } from '@/components/shared/file-preview'
10
10
  import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
11
+ import { toast } from 'sonner'
11
12
  import { safeStorageGet, safeStorageRemove, safeStorageSet } from '@/lib/safe-storage'
12
13
 
13
14
  interface Props {
@@ -18,6 +19,8 @@ interface Props {
18
19
 
19
20
  // FilePreview is now imported from @/components/shared/file-preview
20
21
 
22
+ const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB
23
+
21
24
  export function ChatInput({ streaming, onSend, onStop }: Props) {
22
25
  const [value, setValue] = useState('')
23
26
  const { ref: textareaRef, resize } = useAutoResize()
@@ -90,6 +93,10 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
90
93
  )
91
94
 
92
95
  const uploadAndAdd = useCallback(async (file: File) => {
96
+ if (file.size > MAX_FILE_SIZE) {
97
+ toast.error(`File too large: ${(file.size / 1024 / 1024).toFixed(1)} MB (max 10 MB)`)
98
+ return
99
+ }
93
100
  try {
94
101
  const result = await uploadImage(file)
95
102
  addPendingFile({ file, path: result.path, url: result.url })
@@ -123,7 +130,7 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
123
130
  const hasContent = value.trim().length > 0 || pendingFiles.length > 0
124
131
 
125
132
  return (
126
- <div className="shrink-0 px-6 md:px-12 lg:px-16 pb-4 pt-2"
133
+ <div className="shrink-0 px-4 md:px-12 lg:px-16 pb-4 pt-2 fixed bottom-0 left-0 right-0 z-20 bg-bg/95 backdrop-blur-md md:relative md:z-auto md:bg-transparent md:backdrop-blur-none"
127
134
  style={{ paddingBottom: 'max(16px, env(safe-area-inset-bottom))' }}>
128
135
  <div>
129
136
  {streaming && (
@@ -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">
@@ -4,6 +4,7 @@ import { Component, useState, useEffect, useCallback } from 'react'
4
4
  import type { ReactNode, ErrorInfo } from 'react'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useMediaQuery } from '@/hooks/use-media-query'
7
+ import { useSwipe } from '@/hooks/use-swipe'
7
8
  import { Avatar } from '@/components/shared/avatar'
8
9
  import { SettingsPage } from '@/components/shared/settings/settings-page'
9
10
  import { AgentList } from '@/components/agents/agent-list'
@@ -41,6 +42,7 @@ import { PluginSheet } from '@/components/plugins/plugin-sheet'
41
42
  import { RunList } from '@/components/runs/run-list'
42
43
  import { ActivityFeed } from '@/components/activity/activity-feed'
43
44
  import { MetricsDashboard } from '@/components/usage/metrics-dashboard'
45
+ import { WalletPanel } from '@/components/wallets/wallet-panel'
44
46
  import { ProjectList } from '@/components/projects/project-list'
45
47
  import { ProjectDetail } from '@/components/projects/project-detail'
46
48
  import { ProjectSheet } from '@/components/projects/project-sheet'
@@ -52,6 +54,7 @@ import { HomeView } from '@/components/home/home-view'
52
54
  import { NetworkBanner } from './network-banner'
53
55
  import { UpdateBanner } from './update-banner'
54
56
  import { MobileHeader } from './mobile-header'
57
+ import { CommandPalette } from '@/components/shared/command-palette'
55
58
  import { DaemonIndicator } from './daemon-indicator'
56
59
  import { NotificationCenter } from '@/components/shared/notification-center'
57
60
  import { ChatArea } from '@/components/chat/chat-area'
@@ -193,6 +196,14 @@ export function AppLayout() {
193
196
  : Object.values(agents)[0]?.id || null
194
197
  const isMainChat = activeView === 'agents' && currentAgentId === defaultAgentId
195
198
 
199
+ const swipeHandlers = useSwipe({
200
+ onSwipe: (dir) => {
201
+ if (dir === 'right') setSidebarOpen(true)
202
+ else setSidebarOpen(false)
203
+ },
204
+ leftSwipeEnabled: sidebarOpen,
205
+ })
206
+
196
207
  const currentSession = currentSessionId ? sessions[currentSessionId] : null
197
208
  const hasCanvas = !!(currentSession?.canvasContent && canvasDismissedFor !== currentSessionId)
198
209
  const canvasAgentName = currentSession?.agentId && agents[currentSession.agentId] ? agents[currentSession.agentId].name : undefined
@@ -209,7 +220,12 @@ export function AppLayout() {
209
220
  }
210
221
 
211
222
  return (
212
- <div className="h-full flex overflow-hidden">
223
+ <div
224
+ className="h-full flex overflow-hidden"
225
+ onTouchStart={swipeHandlers.onTouchStart}
226
+ onTouchMove={swipeHandlers.onTouchMove}
227
+ onTouchEnd={swipeHandlers.onTouchEnd}
228
+ >
213
229
  {/* Desktop: Navigation rail (expandable) */}
214
230
  {isDesktop && (
215
231
  <div
@@ -398,6 +414,11 @@ export function AppLayout() {
398
414
  <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
415
  </svg>
400
416
  </NavItem>
417
+ <NavItem view="wallets" label="Wallets" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('wallets')}>
418
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
419
+ <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" />
420
+ </svg>
421
+ </NavItem>
401
422
  <NavItem view="runs" label="Runs" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('runs')}>
402
423
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
403
424
  <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
@@ -519,8 +540,8 @@ export function AppLayout() {
519
540
  </div>
520
541
  )}
521
542
 
522
- {/* Desktop: Side panel */}
523
- {isDesktop && sidebarOpen && (
543
+ {/* Desktop: Side panel (wallets has its own built-in sidebar) */}
544
+ {isDesktop && sidebarOpen && activeView !== 'wallets' && (
524
545
  <div
525
546
  className="w-[280px] shrink-0 bg-raised border-r border-white/[0.04] flex flex-col h-full"
526
547
  style={{ animation: 'panel-in 0.2s cubic-bezier(0.16, 1, 0.3, 1)' }}
@@ -727,6 +748,8 @@ export function AppLayout() {
727
748
  <ActivityFeed />
728
749
  ) : activeView === 'usage' ? (
729
750
  <MetricsDashboard />
751
+ ) : activeView === 'wallets' ? (
752
+ <WalletPanel />
730
753
  ) : activeView === 'chatrooms' ? (
731
754
  <div className="flex-1 flex h-full min-w-0">
732
755
  <div className="w-[280px] shrink-0 border-r border-white/[0.06] flex flex-col">
@@ -793,6 +816,7 @@ export function AppLayout() {
793
816
  </div>
794
817
  </ErrorBoundary>
795
818
 
819
+ <CommandPalette />
796
820
  <SearchDialog />
797
821
  <AgentSwitchDialog />
798
822
  <KeyboardShortcutsDialog />
@@ -886,6 +910,7 @@ const VIEW_DESCRIPTIONS: Record<AppView, string> = {
886
910
  logs: 'Application logs & error tracking',
887
911
  plugins: 'Extend agent capabilities with custom plugins',
888
912
  usage: 'Usage metrics, cost tracking & agent performance',
913
+ wallets: 'Agent crypto wallets — hold funds, send SOL, manage spending',
889
914
  runs: 'Live run monitoring & history',
890
915
  settings: 'Manage providers, API keys & orchestrator engine',
891
916
  projects: 'Group agents, tasks & schedules into projects',
@@ -895,7 +920,7 @@ const VIEW_DESCRIPTIONS: Record<AppView, string> = {
895
920
  const FULL_WIDTH_VIEWS = new Set<AppView>([
896
921
  'home', 'chatrooms', 'schedules', 'secrets', 'providers', 'skills',
897
922
  'connectors', 'webhooks', 'mcp_servers', 'knowledge', 'plugins',
898
- 'usage', 'runs', 'logs', 'settings', 'activity', 'projects',
923
+ 'usage', 'wallets', 'runs', 'logs', 'settings', 'activity', 'projects',
899
924
  ])
900
925
 
901
926
  const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents' | 'home'>, { icon: string; title: string; description: string; features: string[] }> = {
@@ -1007,6 +1032,12 @@ const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents' | 'home'>, { icon: str
1007
1032
  description: 'Audit trail of all entity mutations across the system.',
1008
1033
  features: ['Track agent, task, and connector changes', 'Filter by entity type and action', 'Real-time updates via WebSocket', 'Relative timestamps'],
1009
1034
  },
1035
+ wallets: {
1036
+ icon: 'wallet',
1037
+ title: 'Wallets',
1038
+ description: 'Agent crypto wallets for autonomous financial operations on Solana.',
1039
+ features: ['Create Solana wallets for agents', 'Per-transaction and daily spending limits', 'User approval for transactions', 'Balance tracking and transaction history'],
1040
+ },
1010
1041
  }
1011
1042
 
1012
1043
  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
  )