@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
@@ -35,11 +35,11 @@ export function UserPreferencesSection({ appSettings, patchSettings, inputClass
35
35
  </div>
36
36
  <button
37
37
  type="button"
38
- onClick={() => patchSettings({ suggestionsEnabled: appSettings.suggestionsEnabled === false })}
39
- className={`relative w-9 h-5 rounded-full transition-colors ${appSettings.suggestionsEnabled !== false ? 'bg-accent-bright' : 'bg-white/[0.10]'}`}
38
+ onClick={() => patchSettings({ suggestionsEnabled: !appSettings.suggestionsEnabled })}
39
+ className={`relative w-9 h-5 rounded-full transition-colors ${appSettings.suggestionsEnabled ? 'bg-accent-bright' : 'bg-white/[0.10]'}`}
40
40
  style={{ fontFamily: 'inherit' }}
41
41
  >
42
- <span className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${appSettings.suggestionsEnabled !== false ? 'translate-x-4' : ''}`} />
42
+ <span className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${appSettings.suggestionsEnabled ? 'translate-x-4' : ''}`} />
43
43
  </button>
44
44
  </div>
45
45
 
@@ -70,7 +70,7 @@ export function UserPreferencesSection({ appSettings, patchSettings, inputClass
70
70
  : 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
71
71
  style={{ fontFamily: 'inherit' }}
72
72
  >
73
- <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={18} />
73
+ <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={18} />
74
74
  {agent.name}
75
75
  </button>
76
76
  ))}
@@ -309,7 +309,7 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
309
309
  <div className="flex items-center gap-1.5 mt-1.5">
310
310
  <div className="flex items-center -space-x-1.5">
311
311
  {scopedAgents.slice(0, 5).map((agent) => (
312
- <AgentAvatar key={agent.id} seed={agent.avatarSeed} name={agent.name} size={16} className="ring-1 ring-surface" />
312
+ <AgentAvatar key={agent.id} seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={16} className="ring-1 ring-surface" />
313
313
  ))}
314
314
  </div>
