@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.
- package/README.md +24 -6
- package/package.json +1 -1
- package/src/app/api/agents/route.ts +1 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- package/src/app/api/memory/graph/route.ts +46 -0
- package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/route.ts +5 -0
- package/src/app/api/tasks/route.ts +2 -0
- package/src/app/api/usage/route.ts +9 -2
- package/src/cli/index.js +24 -0
- package/src/components/agents/agent-sheet.tsx +27 -6
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/chat/activity-moment.tsx +2 -0
- package/src/components/chat/checkpoint-timeline.tsx +112 -0
- package/src/components/chat/message-list.tsx +19 -3
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- package/src/components/connectors/connector-sheet.tsx +8 -1
- package/src/components/home/home-view.tsx +39 -15
- package/src/components/layout/app-layout.tsx +18 -2
- package/src/components/memory/memory-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +9 -2
- package/src/components/shared/hint-tip.tsx +31 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -4
- package/src/components/tasks/approvals-panel.tsx +120 -0
- package/src/components/usage/metrics-dashboard.tsx +25 -3
- package/src/lib/server/chat-execution.ts +96 -12
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/daemon-state.ts +70 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -0
- package/src/lib/server/main-agent-loop.ts +114 -15
- package/src/lib/server/memory-db.ts +18 -7
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +3 -0
- package/src/lib/server/plugins.ts +44 -22
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +27 -0
- package/src/lib/server/session-run-manager.ts +21 -1
- package/src/lib/server/session-tools/http.ts +19 -9
- package/src/lib/server/session-tools/index.ts +34 -0
- package/src/lib/server/session-tools/memory.ts +39 -11
- package/src/lib/server/session-tools/schedule.ts +43 -0
- package/src/lib/server/session-tools/web.ts +35 -11
- package/src/lib/server/storage.ts +12 -0
- package/src/lib/server/stream-agent-chat.ts +57 -8
- package/src/lib/server/tool-capability-policy.ts +1 -0
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/view-routes.ts +1 -0
- 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
|
|
109
|
-
|
|
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
|
-
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
<
|
|
122
|
-
|
|
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={
|
|
147
|
-
onClick={() =>
|
|
148
|
-
className=
|
|
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
|
-
{
|
|
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
|
-
{
|
|
176
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
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
|
-
<
|
|
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">
|
|
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 }) {
|