@swarmclawai/swarmclaw 0.6.7 → 0.6.8

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 (73) hide show
  1. package/README.md +24 -6
  2. package/package.json +1 -1
  3. package/src/app/api/agents/route.ts +1 -0
  4. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  5. package/src/app/api/eval/run/route.ts +37 -0
  6. package/src/app/api/eval/scenarios/route.ts +24 -0
  7. package/src/app/api/eval/suite/route.ts +29 -0
  8. package/src/app/api/memory/graph/route.ts +46 -0
  9. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  10. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  11. package/src/app/api/souls/[id]/route.ts +65 -0
  12. package/src/app/api/souls/route.ts +70 -0
  13. package/src/app/api/tasks/[id]/route.ts +5 -0
  14. package/src/app/api/tasks/route.ts +2 -0
  15. package/src/app/api/usage/route.ts +9 -2
  16. package/src/cli/index.js +24 -0
  17. package/src/components/agents/agent-sheet.tsx +27 -6
  18. package/src/components/agents/soul-library-picker.tsx +84 -13
  19. package/src/components/chat/activity-moment.tsx +2 -0
  20. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  21. package/src/components/chat/message-list.tsx +19 -3
  22. package/src/components/chat/session-debug-panel.tsx +106 -84
  23. package/src/components/chat/task-approval-card.tsx +78 -0
  24. package/src/components/chat/tool-call-bubble.tsx +3 -0
  25. package/src/components/connectors/connector-sheet.tsx +8 -1
  26. package/src/components/home/home-view.tsx +39 -15
  27. package/src/components/layout/app-layout.tsx +18 -2
  28. package/src/components/memory/memory-browser.tsx +73 -45
  29. package/src/components/memory/memory-graph-view.tsx +203 -0
  30. package/src/components/plugins/plugin-list.tsx +1 -1
  31. package/src/components/schedules/schedule-sheet.tsx +9 -2
  32. package/src/components/shared/hint-tip.tsx +31 -0
  33. package/src/components/shared/settings/section-runtime-loop.tsx +5 -4
  34. package/src/components/tasks/approvals-panel.tsx +120 -0
  35. package/src/components/usage/metrics-dashboard.tsx +25 -3
  36. package/src/lib/server/chat-execution.ts +96 -12
  37. package/src/lib/server/chatroom-helpers.ts +63 -5
  38. package/src/lib/server/chatroom-orchestration.ts +74 -0
  39. package/src/lib/server/context-manager.ts +132 -50
  40. package/src/lib/server/daemon-state.ts +70 -1
  41. package/src/lib/server/eval/runner.ts +126 -0
  42. package/src/lib/server/eval/scenarios.ts +218 -0
  43. package/src/lib/server/eval/scorer.ts +96 -0
  44. package/src/lib/server/eval/store.ts +37 -0
  45. package/src/lib/server/eval/types.ts +48 -0
  46. package/src/lib/server/execution-log.ts +12 -8
  47. package/src/lib/server/guardian.ts +34 -0
  48. package/src/lib/server/heartbeat-service.ts +53 -1
  49. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  50. package/src/lib/server/link-understanding.ts +55 -0
  51. package/src/lib/server/main-agent-loop.ts +114 -15
  52. package/src/lib/server/memory-db.ts +18 -7
  53. package/src/lib/server/mmr.ts +73 -0
  54. package/src/lib/server/orchestrator-lg.ts +3 -0
  55. package/src/lib/server/plugins.ts +44 -22
  56. package/src/lib/server/query-expansion.ts +57 -0
  57. package/src/lib/server/queue.ts +27 -0
  58. package/src/lib/server/session-run-manager.ts +21 -1
  59. package/src/lib/server/session-tools/http.ts +19 -9
  60. package/src/lib/server/session-tools/index.ts +34 -0
  61. package/src/lib/server/session-tools/memory.ts +39 -11
  62. package/src/lib/server/session-tools/schedule.ts +43 -0
  63. package/src/lib/server/session-tools/web.ts +35 -11
  64. package/src/lib/server/storage.ts +12 -0
  65. package/src/lib/server/stream-agent-chat.ts +57 -8
  66. package/src/lib/server/tool-capability-policy.ts +1 -0
  67. package/src/lib/server/tool-retry.ts +62 -0
  68. package/src/lib/server/transcript-repair.ts +72 -0
  69. package/src/lib/setup-defaults.ts +1 -0
  70. package/src/lib/tool-definitions.ts +1 -0
  71. package/src/lib/validation/schemas.ts +1 -0
  72. package/src/lib/view-routes.ts +1 -0
  73. package/src/types/index.ts +34 -3