315
315
  {scopedAgents.length > 5 && (
@@ -255,7 +255,7 @@ export function SkillSheet() {
255
255
  }`}
256
256
  style={{ fontFamily: 'inherit' }}
257
257
  >
258
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={24} />
258
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={24} />
259
259
  <span className="text-[13px] text-text flex-1 truncate">{agent.name}</span>
260
260
  {selected && (
261
261
  <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">
@@ -264,7 +264,7 @@ export function TaskBoard() {
264
264
  >
265
265
  {filterAgentId && agents[filterAgentId] ? (
266
266
  <>
267
- <AgentAvatar seed={agents[filterAgentId].avatarSeed || null} name={agents[filterAgentId].name} size={18} />
267
+ <AgentAvatar seed={agents[filterAgentId].avatarSeed || null} avatarUrl={agents[filterAgentId].avatarUrl} name={agents[filterAgentId].name} size={18} />
268
268
  {agents[filterAgentId].name}
269
269
  </>
270
270
  ) : 'All Agents'}
@@ -290,7 +290,7 @@ export function TaskBoard() {
290
290
  ${filterAgentId === a.id ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
291
291
  style={{ fontFamily: 'inherit' }}
292
292
  >
293
- <AgentAvatar seed={a.avatarSeed || null} name={a.name} size={20} />
293
+ <AgentAvatar seed={a.avatarSeed || null} avatarUrl={a.avatarUrl} name={a.name} size={20} />
294
294
  {a.name}
295
295
  </button>
296
296
  ))}
@@ -480,7 +480,7 @@ export function TaskBoard() {
480
480
  className="w-full flex items-center gap-2 px-3 py-2 text-[12px] font-600 cursor-pointer border-none text-left bg-transparent text-text-3 hover:bg-white/[0.06] hover:text-text transition-colors"
481
481
  style={{ fontFamily: 'inherit' }}
482
482
  >
483
- <AgentAvatar seed={a.avatarSeed || null} name={a.name} size={16} />
483
+ <AgentAvatar seed={a.avatarSeed || null} avatarUrl={a.avatarUrl} name={a.name} size={16} />
484
484
  {a.name}
485
485
  </button>
486
486
  ))}
@@ -4,6 +4,7 @@ import { useState, useCallback } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { api } from '@/lib/api-client'
6
6
  import { updateTask, archiveTask } from '@/lib/tasks'
7
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
7
8
  import type { BoardTask } from '@/types'
8
9
 
9
10
  function timeAgo(ts: number) {
@@ -30,7 +31,9 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
30
31
  const setCurrentSession = useAppStore((s) => s.setCurrentSession)
31
32
  const setActiveView = useAppStore((s) => s.setActiveView)
32
33
  const [dragging, setDragging] = useState(false)
34
+ const [confirmArchive, setConfirmArchive] = useState(false)
33
35
 
36
+ const tasks = useAppStore((s) => s.tasks)
34
37
  const agent = agents[task.agentId]
35
38
  const project = task.projectId ? projects[task.projectId] : null
36
39
 
@@ -117,7 +120,7 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
117
120
  )}
118
121
  {isBlocked && (
119
122
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-rose-400 shrink-0 mt-0.5">
120
- <title>{`Blocked by ${task.blockedBy?.length} task(s)`}</title>
123
+ <title>{`Blocked by: ${(task.blockedBy || []).map((bid) => tasks[bid]?.title || bid).join(', ')}`}</title>
121
124
  <rect x="3" y="11" width="18" height="11" rx="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" />
122
125
  </svg>
123
126
  )}
@@ -211,6 +214,14 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
211
214
  {task.comments.length}
212
215
  </span>
213
216
  )}
217
+ {Array.isArray(task.blocks) && task.blocks.length > 0 && (
218
+ <span
219
+ className="px-1.5 py-0.5 rounded-[5px] bg-amber-500/10 text-amber-400 text-[10px] font-600"
220
+ title={`Blocks: ${task.blocks.map((bid) => tasks[bid]?.title || bid).join(', ')}`}
221
+ >
222
+ blocks {task.blocks.length}
223
+ </span>
224
+ )}
214
225
 
215
226
  {task.status === 'backlog' && (
216
227
  <button
@@ -236,7 +247,8 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
236
247
 
237
248
  {(task.status === 'completed' || task.status === 'failed') && !task.sessionId && (
238
249
  <button
239
- onClick={handleArchive}
250
+ onClick={(e) => { e.stopPropagation(); setConfirmArchive(true) }}
251
+ aria-label="Archive task"
240
252
  className="ml-auto px-2.5 py-1 rounded-[8px] text-[11px] font-600 bg-white/[0.04] text-text-3 border-none cursor-pointer
241
253
  opacity-0 group-hover:opacity-100 transition-opacity hover:bg-white/[0.08]"
242
254
  style={{ fontFamily: 'inherit' }}
@@ -304,6 +316,14 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
304
316
  ))}
305
317
  </div>
306
318
  )}
319
+ <ConfirmDialog
320
+ open={confirmArchive}
321
+ title="Archive Task"
322
+ message={`Archive "${task.title}"? You can view archived tasks later.`}
323
+ confirmLabel="Archive"
324
+ onConfirm={() => { setConfirmArchive(false); handleArchive({ stopPropagation: () => {} } as React.MouseEvent) }}
325
+ onCancel={() => setConfirmArchive(false)}
326
+ />
307
327
  </div>
308
328
  )
309
329
  }
@@ -54,6 +54,8 @@ export function TaskSheet() {
54
54
  const [tags, setTags] = useState<string[]>([])
55
55
  const [tagInput, setTagInput] = useState('')
56
56
  const [blockedBy, setBlockedBy] = useState<string[]>([])
57
+ const [depSearch, setDepSearch] = useState('')
58
+ const [depError, setDepError] = useState<string | null>(null)
57
59
  const [dueAt, setDueAt] = useState<string>('')
58
60
  const [customFields, setCustomFields] = useState<Record<string, string | number | boolean>>({})
59
61
  const [priority, setPriority] = useState<'low' | 'medium' | 'high' | 'critical' | ''>('')
@@ -76,6 +78,8 @@ export function TaskSheet() {
76
78
  setFile(editing.file || null)
77
79
  setTags(editing.tags || [])
78
80
  setBlockedBy(editing.blockedBy || [])
81
+ setDepSearch('')
82
+ setDepError(null)
79
83
  setDueAt(editing.dueAt ? new Date(editing.dueAt).toISOString().slice(0, 10) : '')
80
84
  setCustomFields(editing.customFields || {})
81
85
  setPriority(editing.priority || '')
@@ -89,6 +93,8 @@ export function TaskSheet() {
89
93
  setFile(null)
90
94
  setTags([])
91
95
  setBlockedBy([])
96
+ setDepSearch('')
97
+ setDepError(null)
92
98
  setDueAt('')
93
99
  setCustomFields({})
94
100
  setPriority('')
@@ -119,11 +125,25 @@ export function TaskSheet() {
119
125
  customFields: Object.keys(customFields).length > 0 ? customFields : undefined,
120
126
  priority: priority || undefined,
121
127
  } as Partial<BoardTask> & { title: string; description: string; agentId: string }
122
- if (editing) {
123
- await updateTask(editing.id, payload)
124
- } else {
125
- await createTask(payload)
128
+ try {
129
+ if (editing) {
130
+ const res = await updateTask(editing.id, payload)
131
+ if (res && typeof res === 'object' && 'error' in res) {
132
+ setDepError(String((res as unknown as Record<string, unknown>).error))
133
+ return
134
+ }
135
+ } else {
136
+ const res = await createTask(payload)
137
+ if (res && typeof res === 'object' && 'error' in res) {
138
+ setDepError(String((res as unknown as Record<string, unknown>).error))
139
+ return
140
+ }
141
+ }
142
+ } catch (err: unknown) {
143
+ setDepError(err instanceof Error ? err.message : String(err))
144
+ return
126
145
  }
146
+ setDepError(null)
127
147
  await loadTasks()
128
148
  onClose()
129
149
  }
@@ -244,7 +264,7 @@ export function TaskSheet() {
244
264
  <div className="mb-8">
245
265
  <SectionLabel>Agent</SectionLabel>
246
266
  <div className="flex items-center gap-2.5 px-4 py-3 rounded-[14px] border border-white/[0.06] bg-surface">
247
- <AgentAvatar seed={taskAgent.avatarSeed || null} name={taskAgent.name} size={24} />
267
+ <AgentAvatar seed={taskAgent.avatarSeed || null} avatarUrl={taskAgent.avatarUrl} name={taskAgent.name} size={24} />
248
268
  <span className="text-[14px] font-600 text-text">{taskAgent.name}</span>
249
269
  </div>
250
270
  </div>
@@ -366,6 +386,26 @@ export function TaskSheet() {
366
386
  </div>
367
387
  )}
368
388
 
389
+ {Array.isArray(editing.outputFiles) && editing.outputFiles.length > 0 && (
390
+ <div className="mb-8">
391
+ <SectionLabel>Output Files</SectionLabel>
392
+ <div className="flex flex-col gap-1.5">
393
+ {editing.outputFiles.map((fileRef) => (
394
+ <code key={fileRef} className="text-[12px] text-text-3 font-mono break-all">
395
+ {fileRef}
396
+ </code>
397
+ ))}
398
+ </div>
399
+ </div>
400
+ )}
401
+
402
+ {editing.completionReportPath && (
403
+ <div className="mb-8">
404
+ <SectionLabel>Task Report</SectionLabel>
405
+ <code className="text-[12px] text-text-3 font-mono break-all">{editing.completionReportPath}</code>
406
+ </div>
407
+ )}
408
+
369
409
  {/* CLI Sessions */}
370
410
  {(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.cliResumeId) && (
371
411
  <div className="mb-8">
@@ -663,18 +703,73 @@ export function TaskSheet() {
663
703
  {/* Dependencies */}
664
704
  <div className="mb-8">
665
705
  <SectionLabel>Blocked By <span className="normal-case tracking-normal font-normal text-text-3">(tasks that must complete first)</span></SectionLabel>
666
- <select
667
- multiple
668
- aria-label="Assign agents"
669
- value={blockedBy}
670
- onChange={(e) => setBlockedBy(Array.from(e.target.selectedOptions, (o) => o.value))}
671
- className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[13px] outline-none min-h-[80px] focus-glow"
672
- style={{ fontFamily: 'inherit' }}
673
- >
674
- {Object.values(tasks)
675
- .filter((t) => t.id !== editingId && t.status !== 'archived')
676
- .map((t) => <option key={t.id} value={t.id}>{t.title} ({t.status})</option>)}
677
- </select>
706
+ {/* Selected blockers as removable chips */}
707
+ {blockedBy.length > 0 && (
708
+ <div className="flex flex-wrap gap-1.5 mb-3">
709
+ {blockedBy.map((bid) => {
710
+ const bt = tasks[bid]
711
+ return (
712
+ <span key={bid} className="inline-flex items-center gap-1 px-2.5 py-1 rounded-[8px] bg-rose-500/10 text-rose-400 text-[12px] font-600">
713
+ {bt ? bt.title : bid}
714
+ <button
715
+ onClick={() => setBlockedBy((prev) => prev.filter((b) => b !== bid))}
716
+ className="text-rose-400/60 hover:text-rose-400 cursor-pointer border-none bg-transparent p-0 text-[14px] leading-none"
717
+ >
718
+ &times;
719
+ </button>
720
+ </span>
721
+ )
722
+ })}
723
+ </div>
724
+ )}
725
+ {/* Searchable dropdown for adding dependencies */}
726
+ <div className="relative">
727
+ <input
728
+ type="text"
729
+ value={depSearch}
730
+ onChange={(e) => setDepSearch(e.target.value)}
731
+ placeholder="Search tasks to add as dependency..."
732
+ className={inputClass}
733
+ style={{ fontFamily: 'inherit' }}
734
+ />
735
+ {depSearch.trim() && (
736
+ <div className="absolute z-20 top-full left-0 right-0 mt-1 max-h-[200px] overflow-y-auto rounded-[12px] border border-white/[0.08] bg-surface shadow-xl">
737
+ {Object.values(tasks)
738
+ .filter((t) =>
739
+ t.id !== editingId &&
740
+ t.status !== 'archived' &&
741
+ !blockedBy.includes(t.id) &&
742
+ t.title.toLowerCase().includes(depSearch.toLowerCase())
743
+ )
744
+ .slice(0, 10)
745
+ .map((t) => (
746
+ <button
747
+ key={t.id}
748
+ onClick={() => {
749
+ setBlockedBy((prev) => [...prev, t.id])
750
+ setDepSearch('')
751
+ }}
752
+ className="w-full text-left px-4 py-2.5 text-[13px] text-text-2 hover:bg-surface-2 cursor-pointer border-none bg-transparent transition-colors flex items-center gap-2"
753
+ style={{ fontFamily: 'inherit' }}
754
+ >
755
+ <span className="flex-1 truncate">{t.title}</span>
756
+ <span className="text-[10px] text-text-3 shrink-0">({t.status})</span>
757
+ </button>
758
+ ))}
759
+ {Object.values(tasks).filter((t) =>
760
+ t.id !== editingId &&
761
+ t.status !== 'archived' &&
762
+ !blockedBy.includes(t.id) &&
763
+ t.title.toLowerCase().includes(depSearch.toLowerCase())
764
+ ).length === 0 && (
765
+ <div className="px-4 py-3 text-[13px] text-text-3">No matching tasks</div>
766
+ )}
767
+ </div>
768
+ )}
769
+ </div>
770
+ {depError && (
771
+ <p className="mt-2 text-[12px] text-red-400 font-600">{depError}</p>
772
+ )}
678
773
  {editing && Array.isArray(editing.blocks) && editing.blocks.length > 0 && (
679
774
  <div className="mt-3">
680
775
  <span className="text-[11px] font-600 text-text-3 uppercase tracking-[0.06em]">Blocks:</span>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useEffect, useState, useCallback } from 'react'
4
4
  import {
5
- LineChart, Line, BarChart, Bar, PieChart, Pie, Cell,
5
+ LineChart, Line, BarChart, Bar, Cell,
6
6
  XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
7
7
  } from 'recharts'
8
8
  import { useAppStore } from '@/stores/use-app-store'
@@ -32,7 +32,7 @@ interface UsageResponse {
32
32
  records: unknown[]
33
33
  totalTokens: number
34
34
  totalCost: number
35
- byAgent: Record<string, { tokens: number; cost: number }>
35
+ byAgent: Record<string, { name: string; cost: number; tokens: number; count: number }>
36
36
  byProvider: Record<string, { tokens: number; cost: number }>
37
37
  timeSeries: TimePoint[]
38
38
  providerHealth?: Record<string, ProviderHealthEntry>
@@ -171,8 +171,8 @@ export function MetricsDashboard() {
171
171
  const agentData = Object.entries(data?.byAgent ?? {})
172
172
  .sort((a, b) => b[1].cost - a[1].cost)
173
173
  .slice(0, 10)
174
- .map(([name, v]) => ({
175
- name: name.length > 12 ? name.slice(0, 12) + '…' : name,
174
+ .map(([_id, v]) => ({
175
+ name: v.name.length > 16 ? v.name.slice(0, 16) + '…' : v.name,
176
176
  cost: Math.round(v.cost * 10000) / 10000,
177
177
  }))
178
178
 
@@ -270,32 +270,20 @@ export function MetricsDashboard() {
270
270
  )}
271
271
  </ChartCard>
272
272
 
273
- <ChartCard title="Cost by Session">
273
+ <ChartCard title="Agent Breakdown">
274
274
  {agentData.length > 0 ? (
275
275
  <ResponsiveContainer width="100%" height={280}>
276
- <PieChart>
277
- <Pie
278
- data={agentData}
279
- cx="50%"
280
- cy="50%"
281
- innerRadius={60}
282
- outerRadius={100}
283
- paddingAngle={2}
284
- dataKey="cost"
285
- nameKey="name"
286
- >
276
+ <BarChart data={agentData} layout="vertical" margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
277
+ <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" horizontal={false} />
278
+ <XAxis type="number" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={(v: number) => `$${v}`} />
279
+ <YAxis type="category" dataKey="name" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} width={100} />
280
+ <Tooltip {...tooltipStyle} formatter={(value: number | undefined) => [formatCost(value ?? 0), 'Cost']} />
281
+ <Bar dataKey="cost" radius={[0, 4, 4, 0]}>
287
282
  {agentData.map((_entry, i) => (
288
283
  <Cell key={i} fill={CHART_COLORS[i % CHART_COLORS.length]} />
289
284
  ))}
290
- </Pie>
291
- <Tooltip {...tooltipStyle} formatter={(value: number | undefined) => [formatCost(value ?? 0), 'Cost']} />
292
- <Legend
293
- verticalAlign="bottom"
294
- iconType="circle"
295
- iconSize={8}
296
- formatter={(value: string) => <span style={{ color: '#a0a0b0', fontSize: 11 }}>{value}</span>}
297
- />
298
- </PieChart>
285
+ </Bar>
286
+ </BarChart>
299
287
  </ResponsiveContainer>
300
288
  ) : (
301
289
  <EmptyChart />
@@ -0,0 +1,99 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+ import { api } from '@/lib/api-client'
5
+ import type { WalletTransaction } from '@/types'
6
+
7
+ interface WalletApprovalDialogProps {
8
+ transaction: WalletTransaction
9
+ walletAddress: string
10
+ onClose: () => void
11
+ onResolved: () => void
12
+ }
13
+
14
+ export function WalletApprovalDialog({ transaction, walletAddress, onClose, onResolved }: WalletApprovalDialogProps) {
15
+ const [submitting, setSubmitting] = useState(false)
16
+ const [error, setError] = useState<string | null>(null)
17
+
18
+ const handleDecision = useCallback(async (decision: 'approve' | 'deny') => {
19
+ setSubmitting(true)
20
+ setError(null)
21
+ try {
22
+ await api('POST', `/wallets/${transaction.walletId}/approve`, {
23
+ transactionId: transaction.id,
24
+ decision,
25
+ })
26
+ onResolved()
27
+ onClose()
28
+ } catch (err: unknown) {
29
+ setError(err instanceof Error ? err.message : String(err))
30
+ } finally {
31
+ setSubmitting(false)
32
+ }
33
+ }, [transaction, onResolved, onClose])
34
+
35
+ const amountSol = transaction.amountLamports / 1e9
36
+
37
+ return (
38
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
39
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
40
+ <div className="relative w-full max-w-md rounded-[16px] border border-white/[0.08] bg-surface-1 shadow-2xl p-6 space-y-5">
41
+ <div className="flex items-center gap-2">
42
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400">
43
+ <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" />
44
+ <line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
45
+ </svg>
46
+ <h3 className="font-display text-[15px] font-600 text-text-1">Transaction Approval</h3>
47
+ </div>
48
+
49
+ <div className="p-4 rounded-[12px] bg-black/20 border border-white/[0.06] space-y-3">
50
+ <div className="flex items-center justify-between">
51
+ <span className="text-[11px] text-text-3/70 uppercase tracking-wide">Amount</span>
52
+ <span className="text-[16px] font-600 text-text-1">{amountSol.toFixed(4)} SOL</span>
53
+ </div>
54
+ <div>
55
+ <span className="text-[11px] text-text-3/70 uppercase tracking-wide block mb-1">From</span>
56
+ <code className="text-[10px] text-text-3 font-mono break-all">{walletAddress}</code>
57
+ </div>
58
+ <div>
59
+ <span className="text-[11px] text-text-3/70 uppercase tracking-wide block mb-1">To</span>
60
+ <code className="text-[10px] text-text-3 font-mono break-all">{transaction.toAddress}</code>
61
+ </div>
62
+ {transaction.memo && (
63
+ <div>
64
+ <span className="text-[11px] text-text-3/70 uppercase tracking-wide block mb-1">Reason</span>
65
+ <p className="text-[12px] text-text-2">{transaction.memo}</p>
66
+ </div>
67
+ )}
68
+ </div>
69
+
70
+ <p className="text-[11px] text-amber-400/80">
71
+ Crypto transactions are irreversible. Verify the recipient address carefully.
72
+ </p>
73
+
74
+ {error && <p className="text-[11px] text-red-400">{error}</p>}
75
+
76
+ <div className="flex gap-3">
77
+ <button
78
+ type="button"
79
+ onClick={() => handleDecision('deny')}
80
+ disabled={submitting}
81
+ className="flex-1 px-4 py-2.5 rounded-[10px] border border-white/[0.08] bg-surface text-text-3 text-[12px] font-600 hover:text-red-400 hover:border-red-400/30 transition-colors cursor-pointer disabled:opacity-50"
82
+ style={{ fontFamily: 'inherit' }}
83
+ >
84
+ Deny
85
+ </button>
86
+ <button
87
+ type="button"
88
+ onClick={() => handleDecision('approve')}
89
+ disabled={submitting}
90
+ className="flex-1 px-4 py-2.5 rounded-[10px] bg-accent text-white text-[12px] font-600 hover:brightness-110 transition-all cursor-pointer disabled:opacity-50"
91
+ style={{ fontFamily: 'inherit' }}
92
+ >
93
+ {submitting ? 'Processing...' : 'Approve & Send'}
94
+ </button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ )
99
+ }