@harbinger-ai/harbinger 0.1.2 → 0.1.3
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/lib/chat/actions.js +397 -0
- package/lib/chat/components/agents-page.js +545 -0
- package/lib/chat/components/agents-page.jsx +571 -0
- package/lib/chat/components/app-sidebar.js +17 -1
- package/lib/chat/components/app-sidebar.jsx +19 -1
- package/lib/chat/components/icons.js +40 -0
- package/lib/chat/components/icons.jsx +42 -0
- package/lib/chat/components/index.js +2 -0
- package/lib/chat/components/mcp-page.js +383 -55
- package/lib/chat/components/mcp-page.jsx +404 -101
- package/lib/chat/components/settings-layout.js +3 -2
- package/lib/chat/components/settings-layout.jsx +2 -1
- package/lib/chat/components/settings-providers-page.js +337 -0
- package/lib/chat/components/settings-providers-page.jsx +410 -0
- package/lib/chat/components/settings-secrets-page.js +91 -66
- package/lib/chat/components/settings-secrets-page.jsx +83 -72
- package/lib/mcp/actions.js +120 -0
- package/lib/mcp/registry.js +164 -0
- package/package.json +1 -1
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
+
import { UsersIcon, RefreshIcon, SpinnerIcon, ChevronDownIcon, PlusIcon, PencilIcon, CheckIcon, XIcon, SearchIcon } from './icons.js';
|
|
6
|
+
import { getAgentProfilesWithStatus, getAgentProfile, updateAgentFile, createAgent, createAgentJob } from '../actions.js';
|
|
7
|
+
|
|
8
|
+
// ─── Agent Card ──────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function AgentCard({ agent, onViewProfile, onAssignTask, index }) {
|
|
11
|
+
const codename = agent.codename || agent.name || agent.id;
|
|
12
|
+
const initial = codename.charAt(0).toUpperCase();
|
|
13
|
+
const isActive = agent.status === 'active';
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<motion.div
|
|
17
|
+
initial={{ opacity: 0, y: 8 }}
|
|
18
|
+
animate={{ opacity: 1, y: 0 }}
|
|
19
|
+
transition={{ duration: 0.25, delay: index * 0.04 }}
|
|
20
|
+
className={`rounded-lg border bg-[--card] transition-all hover:border-[--cyan]/20 ${
|
|
21
|
+
isActive ? 'border-green-500/20 shadow-[0_0_15px_oklch(0.7_0.17_145/8%)]' : 'border-white/[0.06]'
|
|
22
|
+
}`}
|
|
23
|
+
>
|
|
24
|
+
<div className="flex items-center gap-4 p-4">
|
|
25
|
+
{/* Avatar */}
|
|
26
|
+
<div className="shrink-0 w-12 h-12 rounded-lg bg-[--cyan]/10 border border-[--cyan]/20 flex items-center justify-center">
|
|
27
|
+
<span className="text-xl font-mono font-bold text-[--cyan]">{initial}</span>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
{/* Info */}
|
|
31
|
+
<div className="flex-1 min-w-0">
|
|
32
|
+
<div className="flex items-center gap-2">
|
|
33
|
+
<p className="text-sm font-mono font-semibold text-foreground">@{codename.toUpperCase()}</p>
|
|
34
|
+
<div className={`w-2 h-2 rounded-full shrink-0 ${
|
|
35
|
+
isActive ? 'bg-green-500 animate-pulse' : 'bg-muted-foreground/40'
|
|
36
|
+
}`} />
|
|
37
|
+
{isActive && (
|
|
38
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-green-500/10 text-green-500 border border-green-500/20 px-2 py-0.5 text-[9px] font-mono font-medium">
|
|
39
|
+
{agent.activeJobs} job{agent.activeJobs !== 1 ? 's' : ''} running
|
|
40
|
+
</span>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
{agent.role && (
|
|
44
|
+
<p className="text-xs text-muted-foreground mt-0.5 font-mono truncate">{agent.role}</p>
|
|
45
|
+
)}
|
|
46
|
+
{agent.specialization && (
|
|
47
|
+
<p className="text-[10px] text-muted-foreground/70 mt-0.5 font-mono truncate">{agent.specialization}</p>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* Actions */}
|
|
52
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
53
|
+
<button
|
|
54
|
+
onClick={() => onViewProfile(agent.id)}
|
|
55
|
+
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] hover:border-[--cyan]/30 hover:text-[--cyan] transition-colors"
|
|
56
|
+
>
|
|
57
|
+
View
|
|
58
|
+
</button>
|
|
59
|
+
<button
|
|
60
|
+
onClick={() => onAssignTask(agent)}
|
|
61
|
+
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors"
|
|
62
|
+
>
|
|
63
|
+
Assign Task
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</motion.div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Agent Profile Panel ─────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function AgentProfilePanel({ agentId, onClose }) {
|
|
74
|
+
const [profile, setProfile] = useState(null);
|
|
75
|
+
const [loading, setLoading] = useState(true);
|
|
76
|
+
const [editingFile, setEditingFile] = useState(null);
|
|
77
|
+
const [editContent, setEditContent] = useState('');
|
|
78
|
+
const [saving, setSaving] = useState(false);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
setLoading(true);
|
|
82
|
+
getAgentProfile(agentId).then((p) => { setProfile(p); setLoading(false); });
|
|
83
|
+
}, [agentId]);
|
|
84
|
+
|
|
85
|
+
async function handleSave() {
|
|
86
|
+
if (!editingFile) return;
|
|
87
|
+
setSaving(true);
|
|
88
|
+
await updateAgentFile(agentId, editingFile, editContent);
|
|
89
|
+
const updated = await getAgentProfile(agentId);
|
|
90
|
+
setProfile(updated);
|
|
91
|
+
setEditingFile(null);
|
|
92
|
+
setSaving(false);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function startEdit(filename, content) {
|
|
96
|
+
setEditingFile(filename);
|
|
97
|
+
setEditContent(content || '');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (loading) {
|
|
101
|
+
return (
|
|
102
|
+
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="rounded-lg border border-[--cyan]/20 bg-[--card] p-5">
|
|
103
|
+
<div className="flex flex-col gap-3">
|
|
104
|
+
{[...Array(3)].map((_, i) => <div key={i} className="h-12 animate-shimmer rounded-md border border-white/[0.06]" />)}
|
|
105
|
+
</div>
|
|
106
|
+
</motion.div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!profile) return null;
|
|
111
|
+
|
|
112
|
+
const codename = profile.codename || profile.name || profile.id;
|
|
113
|
+
const files = [
|
|
114
|
+
{ name: 'SOUL.md', content: profile.soul, label: 'Soul' },
|
|
115
|
+
{ name: 'SKILLS.md', content: profile.skills, label: 'Skills' },
|
|
116
|
+
{ name: 'TOOLS.md', content: profile.tools, label: 'Tools' },
|
|
117
|
+
{ name: 'HEARTBEAT.md', content: profile.heartbeat, label: 'Heartbeat' },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<motion.div
|
|
122
|
+
initial={{ opacity: 0, y: 12 }}
|
|
123
|
+
animate={{ opacity: 1, y: 0 }}
|
|
124
|
+
exit={{ opacity: 0, y: 12 }}
|
|
125
|
+
className="rounded-lg border border-[--cyan]/20 bg-[--card] overflow-hidden"
|
|
126
|
+
>
|
|
127
|
+
{/* Header */}
|
|
128
|
+
<div className="flex items-center justify-between p-4 border-b border-white/[0.06]">
|
|
129
|
+
<div className="flex items-center gap-3">
|
|
130
|
+
<div className="w-10 h-10 rounded-lg bg-[--cyan]/10 border border-[--cyan]/20 flex items-center justify-center">
|
|
131
|
+
<span className="text-lg font-mono font-bold text-[--cyan]">{codename.charAt(0).toUpperCase()}</span>
|
|
132
|
+
</div>
|
|
133
|
+
<div>
|
|
134
|
+
<p className="text-sm font-mono font-semibold">@{codename.toUpperCase()}</p>
|
|
135
|
+
{profile.role && <p className="text-[10px] font-mono text-muted-foreground">{profile.role}</p>}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<button onClick={onClose} className="text-muted-foreground hover:text-foreground transition-colors">
|
|
139
|
+
<XIcon size={16} />
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Identity */}
|
|
144
|
+
<div className="p-4 border-b border-white/[0.06]">
|
|
145
|
+
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider">Identity</span>
|
|
146
|
+
<div className="grid grid-cols-2 gap-2 mt-2">
|
|
147
|
+
{profile.name && (
|
|
148
|
+
<div>
|
|
149
|
+
<span className="text-[10px] font-mono text-muted-foreground uppercase">Name</span>
|
|
150
|
+
<p className="text-xs font-mono">{profile.name}</p>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
<div>
|
|
154
|
+
<span className="text-[10px] font-mono text-muted-foreground uppercase">Codename</span>
|
|
155
|
+
<p className="text-xs font-mono">{codename}</p>
|
|
156
|
+
</div>
|
|
157
|
+
{profile.role && (
|
|
158
|
+
<div>
|
|
159
|
+
<span className="text-[10px] font-mono text-muted-foreground uppercase">Role</span>
|
|
160
|
+
<p className="text-xs font-mono">{profile.role}</p>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
{profile.specialization && (
|
|
164
|
+
<div>
|
|
165
|
+
<span className="text-[10px] font-mono text-muted-foreground uppercase">Specialization</span>
|
|
166
|
+
<p className="text-xs font-mono">{profile.specialization}</p>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* Config */}
|
|
173
|
+
{profile.config && (
|
|
174
|
+
<div className="p-4 border-b border-white/[0.06]">
|
|
175
|
+
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider">Config</span>
|
|
176
|
+
<pre className="mt-2 text-[11px] bg-black/30 rounded-md p-2.5 font-mono overflow-auto max-h-24 text-foreground/80 border border-white/[0.04]">
|
|
177
|
+
{JSON.stringify(profile.config, null, 2)}
|
|
178
|
+
</pre>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{/* Files */}
|
|
183
|
+
<div className="p-4">
|
|
184
|
+
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider">Files</span>
|
|
185
|
+
<div className="flex flex-col gap-2 mt-2">
|
|
186
|
+
{files.map((f) => (
|
|
187
|
+
<div key={f.name}>
|
|
188
|
+
<div className="flex items-center justify-between">
|
|
189
|
+
<span className="text-[10px] font-mono text-muted-foreground">{f.name}</span>
|
|
190
|
+
<div className="flex items-center gap-1">
|
|
191
|
+
{f.content ? (
|
|
192
|
+
<span className="text-[9px] font-mono text-green-500">exists</span>
|
|
193
|
+
) : (
|
|
194
|
+
<span className="text-[9px] font-mono text-muted-foreground/50">empty</span>
|
|
195
|
+
)}
|
|
196
|
+
<button
|
|
197
|
+
onClick={() => editingFile === f.name ? setEditingFile(null) : startEdit(f.name, f.content)}
|
|
198
|
+
className="text-muted-foreground hover:text-[--cyan] transition-colors p-0.5"
|
|
199
|
+
>
|
|
200
|
+
<PencilIcon size={10} />
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<AnimatePresence>
|
|
206
|
+
{editingFile === f.name && (
|
|
207
|
+
<motion.div
|
|
208
|
+
initial={{ height: 0, opacity: 0 }}
|
|
209
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
210
|
+
exit={{ height: 0, opacity: 0 }}
|
|
211
|
+
className="overflow-hidden"
|
|
212
|
+
>
|
|
213
|
+
<textarea
|
|
214
|
+
value={editContent}
|
|
215
|
+
onChange={(e) => setEditContent(e.target.value)}
|
|
216
|
+
rows={8}
|
|
217
|
+
className="w-full mt-1 text-[11px] border border-white/[0.06] rounded-md p-2.5 bg-black/20 font-mono text-foreground/80 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors resize-y"
|
|
218
|
+
/>
|
|
219
|
+
<div className="flex gap-2 mt-1">
|
|
220
|
+
<button
|
|
221
|
+
onClick={handleSave}
|
|
222
|
+
disabled={saving}
|
|
223
|
+
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50"
|
|
224
|
+
>
|
|
225
|
+
{saving ? <SpinnerIcon size={10} /> : <CheckIcon size={10} />} Save
|
|
226
|
+
</button>
|
|
227
|
+
<button
|
|
228
|
+
onClick={() => setEditingFile(null)}
|
|
229
|
+
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground"
|
|
230
|
+
>
|
|
231
|
+
Cancel
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
</motion.div>
|
|
235
|
+
)}
|
|
236
|
+
</AnimatePresence>
|
|
237
|
+
</div>
|
|
238
|
+
))}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</motion.div>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Assign Task Dialog ──────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
function AssignTaskDialog({ agent, onClose }) {
|
|
248
|
+
const [prompt, setPrompt] = useState('');
|
|
249
|
+
const [submitting, setSubmitting] = useState(false);
|
|
250
|
+
const [result, setResult] = useState(null);
|
|
251
|
+
|
|
252
|
+
async function handleSubmit() {
|
|
253
|
+
if (!prompt) return;
|
|
254
|
+
setSubmitting(true);
|
|
255
|
+
const res = await createAgentJob(agent.id, prompt);
|
|
256
|
+
setResult(res);
|
|
257
|
+
setSubmitting(false);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const codename = agent.codename || agent.name || agent.id;
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<motion.div
|
|
264
|
+
initial={{ opacity: 0, scale: 0.95 }}
|
|
265
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
266
|
+
exit={{ opacity: 0, scale: 0.95 }}
|
|
267
|
+
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
268
|
+
>
|
|
269
|
+
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
|
270
|
+
<div className="relative w-full max-w-lg rounded-lg border border-[--cyan]/20 bg-[--card] shadow-2xl">
|
|
271
|
+
<div className="flex items-center justify-between p-4 border-b border-white/[0.06]">
|
|
272
|
+
<div className="flex items-center gap-2">
|
|
273
|
+
<div className="flex items-center gap-1">
|
|
274
|
+
<div className="w-2 h-2 rounded-full bg-[#ff5f57]" />
|
|
275
|
+
<div className="w-2 h-2 rounded-full bg-[#febc2e]" />
|
|
276
|
+
<div className="w-2 h-2 rounded-full bg-[#28c840]" />
|
|
277
|
+
</div>
|
|
278
|
+
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider ml-1">
|
|
279
|
+
Assign Task to @{codename.toUpperCase()}
|
|
280
|
+
</span>
|
|
281
|
+
</div>
|
|
282
|
+
<button onClick={onClose} className="text-muted-foreground hover:text-foreground"><XIcon size={14} /></button>
|
|
283
|
+
</div>
|
|
284
|
+
<div className="p-4">
|
|
285
|
+
<textarea
|
|
286
|
+
value={prompt}
|
|
287
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
288
|
+
placeholder="Describe the task for this agent..."
|
|
289
|
+
rows={4}
|
|
290
|
+
className="w-full text-sm border border-white/[0.06] rounded-md p-3 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors resize-y"
|
|
291
|
+
autoFocus
|
|
292
|
+
/>
|
|
293
|
+
{result && (
|
|
294
|
+
<p className={`text-xs font-mono mt-2 ${result.error ? 'text-[--destructive]' : 'text-green-500'}`}>
|
|
295
|
+
{result.error || 'Job created successfully'}
|
|
296
|
+
</p>
|
|
297
|
+
)}
|
|
298
|
+
<div className="flex justify-end gap-2 mt-3">
|
|
299
|
+
<button onClick={onClose}
|
|
300
|
+
className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground">
|
|
301
|
+
Cancel
|
|
302
|
+
</button>
|
|
303
|
+
<button onClick={handleSubmit} disabled={submitting || !prompt}
|
|
304
|
+
className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50">
|
|
305
|
+
{submitting ? <SpinnerIcon size={12} /> : 'Create Job'}
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</motion.div>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─── Create Agent Form ───────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
function CreateAgentForm({ onCreated, onClose }) {
|
|
317
|
+
const [name, setName] = useState('');
|
|
318
|
+
const [codename, setCodename] = useState('');
|
|
319
|
+
const [role, setRole] = useState('');
|
|
320
|
+
const [specialization, setSpecialization] = useState('');
|
|
321
|
+
const [soul, setSoul] = useState('');
|
|
322
|
+
const [creating, setCreating] = useState(false);
|
|
323
|
+
const [result, setResult] = useState(null);
|
|
324
|
+
|
|
325
|
+
async function handleCreate() {
|
|
326
|
+
if (!name && !codename) return;
|
|
327
|
+
setCreating(true);
|
|
328
|
+
const res = await createAgent({ name, codename, role, specialization, soul });
|
|
329
|
+
setResult(res);
|
|
330
|
+
if (!res.error) {
|
|
331
|
+
onCreated();
|
|
332
|
+
setName(''); setCodename(''); setRole(''); setSpecialization(''); setSoul('');
|
|
333
|
+
}
|
|
334
|
+
setCreating(false);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<motion.div
|
|
339
|
+
initial={{ opacity: 0, height: 0 }}
|
|
340
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
341
|
+
exit={{ opacity: 0, height: 0 }}
|
|
342
|
+
className="overflow-hidden"
|
|
343
|
+
>
|
|
344
|
+
<div className="rounded-lg border border-[--cyan]/20 bg-[--card] p-5 mt-4">
|
|
345
|
+
<div className="flex items-center justify-between mb-4">
|
|
346
|
+
<div className="flex items-center gap-2">
|
|
347
|
+
<div className="flex items-center gap-1">
|
|
348
|
+
<div className="w-2 h-2 rounded-full bg-[#ff5f57]" />
|
|
349
|
+
<div className="w-2 h-2 rounded-full bg-[#febc2e]" />
|
|
350
|
+
<div className="w-2 h-2 rounded-full bg-[#28c840]" />
|
|
351
|
+
</div>
|
|
352
|
+
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider ml-1">
|
|
353
|
+
Create New Agent
|
|
354
|
+
</span>
|
|
355
|
+
</div>
|
|
356
|
+
<button onClick={onClose} className="text-muted-foreground hover:text-foreground"><XIcon size={14} /></button>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3">
|
|
360
|
+
<div>
|
|
361
|
+
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1">Name</label>
|
|
362
|
+
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Reaper"
|
|
363
|
+
className="w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors" />
|
|
364
|
+
</div>
|
|
365
|
+
<div>
|
|
366
|
+
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1">Codename</label>
|
|
367
|
+
<input value={codename} onChange={(e) => setCodename(e.target.value)} placeholder="REAPER"
|
|
368
|
+
className="w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors" />
|
|
369
|
+
</div>
|
|
370
|
+
<div>
|
|
371
|
+
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1">Role</label>
|
|
372
|
+
<input value={role} onChange={(e) => setRole(e.target.value)} placeholder="Autonomous Recon Specialist"
|
|
373
|
+
className="w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors" />
|
|
374
|
+
</div>
|
|
375
|
+
<div>
|
|
376
|
+
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1">Specialization</label>
|
|
377
|
+
<input value={specialization} onChange={(e) => setSpecialization(e.target.value)} placeholder="Bug Bounty Recon"
|
|
378
|
+
className="w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors" />
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
<div className="mb-3">
|
|
383
|
+
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1">SOUL.md (optional)</label>
|
|
384
|
+
<textarea value={soul} onChange={(e) => setSoul(e.target.value)} rows={4} placeholder="Agent personality and system prompt..."
|
|
385
|
+
className="w-full text-sm border border-white/[0.06] rounded-md p-3 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors resize-y" />
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
{result && (
|
|
389
|
+
<p className={`text-xs font-mono mb-2 ${result.error ? 'text-[--destructive]' : 'text-green-500'}`}>
|
|
390
|
+
{result.error || `Agent created: ${result.id}`}
|
|
391
|
+
</p>
|
|
392
|
+
)}
|
|
393
|
+
|
|
394
|
+
<div className="flex gap-2">
|
|
395
|
+
<button onClick={handleCreate} disabled={creating || (!name && !codename)}
|
|
396
|
+
className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50">
|
|
397
|
+
{creating ? <SpinnerIcon size={12} /> : <PlusIcon size={12} />} Create Agent
|
|
398
|
+
</button>
|
|
399
|
+
<button onClick={onClose}
|
|
400
|
+
className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground">
|
|
401
|
+
Cancel
|
|
402
|
+
</button>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
</motion.div>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ─── Main Page ───────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
export function AgentsPage() {
|
|
412
|
+
const [agents, setAgents] = useState([]);
|
|
413
|
+
const [loading, setLoading] = useState(true);
|
|
414
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
415
|
+
const [viewingProfile, setViewingProfile] = useState(null);
|
|
416
|
+
const [assigningTask, setAssigningTask] = useState(null);
|
|
417
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
418
|
+
const [search, setSearch] = useState('');
|
|
419
|
+
|
|
420
|
+
async function load() {
|
|
421
|
+
try {
|
|
422
|
+
const a = await getAgentProfilesWithStatus();
|
|
423
|
+
setAgents(a);
|
|
424
|
+
} catch {}
|
|
425
|
+
setLoading(false);
|
|
426
|
+
setRefreshing(false);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
useEffect(() => { load(); }, []);
|
|
430
|
+
|
|
431
|
+
const activeCount = agents.filter(a => a.status === 'active').length;
|
|
432
|
+
|
|
433
|
+
const filtered = agents.filter(a => {
|
|
434
|
+
if (!search) return true;
|
|
435
|
+
const q = search.toLowerCase();
|
|
436
|
+
const codename = (a.codename || a.name || a.id || '').toLowerCase();
|
|
437
|
+
const role = (a.role || '').toLowerCase();
|
|
438
|
+
return codename.includes(q) || role.includes(q);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (loading) {
|
|
442
|
+
return (
|
|
443
|
+
<div className="flex flex-col gap-4 p-6">
|
|
444
|
+
<div className="h-8 w-48 animate-pulse rounded-lg bg-white/[0.04]" />
|
|
445
|
+
<div className="flex flex-col gap-3">
|
|
446
|
+
{[...Array(4)].map((_, i) => (
|
|
447
|
+
<div key={i} className="h-20 animate-pulse rounded-lg bg-white/[0.04] border border-white/[0.06]" />
|
|
448
|
+
))}
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<>
|
|
456
|
+
{/* Header */}
|
|
457
|
+
<div className="flex items-center justify-between mb-6">
|
|
458
|
+
<div>
|
|
459
|
+
<h1 className="text-2xl font-mono font-semibold text-[--cyan] text-glow-cyan">Agents</h1>
|
|
460
|
+
<p className="text-[11px] text-muted-foreground mt-1 font-mono">Manage agent profiles, roles, and assignments</p>
|
|
461
|
+
</div>
|
|
462
|
+
<div className="flex items-center gap-2">
|
|
463
|
+
<span className="inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20">
|
|
464
|
+
{agents.length} agents
|
|
465
|
+
</span>
|
|
466
|
+
{activeCount > 0 && (
|
|
467
|
+
<span className="inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-[10px] font-mono font-medium bg-green-500/10 text-green-500 border border-green-500/20">
|
|
468
|
+
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
|
|
469
|
+
{activeCount} active
|
|
470
|
+
</span>
|
|
471
|
+
)}
|
|
472
|
+
<button
|
|
473
|
+
onClick={() => setShowCreate(!showCreate)}
|
|
474
|
+
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors"
|
|
475
|
+
>
|
|
476
|
+
<PlusIcon size={12} /> New Agent
|
|
477
|
+
</button>
|
|
478
|
+
<button
|
|
479
|
+
onClick={() => { setRefreshing(true); load(); }}
|
|
480
|
+
disabled={refreshing}
|
|
481
|
+
className="inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-mono font-medium border border-white/[0.06] text-muted-foreground hover:text-foreground hover:bg-white/[0.04] disabled:opacity-50 transition-colors"
|
|
482
|
+
>
|
|
483
|
+
{refreshing ? <SpinnerIcon size={14} /> : <RefreshIcon size={14} />}
|
|
484
|
+
</button>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
{/* Stats */}
|
|
489
|
+
<div className="grid grid-cols-3 gap-3 mb-6">
|
|
490
|
+
<div className="flex flex-col items-center justify-center p-4 rounded-lg border border-white/[0.06] bg-[--card]">
|
|
491
|
+
<span className="text-2xl font-semibold text-[--cyan] font-mono">{agents.length}</span>
|
|
492
|
+
<span className="font-mono text-[10px] text-muted-foreground uppercase tracking-wider mt-1">Agents</span>
|
|
493
|
+
</div>
|
|
494
|
+
<div className="flex flex-col items-center justify-center p-4 rounded-lg border border-white/[0.06] bg-[--card]">
|
|
495
|
+
<span className="text-2xl font-semibold text-green-500 font-mono">{activeCount}</span>
|
|
496
|
+
<span className="font-mono text-[10px] text-muted-foreground uppercase tracking-wider mt-1">Active</span>
|
|
497
|
+
</div>
|
|
498
|
+
<div className="flex flex-col items-center justify-center p-4 rounded-lg border border-white/[0.06] bg-[--card]">
|
|
499
|
+
<span className="text-2xl font-semibold font-mono text-muted-foreground">{agents.length - activeCount}</span>
|
|
500
|
+
<span className="font-mono text-[10px] text-muted-foreground uppercase tracking-wider mt-1">Idle</span>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
|
|
504
|
+
{/* Search */}
|
|
505
|
+
{agents.length > 0 && (
|
|
506
|
+
<div className="relative mb-4">
|
|
507
|
+
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"><SearchIcon size={14} /></div>
|
|
508
|
+
<input
|
|
509
|
+
placeholder="Search agents..."
|
|
510
|
+
value={search}
|
|
511
|
+
onChange={e => setSearch(e.target.value)}
|
|
512
|
+
className="w-full text-sm border border-white/[0.06] rounded-md pl-9 pr-3 py-2 bg-black/20 font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors"
|
|
513
|
+
/>
|
|
514
|
+
</div>
|
|
515
|
+
)}
|
|
516
|
+
|
|
517
|
+
{/* Create form */}
|
|
518
|
+
<AnimatePresence>
|
|
519
|
+
{showCreate && (
|
|
520
|
+
<CreateAgentForm onCreated={() => { load(); setShowCreate(false); }} onClose={() => setShowCreate(false)} />
|
|
521
|
+
)}
|
|
522
|
+
</AnimatePresence>
|
|
523
|
+
|
|
524
|
+
{/* Agent list */}
|
|
525
|
+
<div className="flex flex-col gap-3 mt-4">
|
|
526
|
+
{filtered.length === 0 && agents.length === 0 ? (
|
|
527
|
+
<div className="flex flex-col items-center justify-center py-16 text-center rounded-lg border border-white/[0.06] bg-[--card]">
|
|
528
|
+
<div className="rounded-full bg-[--cyan]/10 p-4 mb-4">
|
|
529
|
+
<UsersIcon size={24} className="text-[--cyan]" />
|
|
530
|
+
</div>
|
|
531
|
+
<p className="text-sm font-mono font-medium mb-1">No agents configured</p>
|
|
532
|
+
<p className="text-[11px] text-muted-foreground font-mono max-w-sm">
|
|
533
|
+
Create agent profiles in the <span className="text-[--cyan]">agents/</span> directory or click "New Agent" above.
|
|
534
|
+
</p>
|
|
535
|
+
</div>
|
|
536
|
+
) : filtered.length === 0 ? (
|
|
537
|
+
<div className="flex flex-col items-center py-12 text-center">
|
|
538
|
+
<div className="rounded-full bg-white/[0.04] border border-white/[0.06] p-4 mb-4"><SearchIcon size={24} /></div>
|
|
539
|
+
<p className="text-sm font-mono text-muted-foreground">No agents match your search.</p>
|
|
540
|
+
</div>
|
|
541
|
+
) : (
|
|
542
|
+
filtered.map((agent, i) => (
|
|
543
|
+
<AgentCard
|
|
544
|
+
key={agent.id}
|
|
545
|
+
agent={agent}
|
|
546
|
+
onViewProfile={setViewingProfile}
|
|
547
|
+
onAssignTask={setAssigningTask}
|
|
548
|
+
index={i}
|
|
549
|
+
/>
|
|
550
|
+
))
|
|
551
|
+
)}
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
{/* Profile panel */}
|
|
555
|
+
<AnimatePresence>
|
|
556
|
+
{viewingProfile && (
|
|
557
|
+
<div className="mt-4">
|
|
558
|
+
<AgentProfilePanel agentId={viewingProfile} onClose={() => setViewingProfile(null)} />
|
|
559
|
+
</div>
|
|
560
|
+
)}
|
|
561
|
+
</AnimatePresence>
|
|
562
|
+
|
|
563
|
+
{/* Assign task dialog */}
|
|
564
|
+
<AnimatePresence>
|
|
565
|
+
{assigningTask && (
|
|
566
|
+
<AssignTaskDialog agent={assigningTask} onClose={() => setAssigningTask(null)} />
|
|
567
|
+
)}
|
|
568
|
+
</AnimatePresence>
|
|
569
|
+
</>
|
|
570
|
+
);
|
|
571
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
|
-
import { CirclePlusIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon, ArrowUpCircleIcon, LifeBuoyIcon, CrosshairIcon, ShieldIcon, PackageIcon, CommandIcon } from "./icons.js";
|
|
4
|
+
import { CirclePlusIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon, ArrowUpCircleIcon, LifeBuoyIcon, CrosshairIcon, ShieldIcon, PackageIcon, CommandIcon, UsersIcon } from "./icons.js";
|
|
5
5
|
import { getUnreadNotificationCount, getAppVersion } from "../actions.js";
|
|
6
6
|
import { SidebarHistory } from "./sidebar-history.js";
|
|
7
7
|
import { SidebarUserNav } from "./sidebar-user-nav.js";
|
|
@@ -128,6 +128,22 @@ function AppSidebar({ user }) {
|
|
|
128
128
|
) }),
|
|
129
129
|
collapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Mission Control" })
|
|
130
130
|
] }) }),
|
|
131
|
+
/* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
132
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
133
|
+
SidebarMenuButton,
|
|
134
|
+
{
|
|
135
|
+
className: collapsed ? "justify-center" : "",
|
|
136
|
+
onClick: () => {
|
|
137
|
+
window.location.href = "/agents";
|
|
138
|
+
},
|
|
139
|
+
children: [
|
|
140
|
+
/* @__PURE__ */ jsx(UsersIcon, { size: 16 }),
|
|
141
|
+
!collapsed && /* @__PURE__ */ jsx("span", { children: "Agents" })
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
) }),
|
|
145
|
+
collapsed && /* @__PURE__ */ jsx(TooltipContent, { side: "right", children: "Agents" })
|
|
146
|
+
] }) }),
|
|
131
147
|
/* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
132
148
|
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
133
149
|
SidebarMenuButton,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
|
-
import { CirclePlusIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon, ArrowUpCircleIcon, LifeBuoyIcon, CrosshairIcon, ShieldIcon, PackageIcon, CommandIcon } from './icons.js';
|
|
4
|
+
import { CirclePlusIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon, ArrowUpCircleIcon, LifeBuoyIcon, CrosshairIcon, ShieldIcon, PackageIcon, CommandIcon, UsersIcon } from './icons.js';
|
|
5
5
|
import { getUnreadNotificationCount, getAppVersion } from '../actions.js';
|
|
6
6
|
import { SidebarHistory } from './sidebar-history.js';
|
|
7
7
|
import { SidebarUserNav } from './sidebar-user-nav.js';
|
|
@@ -147,6 +147,24 @@ export function AppSidebar({ user }) {
|
|
|
147
147
|
</Tooltip>
|
|
148
148
|
</SidebarMenuItem>
|
|
149
149
|
|
|
150
|
+
{/* Agents */}
|
|
151
|
+
<SidebarMenuItem>
|
|
152
|
+
<Tooltip>
|
|
153
|
+
<TooltipTrigger asChild>
|
|
154
|
+
<SidebarMenuButton
|
|
155
|
+
className={collapsed ? 'justify-center' : ''}
|
|
156
|
+
onClick={() => { window.location.href = '/agents'; }}
|
|
157
|
+
>
|
|
158
|
+
<UsersIcon size={16} />
|
|
159
|
+
{!collapsed && <span>Agents</span>}
|
|
160
|
+
</SidebarMenuButton>
|
|
161
|
+
</TooltipTrigger>
|
|
162
|
+
{collapsed && (
|
|
163
|
+
<TooltipContent side="right">Agents</TooltipContent>
|
|
164
|
+
)}
|
|
165
|
+
</Tooltip>
|
|
166
|
+
</SidebarMenuItem>
|
|
167
|
+
|
|
150
168
|
{/* Targets */}
|
|
151
169
|
<SidebarMenuItem>
|
|
152
170
|
<Tooltip>
|