@@ -3,6 +3,8 @@
3
3
  import { useState, useEffect } from 'react'
4
4
  import type { Message } from '@/types'
5
5
  import { IconButton } from '@/components/shared/icon-button'
6
+ import { CheckpointTimeline } from './checkpoint-timeline'
7
+ import { useAppStore } from '@/stores/use-app-store'
6
8
 
7
9
  interface Props {
8
10
  messages: Message[]
@@ -60,23 +62,16 @@ const TYPE_COLORS: Record<EventType, string> = {
60
62
  tool_call: '#8B5CF6',
61
63
  }
62
64
 
63
- const TYPE_ICONS: Record<EventType, string> = {
64
- user: 'U',
65
- assistant: 'AI',
66
- delegation: 'D',
67
- agent_result: 'R',
68
- system: 'S',
69
- error: '!',
70
- tool_call: 'T',
71
- }
72
-
73
65
  function fmtTime(ts: number) {
74
66
  return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
75
67
  }
76
68
 
77
69
  export function SessionDebugPanel({ messages, open, onClose }: Props) {
70
+ const [tab, setTab] = useState<'log' | 'timeline'>('log')
78
71
  const [filter, setFilter] = useState<EventType | 'all'>('all')
79
72
  const [expandedIdx, setExpandedIdx] = useState<number | null>(null)
73
+
74
+ const currentSessionId = useAppStore((s) => s.currentSessionId)
80
75
 
81
76
  const events = messages.map(classifyMessage)
82
77
  const filtered = filter === 'all' ? events : events.filter((e) => e.type === filter)
@@ -105,8 +100,23 @@ export function SessionDebugPanel({ messages, open, onClose }: Props) {
105
100
  <path d="M18 20V4" />
106
101
  <path d="M6 20v-4" />
107
102
  </svg>
108
- <span className="font-display text-[16px] font-600 tracking-[-0.02em] flex-1">Session Debug</span>
109
- <span className="text-[12px] text-text-3 font-mono">{events.length} events</span>
103
+ <span className="font-display text-[16px] font-600 tracking-[-0.02em] flex-1">Session X-Ray</span>
104
+
105
+ <div className="flex bg-white/[0.04] p-0.5 rounded-[8px] mr-2">
106
+ <button
107
+ onClick={() => setTab('log')}
108
+ className={`px-3 py-1 rounded-[6px] text-[11px] font-600 transition-all ${tab === 'log' ? 'bg-white/[0.08] text-text shadow-sm' : 'text-text-3 hover:text-text-2'}`}
109
+ >
110
+ Event Log
111
+ </button>
112
+ <button
113
+ onClick={() => setTab('timeline')}
114
+ className={`px-3 py-1 rounded-[6px] text-[11px] font-600 transition-all ${tab === 'timeline' ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:text-text-2'}`}
115
+ >
116
+ Time Travel
117
+ </button>
118
+ </div>
119
+
110
120
  <IconButton onClick={onClose} aria-label="Close debug panel">
111
121
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
112
122
  <line x1="18" y1="6" x2="6" y2="18" />
@@ -115,82 +125,94 @@ export function SessionDebugPanel({ messages, open, onClose }: Props) {
115
125
  </IconButton>
116
126
  </div>
117
127
 
118
- {/* Filters */}
119
- <div className="flex gap-2 px-5 py-3 border-b border-white/[0.04] overflow-x-auto shrink-0">
120
- {filters.map((f) => (
121
- <button
122
- key={f.id}
123
- onClick={() => setFilter(f.id)}
124
- className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border whitespace-nowrap
125
- ${filter === f.id
126
- ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
127
- : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
128
- style={{ fontFamily: 'inherit' }}
129
- >
130
- {f.label}
131
- </button>
132
- ))}
133
- </div>
134
-
135
- {/* Event timeline */}
136
- <div className="flex-1 overflow-y-auto px-5 py-4">
137
- <div className="relative">
138
- {/* Timeline line */}
139
- <div className="absolute left-[15px] top-0 bottom-0 w-px bg-white/[0.06]" />
140
-
141
- {filtered.map((event, i) => {
142
- const color = TYPE_COLORS[event.type]
143
- const expanded = expandedIdx === i
144
- return (
128
+ {tab === 'log' ? (
129
+ <>
130
+ {/* Filters */}
131
+ <div className="flex gap-2 px-5 py-3 border-b border-white/[0.04] overflow-x-auto shrink-0">
132
+ {filters.map((f) => (
145
133
  <button
146
- key={i}
147
- onClick={() => setExpandedIdx(expanded ? null : i)}
148
- className="w-full text-left relative pl-10 pb-4 group cursor-pointer"
134
+ key={f.id}
135
+ onClick={() => setFilter(f.id)}
136
+ className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border whitespace-nowrap
137
+ ${filter === f.id
138
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
139
+ : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
140
+ style={{ fontFamily: 'inherit' }}
149
141
  >
150
- {/* Dot */}
151
- <div
152
- className="absolute left-[10px] top-1 w-[11px] h-[11px] rounded-full border-2"
153
- style={{ borderColor: color, backgroundColor: expanded ? color : 'transparent' }}
154
- />
155
-
156
- {/* Content */}
157
- <div className="flex items-center gap-2 mb-0.5">
158
- <span className="text-[11px] font-700 uppercase tracking-wider" style={{ color }}>
159
- {event.label}
160
- </span>
161
- <span className="text-[10px] text-text-3/70 font-mono">{fmtTime(event.time)}</span>
162
- </div>
163
-
164
- <p className={`text-[12px] text-text-3 leading-[1.5] ${expanded ? 'whitespace-pre-wrap' : 'line-clamp-2'}`}>
165
- {event.detail}
166
- </p>
167
-
168
- {!expanded && event.detail.length > 150 && (
169
- <span className="text-[11px] text-accent-bright/60 mt-1 inline-block">click to expand</span>
170
- )}
142
+ {f.label}
171
143
  </button>
172
- )
173
- })}
174
-
175
- {filtered.length === 0 && (
176
- <p className="text-center text-[13px] text-text-3 py-12">No events matching filter</p>
177
- )}
144
+ ))}
145
+ </div>
146
+
147
+ {/* Event timeline */}
148
+ <div className="flex-1 overflow-y-auto px-5 py-4">
149
+ <div className="relative">
150
+ {/* Timeline line */}
151
+ <div className="absolute left-[15px] top-0 bottom-0 w-px bg-white/[0.06]" />
152
+
153
+ {filtered.map((event, i) => {
154
+ const color = TYPE_COLORS[event.type]
155
+ const expanded = expandedIdx === i
156
+ return (
157
+ <button
158
+ key={i}
159
+ onClick={() => setExpandedIdx(expanded ? null : i)}
160
+ className="w-full text-left relative pl-10 pb-4 group cursor-pointer"
161
+ >
162
+ {/* Dot */}
163
+ <div
164
+ className="absolute left-[10px] top-1 w-[11px] h-[11px] rounded-full border-2"
165
+ style={{ borderColor: color, backgroundColor: expanded ? color : 'transparent' }}
166
+ />
167
+
168
+ {/* Content */}
169
+ <div className="flex items-center gap-2 mb-0.5">
170
+ <span className="text-[11px] font-700 uppercase tracking-wider" style={{ color }}>
171
+ {event.label}
172
+ </span>
173
+ <span className="text-[10px] text-text-3/70 font-mono">{fmtTime(event.time)}</span>
174
+ </div>
175
+
176
+ <p className={`text-[12px] text-text-3 leading-[1.5] ${expanded ? 'whitespace-pre-wrap' : 'line-clamp-2'}`}>
177
+ {event.detail}
178
+ </p>
179
+
180
+ {!expanded && event.detail.length > 150 && (
181
+ <span className="text-[11px] text-accent-bright/60 mt-1 inline-block">click to expand</span>
182
+ )}
183
+ </button>
184
+ )
185
+ })}
186
+
187
+ {filtered.length === 0 && (
188
+ <p className="text-center text-[13px] text-text-3 py-12">No events matching filter</p>
189
+ )}
190
+ </div>
191
+ </div>
192
+
193
+ {/* Stats bar */}
194
+ <div className="flex items-center gap-4 px-5 py-3 border-t border-white/[0.06] shrink-0">
195
+ {(['delegation', 'agent_result', 'error'] as EventType[]).map((type) => {
196
+ const count = events.filter((e) => e.type === type).length
197
+ if (!count) return null
198
+ return (
199
+ <span key={type} className="flex items-center gap-1.5 text-[11px] font-mono" style={{ color: TYPE_COLORS[type] }}>
200
+ <span className="w-2 h-2 rounded-full" style={{ backgroundColor: TYPE_COLORS[type] }} />
201
+ {count} {type === 'delegation' ? 'delegations' : type === 'agent_result' ? 'results' : 'errors'}
202
+ </span>
203
+ )
204
+ })}
205
+ </div>
206
+ </>
207
+ ) : (
208
+ <div className="flex-1 overflow-y-auto">
209
+ {currentSessionId ? (
210
+ <CheckpointTimeline sessionId={currentSessionId} />
211
+ ) : (
212
+ <div className="p-12 text-center text-text-3">No active session</div>
213
+ )}
178
214
  </div>
179
- </div>
180
-
181
- {/* Stats bar */}
182
- <div className="flex items-center gap-4 px-5 py-3 border-t border-white/[0.06] shrink-0">
183
- {(['delegation', 'agent_result', 'error'] as EventType[]).map((type) => {
184
- const count = events.filter((e) => e.type === type).length
185
- if (!count) return null
186
- return (
187
- <span key={type} className="flex items-center gap-1.5 text-[11px] font-mono" style={{ color: TYPE_COLORS[type] }}>
188
- <span className="w-2 h-2 rounded-full" style={{ backgroundColor: TYPE_COLORS[type] }} />
189
- {count} {type === 'delegation' ? 'delegations' : type === 'agent_result' ? 'results' : 'errors'}
190
- </span>
191
- )
192
- })}
193
- </div>
215
+ )}
194
216
  </div>
195
217
  )
196
218
  }
@@ -0,0 +1,78 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import type { BoardTask } from '@/types'
5
+ import { api } from '@/lib/api-client'
6
+ import { useAppStore } from '@/stores/use-app-store'
7
+ import { toast } from 'sonner'
8
+
9
+ interface Props {
10
+ task: BoardTask
11
+ }
12
+
13
+ export function TaskApprovalCard({ task }: Props) {
14
+ const [resolving, setResolving] = useState(false)
15
+ const loadTasks = useAppStore((s) => s.loadTasks)
16
+ const loadSessions = useAppStore((s) => s.loadSessions)
17
+
18
+ const handleResolve = async (approved: boolean) => {
19
+ setResolving(true)
20
+ try {
21
+ await api('POST', `/tasks/${task.id}/approve`, { approved })
22
+ toast.success(approved ? 'Tool execution approved' : 'Tool execution rejected')
23
+ await loadTasks()
24
+ await loadSessions()
25
+ } catch (err: unknown) {
26
+ toast.error(err instanceof Error ? err.message : 'Failed to submit decision')
27
+ } finally {
28
+ setResolving(false)
29
+ }
30
+ }
31
+
32
+ if (!task.pendingApproval) return null
33
+
34
+ const { toolName, args } = task.pendingApproval
35
+
36
+ return (
37
+ <div className="my-2 rounded-[12px] border border-amber-500/20 bg-amber-500/[0.04] p-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
38
+ <div className="flex items-center gap-2 mb-3">
39
+ <div className="w-5 h-5 rounded-full bg-amber-500/20 flex items-center justify-center">
40
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-amber-400">
41
+ <path d="M12 9v2m0 4h.01" />
42
+ <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" />
43
+ </svg>
44
+ </div>
45
+ <span className="text-[12px] font-700 text-amber-400 uppercase tracking-wider">Orchestration Intervention Required</span>
46
+ </div>
47
+
48
+ <p className="text-[13px] text-text-2 mb-3 font-500">
49
+ Agent requested to use <span className="text-amber-400 font-600 font-mono">{toolName}</span>:
50
+ </p>
51
+
52
+ <div className="bg-black/30 rounded-[10px] border border-white/[0.04] p-3 mb-4 overflow-x-auto max-h-[200px] overflow-y-auto">
53
+ <pre className="text-[11px] font-mono text-text-2/80 leading-relaxed whitespace-pre-wrap break-all">
54
+ {JSON.stringify(args, null, 2)}
55
+ </pre>
56
+ </div>
57
+
58
+ <div className="flex items-center gap-2">
59
+ <button
60
+ onClick={() => handleResolve(true)}
61
+ disabled={resolving}
62
+ className="flex-1 px-4 py-2 rounded-[10px] bg-emerald-500 text-[#000] text-[12px] font-700 hover:brightness-110 active:scale-[0.98] transition-all disabled:opacity-50"
63
+ style={{ fontFamily: 'inherit' }}
64
+ >
65
+ {resolving ? 'Applying...' : 'Approve'}
66
+ </button>
67
+ <button
68
+ onClick={() => handleResolve(false)}
69
+ disabled={resolving}
70
+ className="px-4 py-2 rounded-[10px] bg-white/[0.04] border border-white/[0.08] text-text-3 text-[12px] font-600 hover:bg-white/[0.08] active:scale-[0.98] transition-all disabled:opacity-50"
71
+ style={{ fontFamily: 'inherit' }}
72
+ >
73
+ Reject
74
+ </button>
75
+ </div>
76
+ </div>
77
+ )
78
+ }
@@ -29,6 +29,7 @@ const TOOL_COLORS: Record<string, string> = {
29
29
  search_history_tool: '#8B5CF6',
30
30
  manage_tasks: '#EC4899',
31
31
  manage_schedules: '#EC4899',
32
+ schedule_wake: '#F59E0B',
32
33
  manage_agents: '#EC4899',
33
34
  manage_skills: '#EC4899',
34
35
  manage_documents: '#EC4899',
@@ -81,6 +82,7 @@ export const TOOL_LABELS: Record<string, string> = {
81
82
  search_history_tool: 'Search History',
82
83
  manage_tasks: 'Tasks',
83
84
  manage_schedules: 'Schedules',
85
+ schedule_wake: 'Set Reminder',
84
86
  manage_agents: 'Agents',
85
87
  manage_skills: 'Skills',
86
88
  manage_documents: 'Documents',
@@ -118,6 +120,7 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
118
120
  search_history_tool: 'Search chat history for relevant prior context',
119
121
  manage_tasks: 'Create, update, and manage tasks on the board',
120
122
  manage_schedules: 'Create and manage cron schedules',
123
+ schedule_wake: 'Set a timer to wake up in this chat later',
121
124
  manage_agents: 'Create and configure other agents',
122
125
  manage_skills: 'Create and manage agent skills',
123
126
  manage_documents: 'Upload and search indexed documents',
@@ -12,6 +12,7 @@ import { AgentPickerList } from '@/components/shared/agent-picker-list'
12
12
  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
+ import { HintTip } from '@/components/shared/hint-tip'
15
16
  import { useChatroomStore } from '@/stores/use-chatroom-store'
16
17
  import { ConnectorHealth } from '@/components/connectors/connector-health'
17
18
 
@@ -643,12 +644,18 @@ export function ConnectorSheet() {
643
644
  {/* Platform-specific config */}
644
645
  {[...platformConfig.configFields, ...COMMON_CONFIG_FIELDS].map((field) => {
645
646
  const isTagField = field.key === 'allowedJids' || field.key === 'channelIds' || field.key === 'chatIds' || field.key === 'allowFrom'
647
+ const fieldHint: Record<string, string> = {
648
+ channelIds: "Find these in your platform's developer settings. Leave empty to allow all channels",
649
+ chatIds: "Find these in your platform's developer settings. Leave empty to allow all channels",
650
+ allowedJids: "Phone numbers in international format (e.g. 447xxx). Leave empty to allow all",
651
+ }
646
652
  if (isTagField) {
647
653
  const tags = (config[field.key] || '').split(',').map((s) => s.trim()).filter(Boolean)
648
654
  return (
649
655
  <div key={field.key} className="mb-6">
650
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
656
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
651
657
  {field.label} <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
658
+ {fieldHint[field.key] && <HintTip text={fieldHint[field.key]} />}
652
659
  </label>
653
660
  {field.help && <p className="text-[12px] text-text-3/60 mb-2">{field.help}</p>}
654
661
  <div className="flex flex-wrap gap-2 mb-2">
@@ -1,12 +1,13 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useMemo, useState } from 'react'
4
- import { AreaChart, Area, ResponsiveContainer } from 'recharts'
4
+ import { AreaChart, Area, ResponsiveContainer, Tooltip } from 'recharts'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
7
7
  import { AgentAvatar } from '@/components/agents/agent-avatar'
8
8
  import { api } from '@/lib/api-client'
9
9
  import type { Agent, Session, ActivityEntry, BoardTask, AppNotification } from '@/types'
10
+ import { HintTip } from '@/components/shared/hint-tip'
10
11
 
11
12
  function timeAgo(ts: number): string {
12
13
  const diff = Date.now() - ts
@@ -86,7 +87,7 @@ export function HomeView() {
86
87
  const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
87
88
  const setMessages = useChatStore((s) => s.setMessages)
88
89
  const [todayCost, setTodayCost] = useState(0)
89
- const [costTrend, setCostTrend] = useState<{ cost: number }[]>([])
90
+ const [costTrend, setCostTrend] = useState<{ cost: number; bucket: string }[]>([])
90
91
 
91
92
  const allAgents = Object.values(agents).filter((a) => !a.trashedAt)
92
93
  const pinnedAgents = allAgents.filter((a) => a.pinned)
@@ -146,11 +147,13 @@ export function HomeView() {
146
147
  void loadSchedules()
147
148
  void loadNotifications()
148
149
  void loadConnectors()
149
- api<{ records: Array<{ estimatedCost: number }>; timeSeries: Array<{ cost: number }> }>('GET', '/usage?range=7d')
150
+ api<{ records: Array<{ estimatedCost: number }>; timeSeries: Array<{ cost: number; bucket: string }> }>('GET', '/usage?range=7d')
150
151
  .then((data) => {
151
- const total = (data.records || []).reduce((s, r) => s + (r.estimatedCost || 0), 0)
152
- setTodayCost(total)
153
- setCostTrend((data.timeSeries || []).map((pt) => ({ cost: pt.cost })))
152
+ const series = (data.timeSeries || []).map((pt: { cost: number; bucket?: string }) => ({ cost: pt.cost, bucket: pt.bucket || '' }))
153
+ setCostTrend(series)
154
+ const todayBucket = new Date().toISOString().slice(0, 10)
155
+ const todayPt = series.find((pt) => pt.bucket === todayBucket)
156
+ setTodayCost(todayPt?.cost || 0)
154
157
  })
155
158
  .catch(() => {})
156
159
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -200,25 +203,43 @@ export function HomeView() {
200
203
 
201
204
  {/* Quick Stats */}
202
205
  <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
203
- <StatCard label="Agents" value={String(agentCount)} />
204
- <StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} />
205
- <StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} />
206
- <StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} />
206
+ <StatCard label="Agents" value={String(agentCount)} hint="Total active agents configured in your dashboard" />
207
+ <StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} hint="Tasks currently running or queued for execution" />
208
+ <StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} hint="Estimated API cost for today across all providers" />
209
+ <StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} hint="Active bridges to chat platforms (Discord, Slack, etc.)" />
207
210
  </div>
208
211
 
209
212
  {/* Cost trend sparkline */}
210
213
  {costTrend.length > 1 && (
211
214
  <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>
215
+ <p className="text-[10px] text-text-3/50 uppercase tracking-wider mb-1 flex items-center gap-1.5">
216
+ 7-day cost trend <HintTip text="Daily API spend over the past week — hover for details" />
217
+ </p>
213
218
  <ResponsiveContainer width="100%" height={60}>
214
- <AreaChart data={costTrend} margin={{ top: 2, right: 0, bottom: 0, left: 0 }}>
219
+ <AreaChart data={costTrend} margin={{ top: 2, right: 0, bottom: 0, left: 0 }} style={{ cursor: 'crosshair' }}>
215
220
  <defs>
216
221
  <linearGradient id="costGrad" x1="0" y1="0" x2="0" y2="1">
217
222
  <stop offset="0%" stopColor="#818CF8" stopOpacity={0.3} />
218
223
  <stop offset="100%" stopColor="#818CF8" stopOpacity={0} />
219
224
  </linearGradient>
220
225
  </defs>
221
- <Area type="monotone" dataKey="cost" stroke="#818CF8" strokeWidth={1.5} fill="url(#costGrad)" dot={false} />
226
+ <Tooltip
227
+ content={({ active, payload }) => {
228
+ if (!active || !payload?.[0]) return null
229
+ const d = payload[0].payload as { cost: number; bucket: string }
230
+ const label = d.bucket
231
+ ? new Date(d.bucket + 'T00:00:00').toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })
232
+ : ''
233
+ return (
234
+ <div className="rounded-[8px] bg-surface border border-white/[0.1] px-3 py-2 shadow-lg">
235
+ <p className="text-[11px] text-text-3/70 m-0">{label}</p>
236
+ <p className="text-[14px] font-600 text-text m-0 mt-0.5">${d.cost.toFixed(4)}</p>
237
+ </div>
238
+ )
239
+ }}
240
+ cursor={{ stroke: '#818CF8', strokeWidth: 1, strokeDasharray: '3 3' }}
241
+ />
242
+ <Area type="monotone" dataKey="cost" stroke="#818CF8" strokeWidth={1.5} fill="url(#costGrad)" dot={false} activeDot={{ r: 3, fill: '#818CF8', stroke: '#818CF8' }} />
222
243
  </AreaChart>
223
244
  </ResponsiveContainer>
224
245
  </div>
@@ -505,10 +526,13 @@ function SectionHeader({ label, onViewAll }: { label: string; onViewAll?: () =>
505
526
  )
506
527
  }
507
528
 
508
- function StatCard({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
529
+ function StatCard({ label, value, accent, hint }: { label: string; value: string; accent?: boolean; hint?: string }) {
509
530
  return (
510
531
  <div className="px-4 py-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]">
511
- <p className="text-[11px] font-600 text-text-3/60 uppercase tracking-wider mb-1">{label}</p>
532
+ <p className="text-[11px] font-600 text-text-3/60 uppercase tracking-wider mb-1 flex items-center gap-1.5">
533
+ {label}
534
+ {hint && <HintTip text={hint} />}
535
+ </p>
512
536
  <p className={`font-display text-[20px] font-700 tracking-[-0.02em] ${accent ? 'text-accent-bright' : 'text-text'}`}>{value}</p>
513
537
  </div>
514
538
  )
@@ -18,6 +18,7 @@ import { MemoryBrowser } from '@/components/memory/memory-browser'
18
18
  import { TaskList } from '@/components/tasks/task-list'
19
19
  import { TaskSheet } from '@/components/tasks/task-sheet'
20
20
  import { TaskBoard } from '@/components/tasks/task-board'
21
+ import { ApprovalsPanel } from '@/components/tasks/approvals-panel'
21
22
  import { SecretsList } from '@/components/secrets/secrets-list'
22
23
  import { SecretSheet } from '@/components/secrets/secret-sheet'
23
24
  import { ProviderList } from '@/components/providers/provider-list'
@@ -369,6 +370,12 @@ export function AppLayout() {
369
370
  <path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2" /><rect x="9" y="3" width="6" height="4" rx="1" /><path d="M9 14l2 2 4-4" />
370
371
  </svg>
371
372
  </NavItem>
373
+ <NavItem view="approvals" label="Approvals" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('approvals')} badge={pendingApprovalCount}>
374
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
375
+ <path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
376
+ <path d="m9 12 2 2 4-4"/>
377
+ </svg>
378
+ </NavItem>
372
379
  <NavItem view="secrets" label="Secrets" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('secrets')}>
373
380
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
374
381
  <rect x="3" y="11" width="18" height="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" />
@@ -548,7 +555,7 @@ export function AppLayout() {
548
555
  >
549
556
  <div className="flex items-center px-5 pt-5 pb-3 shrink-0">
550
557
  <h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em] capitalize flex-1">{activeView}</h2>
551
- {activeView === 'logs' || activeView === 'usage' || activeView === 'runs' ? null : activeView === 'memory' ? (
558
+ {activeView === 'logs' || activeView === 'usage' || activeView === 'runs' || activeView === 'approvals' ? null : activeView === 'memory' ? (
552
559
  <button
553
560
  onClick={() => useAppStore.getState().setMemorySheetOpen(true)}
554
561
  className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 text-accent-bright bg-accent-soft hover:bg-accent-bright/15 transition-all cursor-pointer"
@@ -653,7 +660,7 @@ export function AppLayout() {
653
660
  </button>
654
661
  ))}
655
662
  </div>
656
- {activeView !== 'logs' && activeView !== 'usage' && activeView !== 'runs' && activeView !== 'settings' && (
663
+ {activeView !== 'logs' && activeView !== 'usage' && activeView !== 'runs' && activeView !== 'settings' && activeView !== 'approvals' && (
657
664
  <div className="px-4 py-2.5 shrink-0">
658
665
  <button
659
666
  onClick={() => {
@@ -742,6 +749,8 @@ export function AppLayout() {
742
749
  </div>
743
750
  ) : activeView === 'tasks' && isDesktop ? (
744
751
  <TaskBoard />
752
+ ) : activeView === 'approvals' ? (
753
+ <ApprovalsPanel />
745
754
  ) : activeView === 'memory' ? (
746
755
  <MemoryBrowser />
747
756
  ) : activeView === 'activity' ? (
@@ -900,6 +909,7 @@ const VIEW_DESCRIPTIONS: Record<AppView, string> = {
900
909
  schedules: 'Automated task schedules',
901
910
  memory: 'Long-term agent memory store',
902
911
  tasks: 'Task board for orchestrator jobs',
912
+ approvals: 'Pending tool execution approvals',
903
913
  secrets: 'API keys & credentials for orchestrators',
904
914
  providers: 'LLM providers & custom endpoints',
905
915
  skills: 'Reusable instruction sets for agents',
@@ -1038,6 +1048,12 @@ const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents' | 'home'>, { icon: str
1038
1048
  description: 'Agent crypto wallets for autonomous financial operations on Solana.',
1039
1049
  features: ['Create Solana wallets for agents', 'Per-transaction and daily spending limits', 'User approval for transactions', 'Balance tracking and transaction history'],
1040
1050
  },
1051
+ approvals: {
1052
+ icon: 'check-circle',
1053
+ title: 'Approvals',
1054
+ description: 'Review and approve pending tool executions from agents.',
1055
+ features: ['Review tool calls before execution', 'Approve or reject agent actions', 'Full context for each pending approval'],
1056
+ },
1041
1057
  }
1042
1058
 
1043
1059
  function ViewEmptyState({ view }: { view: AppView }) {