@harbinger-ai/harbinger 0.1.3 → 0.1.4

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.
@@ -2,8 +2,25 @@
2
2
 
3
3
  import { useState, useEffect } from 'react';
4
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';
5
+ import { UsersIcon, RefreshIcon, SpinnerIcon, ChevronDownIcon, PlusIcon, PencilIcon, CheckIcon, XIcon, SearchIcon, CpuIcon, PackageIcon, DatabaseIcon, FolderIcon, ClockIcon, SlidersIcon, TagIcon, HashIcon, PlayIcon } from './icons.js';
6
+ import { getAgentProfilesWithStatus, getAgentProfile, updateAgentFile, createAgent, createAgentJob, getAvailableMcpTools, getLlmProviders } from '../actions.js';
7
+
8
+ const PROVIDER_MODELS = {
9
+ anthropic: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-haiku-4-5-20251001'],
10
+ openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1', 'o1-mini', 'o3-mini'],
11
+ google: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash'],
12
+ custom: [],
13
+ };
14
+
15
+ const ROLE_OPTIONS = ['Recon', 'Exploitation', 'Reporting', 'Infrastructure', 'Research', 'Analysis', 'Development', 'Custom'];
16
+ const SPEC_OPTIONS = ['Web', 'API', 'Mobile', 'Cloud', 'Network', 'Blockchain', 'IoT', 'Social Engineering', 'Custom'];
17
+ const RUN_MODES = ['manual', 'scheduled', 'continuous', 'event-triggered'];
18
+ const SOUL_TEMPLATES = {
19
+ recon: `# {{CODENAME}}\n\nYou are {{NAME}}, an autonomous reconnaissance specialist.\n\n## Mission\nDiscover and enumerate all attack surface for the assigned target.\n\n## Approach\n- Start with passive reconnaissance\n- Enumerate subdomains, ports, services\n- Identify technologies and frameworks\n- Map the application architecture\n- Document all findings systematically\n\n## Personality\n- Methodical and thorough\n- Never skip steps\n- Always verify findings`,
20
+ exploit: `# {{CODENAME}}\n\nYou are {{NAME}}, an autonomous exploitation specialist.\n\n## Mission\nIdentify and validate security vulnerabilities in assigned targets.\n\n## Approach\n- Analyze attack surface from recon data\n- Test for common vulnerability classes (OWASP Top 10)\n- Develop and validate proof-of-concept exploits\n- Assess impact and severity\n- Document reproduction steps clearly\n\n## Personality\n- Creative and persistent\n- Ethical — never cause damage\n- Report accurately, no exaggeration`,
21
+ report: `# {{CODENAME}}\n\nYou are {{NAME}}, an autonomous report writer.\n\n## Mission\nCreate clear, actionable security reports from findings data.\n\n## Approach\n- Gather all findings and evidence\n- Assess severity using CVSS\n- Write clear reproduction steps\n- Suggest remediation\n- Format for platform submission\n\n## Personality\n- Precise and concise\n- Technical but accessible\n- Focus on impact`,
22
+ custom: `# {{CODENAME}}\n\nYou are {{NAME}}, a specialized AI agent.\n\n## Role\n{{ROLE}}\n\n## Specialization\n{{SPEC}}\n\n## Mission\nDescribe the agent's primary objective here.\n\n## Approach\n- Step 1\n- Step 2\n- Step 3`,
23
+ };
7
24
 
8
25
  // ─── Agent Card ──────────────────────────────────────────────────────────────
9
26
 
@@ -22,44 +39,29 @@ function AgentCard({ agent, onViewProfile, onAssignTask, index }) {
22
39
  }`}
23
40
  >
24
41
  <div className="flex items-center gap-4 p-4">
25
- {/* Avatar */}
26
42
  <div className="shrink-0 w-12 h-12 rounded-lg bg-[--cyan]/10 border border-[--cyan]/20 flex items-center justify-center">
27
43
  <span className="text-xl font-mono font-bold text-[--cyan]">{initial}</span>
28
44
  </div>
29
-
30
- {/* Info */}
31
45
  <div className="flex-1 min-w-0">
32
46
  <div className="flex items-center gap-2">
33
47
  <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
- }`} />
48
+ <div className={`w-2 h-2 rounded-full shrink-0 ${isActive ? 'bg-green-500 animate-pulse' : 'bg-muted-foreground/40'}`} />
37
49
  {isActive && (
38
50
  <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
51
  {agent.activeJobs} job{agent.activeJobs !== 1 ? 's' : ''} running
40
52
  </span>
41
53
  )}
42
54
  </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
- )}
55
+ {agent.role && <p className="text-xs text-muted-foreground mt-0.5 font-mono truncate">{agent.role}</p>}
56
+ {agent.specialization && <p className="text-[10px] text-muted-foreground/70 mt-0.5 font-mono truncate">{agent.specialization}</p>}
49
57
  </div>
50
-
51
- {/* Actions */}
52
58
  <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
- >
59
+ <button onClick={() => onViewProfile(agent.id)}
60
+ 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">
57
61
  View
58
62
  </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
+ <button onClick={() => onAssignTask(agent)}
64
+ 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">
63
65
  Assign Task
64
66
  </button>
65
67
  </div>
@@ -115,16 +117,12 @@ function AgentProfilePanel({ agentId, onClose }) {
115
117
  { name: 'SKILLS.md', content: profile.skills, label: 'Skills' },
116
118
  { name: 'TOOLS.md', content: profile.tools, label: 'Tools' },
117
119
  { name: 'HEARTBEAT.md', content: profile.heartbeat, label: 'Heartbeat' },
120
+ { name: 'IDENTITY.md', content: null, label: 'Identity' },
118
121
  ];
119
122
 
120
123
  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 */}
124
+ <motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 12 }}
125
+ className="rounded-lg border border-[--cyan]/20 bg-[--card] overflow-hidden">
128
126
  <div className="flex items-center justify-between p-4 border-b border-white/[0.06]">
129
127
  <div className="flex items-center gap-3">
130
128
  <div className="w-10 h-10 rounded-lg bg-[--cyan]/10 border border-[--cyan]/20 flex items-center justify-center">
@@ -135,51 +133,28 @@ function AgentProfilePanel({ agentId, onClose }) {
135
133
  {profile.role && <p className="text-[10px] font-mono text-muted-foreground">{profile.role}</p>}
136
134
  </div>
137
135
  </div>
138
- <button onClick={onClose} className="text-muted-foreground hover:text-foreground transition-colors">
139
- <XIcon size={16} />
140
- </button>
136
+ <button onClick={onClose} className="text-muted-foreground hover:text-foreground transition-colors"><XIcon size={16} /></button>
141
137
  </div>
142
138
 
143
- {/* Identity */}
144
139
  <div className="p-4 border-b border-white/[0.06]">
145
140
  <span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider">Identity</span>
146
141
  <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
- )}
142
+ {profile.name && <div><span className="text-[10px] font-mono text-muted-foreground uppercase">Name</span><p className="text-xs font-mono">{profile.name}</p></div>}
143
+ <div><span className="text-[10px] font-mono text-muted-foreground uppercase">Codename</span><p className="text-xs font-mono">{codename}</p></div>
144
+ {profile.role && <div><span className="text-[10px] font-mono text-muted-foreground uppercase">Role</span><p className="text-xs font-mono">{profile.role}</p></div>}
145
+ {profile.specialization && <div><span className="text-[10px] font-mono text-muted-foreground uppercase">Specialization</span><p className="text-xs font-mono">{profile.specialization}</p></div>}
169
146
  </div>
170
147
  </div>
171
148
 
172
- {/* Config */}
173
149
  {profile.config && (
174
150
  <div className="p-4 border-b border-white/[0.06]">
175
151
  <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]">
152
+ <pre className="mt-2 text-[11px] bg-black/30 rounded-md p-2.5 font-mono overflow-auto max-h-32 text-foreground/80 border border-white/[0.04] scrollbar-thin">
177
153
  {JSON.stringify(profile.config, null, 2)}
178
154
  </pre>
179
155
  </div>
180
156
  )}
181
157
 
182
- {/* Files */}
183
158
  <div className="p-4">
184
159
  <span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider">Files</span>
185
160
  <div className="flex flex-col gap-2 mt-2">
@@ -188,48 +163,23 @@ function AgentProfilePanel({ agentId, onClose }) {
188
163
  <div className="flex items-center justify-between">
189
164
  <span className="text-[10px] font-mono text-muted-foreground">{f.name}</span>
190
165
  <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>
166
+ {f.content ? <span className="text-[9px] font-mono text-green-500">exists</span> : <span className="text-[9px] font-mono text-muted-foreground/50">empty</span>}
167
+ <button onClick={() => editingFile === f.name ? setEditingFile(null) : startEdit(f.name, f.content)}
168
+ className="text-muted-foreground hover:text-[--cyan] transition-colors p-0.5"><PencilIcon size={10} /></button>
202
169
  </div>
203
170
  </div>
204
-
205
171
  <AnimatePresence>
206
172
  {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
- />
173
+ <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} className="overflow-hidden">
174
+ <textarea value={editContent} onChange={(e) => setEditContent(e.target.value)} rows={8}
175
+ 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" />
219
176
  <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
- >
177
+ <button onClick={handleSave} disabled={saving}
178
+ 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">
225
179
  {saving ? <SpinnerIcon size={10} /> : <CheckIcon size={10} />} Save
226
180
  </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>
181
+ <button onClick={() => setEditingFile(null)}
182
+ 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">Cancel</button>
233
183
  </div>
234
184
  </motion.div>
235
185
  )}
@@ -260,46 +210,27 @@ function AssignTaskDialog({ agent, onClose }) {
260
210
  const codename = agent.codename || agent.name || agent.id;
261
211
 
262
212
  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
- >
213
+ <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }}
214
+ className="fixed inset-0 z-50 flex items-center justify-center p-4">
269
215
  <div className="absolute inset-0 bg-black/60" onClick={onClose} />
270
216
  <div className="relative w-full max-w-lg rounded-lg border border-[--cyan]/20 bg-[--card] shadow-2xl">
271
217
  <div className="flex items-center justify-between p-4 border-b border-white/[0.06]">
272
218
  <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>
219
+ <div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-[#ff5f57]" /><div className="w-2 h-2 rounded-full bg-[#febc2e]" /><div className="w-2 h-2 rounded-full bg-[#28c840]" /></div>
220
+ <span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider ml-1">Assign Task to @{codename.toUpperCase()}</span>
281
221
  </div>
282
222
  <button onClick={onClose} className="text-muted-foreground hover:text-foreground"><XIcon size={14} /></button>
283
223
  </div>
284
224
  <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
- />
225
+ <textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Describe the task for this agent..." rows={4}
226
+ 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" autoFocus />
293
227
  {result && (
294
228
  <p className={`text-xs font-mono mt-2 ${result.error ? 'text-[--destructive]' : 'text-green-500'}`}>
295
229
  {result.error || 'Job created successfully'}
296
230
  </p>
297
231
  )}
298
232
  <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>
233
+ <button onClick={onClose} 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">Cancel</button>
303
234
  <button onClick={handleSubmit} disabled={submitting || !prompt}
304
235
  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
236
  {submitting ? <SpinnerIcon size={12} /> : 'Create Job'}
@@ -311,95 +242,491 @@ function AssignTaskDialog({ agent, onClose }) {
311
242
  );
312
243
  }
313
244
 
314
- // ─── Create Agent Form ───────────────────────────────────────────────────────
245
+ // ─── Tab Button ──────────────────────────────────────────────────────────────
246
+
247
+ function TabBtn({ active, onClick, icon: Icon, label }) {
248
+ return (
249
+ <button onClick={onClick}
250
+ className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-mono font-medium rounded-md border transition-colors ${
251
+ active
252
+ ? 'bg-[--cyan]/10 text-[--cyan] border-[--cyan]/20'
253
+ : 'border-white/[0.06] text-muted-foreground hover:text-foreground hover:bg-white/[0.04]'
254
+ }`}>
255
+ {Icon && <Icon size={12} />}
256
+ {label}
257
+ </button>
258
+ );
259
+ }
260
+
261
+ // ─── Form Input Helpers ──────────────────────────────────────────────────────
262
+
263
+ function FormField({ label, children }) {
264
+ return (
265
+ <div>
266
+ <label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1">{label}</label>
267
+ {children}
268
+ </div>
269
+ );
270
+ }
271
+
272
+ const inputClass = "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";
273
+ const selectClass = "w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono focus:outline-none focus:border-[--cyan]/40 transition-colors";
274
+ const textareaClass = "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";
275
+
276
+ // ─── Create Agent Form (Enhanced) ────────────────────────────────────────────
315
277
 
316
278
  function CreateAgentForm({ onCreated, onClose }) {
279
+ const [tab, setTab] = useState('basic');
280
+ const [creating, setCreating] = useState(false);
281
+ const [testing, setTesting] = useState(false);
282
+ const [testResult, setTestResult] = useState(null);
283
+ const [result, setResult] = useState(null);
284
+ const [mcpTools, setMcpTools] = useState([]);
285
+ const [loadingTools, setLoadingTools] = useState(false);
286
+
287
+ // Basic
317
288
  const [name, setName] = useState('');
318
289
  const [codename, setCodename] = useState('');
319
290
  const [role, setRole] = useState('');
320
291
  const [specialization, setSpecialization] = useState('');
292
+ const [description, setDescription] = useState('');
293
+ const [goal, setGoal] = useState('');
294
+ const [tags, setTags] = useState([]);
295
+ const [tagInput, setTagInput] = useState('');
296
+
297
+ // LLM
298
+ const [llmProvider, setLlmProvider] = useState('');
299
+ const [llmModel, setLlmModel] = useState('');
300
+ const [temperature, setTemperature] = useState(0.7);
301
+ const [maxTokens, setMaxTokens] = useState(4096);
302
+
303
+ // Soul / Skills / Heartbeat
304
+ const [soulTemplate, setSoulTemplate] = useState('custom');
321
305
  const [soul, setSoul] = useState('');
322
- const [creating, setCreating] = useState(false);
323
- const [result, setResult] = useState(null);
306
+ const [skills, setSkills] = useState('');
307
+ const [heartbeat, setHeartbeat] = useState('');
308
+
309
+ // Tools
310
+ const [selectedTools, setSelectedTools] = useState([]);
311
+
312
+ // Scheduling
313
+ const [runMode, setRunMode] = useState('manual');
314
+ const [schedule, setSchedule] = useState('');
315
+ const [maxConcurrent, setMaxConcurrent] = useState(1);
316
+ const [timeout, setTimeout_] = useState(0);
317
+
318
+ // Resources
319
+ const [cpu, setCpu] = useState('1');
320
+ const [memory, setMemory] = useState('1GB');
321
+ const [disk, setDisk] = useState('5GB');
322
+ const [network, setNetwork] = useState('full');
323
+
324
+ // Filesystem
325
+ const [workDir, setWorkDir] = useState('/workspace');
326
+ const [allowedPaths, setAllowedPaths] = useState('');
327
+
328
+ // Env vars
329
+ const [envVars, setEnvVars] = useState([]);
330
+
331
+ // Team
332
+ const [swarm, setSwarm] = useState('');
333
+ const [supervisor, setSupervisor] = useState('');
334
+
335
+ // Auto-generate codename from name
336
+ useEffect(() => {
337
+ if (name && !codename) {
338
+ setCodename(name.toUpperCase().replace(/[^A-Z0-9]/g, '_').replace(/_+/g, '_'));
339
+ }
340
+ }, [name]);
341
+
342
+ // Load MCP tools on Tools tab
343
+ useEffect(() => {
344
+ if (tab === 'tools' && mcpTools.length === 0 && !loadingTools) {
345
+ setLoadingTools(true);
346
+ getAvailableMcpTools().then(t => { setMcpTools(t); setLoadingTools(false); }).catch(() => setLoadingTools(false));
347
+ }
348
+ }, [tab]);
349
+
350
+ // Apply soul template
351
+ function applySoulTemplate(templateKey) {
352
+ setSoulTemplate(templateKey);
353
+ let content = SOUL_TEMPLATES[templateKey] || SOUL_TEMPLATES.custom;
354
+ content = content.replace(/\{\{CODENAME\}\}/g, codename || 'AGENT');
355
+ content = content.replace(/\{\{NAME\}\}/g, name || 'Agent');
356
+ content = content.replace(/\{\{ROLE\}\}/g, role || 'General Agent');
357
+ content = content.replace(/\{\{SPEC\}\}/g, specialization || 'General');
358
+ setSoul(content);
359
+ }
360
+
361
+ function addTag() {
362
+ const t = tagInput.trim().toLowerCase();
363
+ if (t && !tags.includes(t)) setTags([...tags, t]);
364
+ setTagInput('');
365
+ }
366
+
367
+ function addEnvVar() {
368
+ setEnvVars([...envVars, { key: '', value: '' }]);
369
+ }
370
+
371
+ async function handleTest() {
372
+ setTesting(true);
373
+ setTestResult(null);
374
+ const issues = [];
375
+ if (!name && !codename) issues.push('Name or codename is required');
376
+ if (llmProvider && !llmModel) issues.push('LLM model not selected');
377
+ if (runMode === 'scheduled' && !schedule) issues.push('Schedule expression is required for scheduled mode');
378
+ setTestResult(issues.length === 0 ? { success: true } : { issues });
379
+ setTesting(false);
380
+ }
324
381
 
325
382
  async function handleCreate() {
326
383
  if (!name && !codename) return;
327
384
  setCreating(true);
328
- const res = await createAgent({ name, codename, role, specialization, soul });
385
+ const identity = {
386
+ name, codename, role, specialization, description, goal, tags,
387
+ soul: soul || undefined,
388
+ skills: skills || undefined,
389
+ heartbeat: heartbeat || undefined,
390
+ llm: llmProvider ? { provider: llmProvider, model: llmModel, temperature, maxTokens } : undefined,
391
+ scheduling: runMode !== 'manual' ? { mode: runMode, schedule, maxConcurrent, timeout: timeout || undefined } : undefined,
392
+ resources: { cpu, memory, disk, network },
393
+ mcpTools: selectedTools.length > 0 ? selectedTools : undefined,
394
+ envVars: envVars.reduce((acc, v) => { if (v.key) acc[v.key] = v.value; return acc; }, {}),
395
+ team: (swarm || supervisor) ? { swarm: swarm || undefined, supervisor: supervisor || undefined } : undefined,
396
+ };
397
+ const res = await createAgent(identity);
329
398
  setResult(res);
330
- if (!res.error) {
331
- onCreated();
332
- setName(''); setCodename(''); setRole(''); setSpecialization(''); setSoul('');
333
- }
399
+ if (!res.error) onCreated();
334
400
  setCreating(false);
335
401
  }
336
402
 
403
+ const TABS = [
404
+ { id: 'basic', label: 'Basic', icon: UsersIcon },
405
+ { id: 'llm', label: 'LLM', icon: CpuIcon },
406
+ { id: 'soul', label: 'Soul', icon: PencilIcon },
407
+ { id: 'tools', label: 'Tools', icon: PackageIcon },
408
+ { id: 'schedule', label: 'Schedule', icon: ClockIcon },
409
+ { id: 'resources', label: 'Resources', icon: SlidersIcon },
410
+ { id: 'files', label: 'Files', icon: FolderIcon },
411
+ { id: 'env', label: 'Env', icon: HashIcon },
412
+ { id: 'team', label: 'Team', icon: UsersIcon },
413
+ ];
414
+
337
415
  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">
416
+ <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }} className="overflow-hidden">
417
+ <div className="rounded-lg border border-[--cyan]/20 bg-[--card] mt-4 overflow-hidden">
418
+ {/* Header */}
419
+ <div className="flex items-center justify-between p-4 border-b border-white/[0.06]">
346
420
  <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>
421
+ <div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-[#ff5f57]" /><div className="w-2 h-2 rounded-full bg-[#febc2e]" /><div className="w-2 h-2 rounded-full bg-[#28c840]" /></div>
422
+ <span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider ml-1">Create New Agent</span>
355
423
  </div>
356
424
  <button onClick={onClose} className="text-muted-foreground hover:text-foreground"><XIcon size={14} /></button>
357
425
  </div>
358
426
 
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>
427
+ {/* Tabs */}
428
+ <div className="flex flex-wrap gap-1.5 p-4 pb-0 overflow-x-auto scrollbar-thin">
429
+ {TABS.map(t => <TabBtn key={t.id} active={tab === t.id} onClick={() => setTab(t.id)} icon={t.icon} label={t.label} />)}
380
430
  </div>
381
431
 
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>
432
+ <div className="p-4">
433
+ {/* ── Basic Tab ──────────────────────────────────────────────────── */}
434
+ {tab === 'basic' && (
435
+ <div className="flex flex-col gap-4">
436
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
437
+ <FormField label="Name">
438
+ <input value={name} onChange={e => setName(e.target.value)} placeholder="Reaper" className={inputClass} />
439
+ </FormField>
440
+ <FormField label="Codename (auto-generated)">
441
+ <input value={codename} onChange={e => setCodename(e.target.value)} placeholder="REAPER" className={inputClass} />
442
+ </FormField>
443
+ <FormField label="Role">
444
+ <select value={role} onChange={e => setRole(e.target.value)} className={selectClass}>
445
+ <option value="">Select role...</option>
446
+ {ROLE_OPTIONS.map(r => <option key={r} value={r}>{r}</option>)}
447
+ </select>
448
+ </FormField>
449
+ <FormField label="Specialization">
450
+ <select value={specialization} onChange={e => setSpecialization(e.target.value)} className={selectClass}>
451
+ <option value="">Select specialization...</option>
452
+ {SPEC_OPTIONS.map(s => <option key={s} value={s}>{s}</option>)}
453
+ </select>
454
+ </FormField>
455
+ </div>
456
+ <FormField label="Description">
457
+ <input value={description} onChange={e => setDescription(e.target.value)} placeholder="Brief purpose statement" className={inputClass} />
458
+ </FormField>
459
+ <FormField label="Goal">
460
+ <textarea value={goal} onChange={e => setGoal(e.target.value)} placeholder="Specific mission objective..." rows={2} className={textareaClass} />
461
+ </FormField>
462
+ <FormField label="Tags">
463
+ <div className="flex flex-wrap gap-1.5 mb-2">
464
+ {tags.map(t => (
465
+ <span key={t} className="inline-flex items-center gap-1 rounded-full bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 px-2 py-0.5 text-[10px] font-mono">
466
+ {t}
467
+ <button onClick={() => setTags(tags.filter(x => x !== t))} className="hover:text-[--destructive]"><XIcon size={8} /></button>
468
+ </span>
469
+ ))}
470
+ </div>
471
+ <div className="flex gap-2">
472
+ <input value={tagInput} onChange={e => setTagInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), addTag())}
473
+ placeholder="Add tag..." className={inputClass} />
474
+ <button onClick={addTag} className="shrink-0 inline-flex items-center gap-1 rounded-md px-3 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors">
475
+ <PlusIcon size={12} />
476
+ </button>
477
+ </div>
478
+ </FormField>
479
+ </div>
480
+ )}
387
481
 
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
- )}
482
+ {/* ── LLM Tab ────────────────────────────────────────────────────── */}
483
+ {tab === 'llm' && (
484
+ <div className="flex flex-col gap-4">
485
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
486
+ <FormField label="Provider">
487
+ <select value={llmProvider} onChange={e => { setLlmProvider(e.target.value); setLlmModel(''); }} className={selectClass}>
488
+ <option value="">Default (inherit)</option>
489
+ <option value="anthropic">Anthropic</option>
490
+ <option value="openai">OpenAI</option>
491
+ <option value="google">Google</option>
492
+ <option value="custom">Custom</option>
493
+ </select>
494
+ </FormField>
495
+ <FormField label="Model">
496
+ <select value={llmModel} onChange={e => setLlmModel(e.target.value)} className={selectClass} disabled={!llmProvider}>
497
+ <option value="">Select model...</option>
498
+ {(PROVIDER_MODELS[llmProvider] || []).map(m => <option key={m} value={m}>{m}</option>)}
499
+ </select>
500
+ {llmProvider === 'custom' && (
501
+ <input value={llmModel} onChange={e => setLlmModel(e.target.value)} placeholder="model-name" className={`${inputClass} mt-2`} />
502
+ )}
503
+ </FormField>
504
+ </div>
505
+ <FormField label={`Temperature: ${temperature}`}>
506
+ <input type="range" min="0" max="1" step="0.05" value={temperature} onChange={e => setTemperature(parseFloat(e.target.value))}
507
+ className="w-full accent-[--cyan]" />
508
+ <div className="flex justify-between text-[9px] font-mono text-muted-foreground mt-1">
509
+ <span>Precise (0)</span><span>Creative (1)</span>
510
+ </div>
511
+ </FormField>
512
+ <FormField label="Max Tokens">
513
+ <div className="flex items-center gap-2">
514
+ <input type="number" value={maxTokens} onChange={e => setMaxTokens(parseInt(e.target.value) || 4096)} className={inputClass} />
515
+ <div className="flex gap-1">
516
+ {[4096, 8192, 16384].map(v => (
517
+ <button key={v} onClick={() => setMaxTokens(v)}
518
+ className={`px-2 py-1 text-[10px] font-mono rounded border transition-colors ${maxTokens === v ? 'bg-[--cyan]/10 text-[--cyan] border-[--cyan]/20' : 'border-white/[0.06] text-muted-foreground hover:text-foreground'}`}>
519
+ {v >= 1000 ? `${v/1000}k` : v}
520
+ </button>
521
+ ))}
522
+ </div>
523
+ </div>
524
+ </FormField>
525
+ </div>
526
+ )}
393
527
 
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>
528
+ {/* ── Soul Tab ───────────────────────────────────────────────────── */}
529
+ {tab === 'soul' && (
530
+ <div className="flex flex-col gap-4">
531
+ <FormField label="Template">
532
+ <div className="flex gap-2 mb-2">
533
+ {Object.entries({ recon: 'Recon Agent', exploit: 'Exploit Dev', report: 'Report Writer', custom: 'Custom' }).map(([k, label]) => (
534
+ <button key={k} onClick={() => applySoulTemplate(k)}
535
+ className={`px-3 py-1.5 text-xs font-mono rounded-md border transition-colors ${
536
+ soulTemplate === k ? 'bg-[--cyan]/10 text-[--cyan] border-[--cyan]/20' : 'border-white/[0.06] text-muted-foreground hover:text-foreground hover:bg-white/[0.04]'
537
+ }`}>{label}</button>
538
+ ))}
539
+ </div>
540
+ </FormField>
541
+ <FormField label="SOUL.md">
542
+ <textarea value={soul} onChange={e => setSoul(e.target.value)} rows={10} placeholder="Agent personality and system prompt..."
543
+ className={textareaClass} />
544
+ </FormField>
545
+ <FormField label="SKILLS.md (optional)">
546
+ <textarea value={skills} onChange={e => setSkills(e.target.value)} rows={4} placeholder="Define agent skills and capabilities..."
547
+ className={textareaClass} />
548
+ </FormField>
549
+ <FormField label="HEARTBEAT.md (optional)">
550
+ <textarea value={heartbeat} onChange={e => setHeartbeat(e.target.value)} rows={3} placeholder="Self-monitoring behavior..."
551
+ className={textareaClass} />
552
+ </FormField>
553
+ </div>
554
+ )}
555
+
556
+ {/* ── Tools Tab ──────────────────────────────────────────────────── */}
557
+ {tab === 'tools' && (
558
+ <div className="flex flex-col gap-4">
559
+ <p className="text-xs font-mono text-muted-foreground">Select MCP tools this agent can use.</p>
560
+ {loadingTools ? (
561
+ <div className="flex items-center gap-2 py-4"><SpinnerIcon size={14} /><span className="text-xs font-mono text-muted-foreground">Loading tools...</span></div>
562
+ ) : mcpTools.length === 0 ? (
563
+ <div className="text-xs font-mono text-muted-foreground py-4">No MCP tools configured. Add tools in Settings &gt; MCP.</div>
564
+ ) : (
565
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 max-h-[300px] overflow-y-auto scrollbar-thin">
566
+ {mcpTools.map(tool => {
567
+ const checked = selectedTools.includes(tool.name);
568
+ return (
569
+ <label key={tool.name}
570
+ className={`flex items-start gap-2.5 p-2.5 rounded-lg border cursor-pointer transition-colors ${
571
+ checked ? 'border-[--cyan]/30 bg-[--cyan]/5' : 'border-white/[0.06] bg-[--card] hover:border-white/[0.12]'
572
+ }`}>
573
+ <input type="checkbox" checked={checked}
574
+ onChange={e => setSelectedTools(e.target.checked ? [...selectedTools, tool.name] : selectedTools.filter(t => t !== tool.name))}
575
+ className="mt-0.5 accent-[--cyan]" />
576
+ <div className="min-w-0">
577
+ <p className="text-xs font-mono font-medium truncate">{tool.name}</p>
578
+ {tool.description && <p className="text-[10px] font-mono text-muted-foreground truncate mt-0.5">{tool.description}</p>}
579
+ </div>
580
+ </label>
581
+ );
582
+ })}
583
+ </div>
584
+ )}
585
+ {selectedTools.length > 0 && (
586
+ <div className="flex flex-wrap gap-1.5">
587
+ <span className="text-[10px] font-mono text-muted-foreground">Selected:</span>
588
+ {selectedTools.map(t => (
589
+ <span key={t} className="inline-flex items-center gap-1 rounded-full bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 px-2 py-0.5 text-[9px] font-mono">
590
+ {t}<button onClick={() => setSelectedTools(selectedTools.filter(x => x !== t))}><XIcon size={8} /></button>
591
+ </span>
592
+ ))}
593
+ </div>
594
+ )}
595
+ </div>
596
+ )}
597
+
598
+ {/* ── Schedule Tab ───────────────────────────────────────────────── */}
599
+ {tab === 'schedule' && (
600
+ <div className="flex flex-col gap-4">
601
+ <FormField label="Run Mode">
602
+ <select value={runMode} onChange={e => setRunMode(e.target.value)} className={selectClass}>
603
+ {RUN_MODES.map(m => <option key={m} value={m}>{m}</option>)}
604
+ </select>
605
+ </FormField>
606
+ {runMode === 'scheduled' && (
607
+ <FormField label="Cron Schedule">
608
+ <input value={schedule} onChange={e => setSchedule(e.target.value)} placeholder="0 */6 * * *" className={inputClass} />
609
+ <p className="text-[9px] font-mono text-muted-foreground mt-1">Standard cron expression (min hour dom month dow)</p>
610
+ </FormField>
611
+ )}
612
+ <div className="grid grid-cols-2 gap-3">
613
+ <FormField label="Max Concurrent Jobs">
614
+ <select value={maxConcurrent} onChange={e => setMaxConcurrent(parseInt(e.target.value))} className={selectClass}>
615
+ {[1, 2, 5, 10].map(v => <option key={v} value={v}>{v}</option>)}
616
+ </select>
617
+ </FormField>
618
+ <FormField label="Timeout (seconds, 0=none)">
619
+ <input type="number" value={timeout} onChange={e => setTimeout_(parseInt(e.target.value) || 0)} className={inputClass} min="0" />
620
+ </FormField>
621
+ </div>
622
+ </div>
623
+ )}
624
+
625
+ {/* ── Resources Tab ──────────────────────────────────────────────── */}
626
+ {tab === 'resources' && (
627
+ <div className="flex flex-col gap-4">
628
+ <div className="grid grid-cols-2 gap-3">
629
+ <FormField label="CPU Cores">
630
+ <select value={cpu} onChange={e => setCpu(e.target.value)} className={selectClass}>
631
+ {['0.5', '1', '2', '4'].map(v => <option key={v} value={v}>{v} core{v !== '1' ? 's' : ''}</option>)}
632
+ </select>
633
+ </FormField>
634
+ <FormField label="Memory">
635
+ <select value={memory} onChange={e => setMemory(e.target.value)} className={selectClass}>
636
+ {['512MB', '1GB', '2GB', '4GB', '8GB'].map(v => <option key={v} value={v}>{v}</option>)}
637
+ </select>
638
+ </FormField>
639
+ <FormField label="Disk">
640
+ <select value={disk} onChange={e => setDisk(e.target.value)} className={selectClass}>
641
+ {['1GB', '5GB', '10GB', 'Unlimited'].map(v => <option key={v} value={v}>{v}</option>)}
642
+ </select>
643
+ </FormField>
644
+ <FormField label="Network">
645
+ <select value={network} onChange={e => setNetwork(e.target.value)} className={selectClass}>
646
+ <option value="isolated">Isolated</option>
647
+ <option value="internal">Internal only</option>
648
+ <option value="full">Full internet</option>
649
+ </select>
650
+ </FormField>
651
+ </div>
652
+ </div>
653
+ )}
654
+
655
+ {/* ── Files Tab ──────────────────────────────────────────────────── */}
656
+ {tab === 'files' && (
657
+ <div className="flex flex-col gap-4">
658
+ <FormField label="Working Directory">
659
+ <input value={workDir} onChange={e => setWorkDir(e.target.value)} className={inputClass} />
660
+ </FormField>
661
+ <FormField label="Allowed Paths (comma-separated)">
662
+ <input value={allowedPaths} onChange={e => setAllowedPaths(e.target.value)} placeholder="/workspace, /tmp" className={inputClass} />
663
+ </FormField>
664
+ </div>
665
+ )}
666
+
667
+ {/* ── Env Tab ────────────────────────────────────────────────────── */}
668
+ {tab === 'env' && (
669
+ <div className="flex flex-col gap-3">
670
+ <p className="text-xs font-mono text-muted-foreground">Agent-specific environment variables.</p>
671
+ {envVars.map((v, i) => (
672
+ <div key={i} className="flex gap-2">
673
+ <input value={v.key} onChange={e => { const nv = [...envVars]; nv[i] = { ...nv[i], key: e.target.value }; setEnvVars(nv); }}
674
+ placeholder="KEY" className={inputClass} />
675
+ <input value={v.value} onChange={e => { const nv = [...envVars]; nv[i] = { ...nv[i], value: e.target.value }; setEnvVars(nv); }}
676
+ placeholder="value" className={inputClass} />
677
+ <button onClick={() => setEnvVars(envVars.filter((_, j) => j !== i))}
678
+ className="shrink-0 p-2 text-muted-foreground hover:text-[--destructive] transition-colors"><TrashIcon size={12} /></button>
679
+ </div>
680
+ ))}
681
+ <button onClick={addEnvVar}
682
+ className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-mono font-medium border border-dashed border-white/[0.12] hover:bg-white/[0.04] transition-colors text-muted-foreground hover:text-foreground self-start">
683
+ <PlusIcon size={12} /> Add Variable
684
+ </button>
685
+ </div>
686
+ )}
687
+
688
+ {/* ── Team Tab ───────────────────────────────────────────────────── */}
689
+ {tab === 'team' && (
690
+ <div className="flex flex-col gap-4">
691
+ <FormField label="Part of Swarm">
692
+ <select value={swarm} onChange={e => setSwarm(e.target.value)} className={selectClass}>
693
+ <option value="">None</option>
694
+ <option value="recon-team">Recon Team</option>
695
+ <option value="exploit-team">Exploit Team</option>
696
+ <option value="full-swarm">Full Swarm</option>
697
+ </select>
698
+ </FormField>
699
+ <FormField label="Supervisor Agent">
700
+ <input value={supervisor} onChange={e => setSupervisor(e.target.value)} placeholder="@OVERSEER" className={inputClass} />
701
+ </FormField>
702
+ </div>
703
+ )}
704
+
705
+ {/* ── Actions ────────────────────────────────────────────────────── */}
706
+ <div className="flex items-center gap-2 mt-4 pt-4 border-t border-white/[0.06]">
707
+ <button onClick={handleCreate} disabled={creating || (!name && !codename)}
708
+ 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">
709
+ {creating ? <SpinnerIcon size={12} /> : <PlusIcon size={12} />} Create Agent
710
+ </button>
711
+ <button onClick={handleTest} disabled={testing}
712
+ className="inline-flex items-center gap-1.5 rounded-md px-3 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground hover:text-foreground disabled:opacity-50">
713
+ {testing ? <SpinnerIcon size={12} /> : <PlayIcon size={12} />} Validate
714
+ </button>
715
+ <button onClick={onClose}
716
+ 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">
717
+ Cancel
718
+ </button>
719
+ {result && (
720
+ <p className={`text-xs font-mono ml-2 ${result.error ? 'text-[--destructive]' : 'text-green-500'}`}>
721
+ {result.error || `Agent created: ${result.id}`}
722
+ </p>
723
+ )}
724
+ {testResult && (
725
+ <p className={`text-xs font-mono ml-2 ${testResult.success ? 'text-green-500' : 'text-yellow-500'}`}>
726
+ {testResult.success ? 'Configuration valid' : testResult.issues?.join(', ')}
727
+ </p>
728
+ )}
729
+ </div>
403
730
  </div>
404
731
  </div>
405
732
  </motion.div>
@@ -440,12 +767,10 @@ export function AgentsPage() {
440
767
 
441
768
  if (loading) {
442
769
  return (
443
- <div className="flex flex-col gap-4 p-6">
770
+ <div className="flex flex-col gap-4">
444
771
  <div className="h-8 w-48 animate-pulse rounded-lg bg-white/[0.04]" />
445
772
  <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
- ))}
773
+ {[...Array(4)].map((_, i) => <div key={i} className="h-20 animate-pulse rounded-lg bg-white/[0.04] border border-white/[0.06]" />)}
449
774
  </div>
450
775
  </div>
451
776
  );
@@ -453,33 +778,24 @@ export function AgentsPage() {
453
778
 
454
779
  return (
455
780
  <>
456
- {/* Header */}
457
781
  <div className="flex items-center justify-between mb-6">
458
782
  <div>
459
783
  <h1 className="text-2xl font-mono font-semibold text-[--cyan] text-glow-cyan">Agents</h1>
460
784
  <p className="text-[11px] text-muted-foreground mt-1 font-mono">Manage agent profiles, roles, and assignments</p>
461
785
  </div>
462
786
  <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>
787
+ <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">{agents.length} agents</span>
466
788
  {activeCount > 0 && (
467
789
  <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
790
+ <span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />{activeCount} active
470
791
  </span>
471
792
  )}
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
- >
793
+ <button onClick={() => setShowCreate(!showCreate)}
794
+ 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">
476
795
  <PlusIcon size={12} /> New Agent
477
796
  </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
- >
797
+ <button onClick={() => { setRefreshing(true); load(); }} disabled={refreshing}
798
+ 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">
483
799
  {refreshing ? <SpinnerIcon size={14} /> : <RefreshIcon size={14} />}
484
800
  </button>
485
801
  </div>
@@ -505,32 +821,24 @@ export function AgentsPage() {
505
821
  {agents.length > 0 && (
506
822
  <div className="relative mb-4">
507
823
  <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
- />
824
+ <input placeholder="Search agents..." value={search} onChange={e => setSearch(e.target.value)}
825
+ 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" />
514
826
  </div>
515
827
  )}
516
828
 
517
829
  {/* Create form */}
518
830
  <AnimatePresence>
519
- {showCreate && (
520
- <CreateAgentForm onCreated={() => { load(); setShowCreate(false); }} onClose={() => setShowCreate(false)} />
521
- )}
831
+ {showCreate && <CreateAgentForm onCreated={() => { load(); setShowCreate(false); }} onClose={() => setShowCreate(false)} />}
522
832
  </AnimatePresence>
523
833
 
524
834
  {/* Agent list */}
525
835
  <div className="flex flex-col gap-3 mt-4">
526
836
  {filtered.length === 0 && agents.length === 0 ? (
527
837
  <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>
838
+ <div className="rounded-full bg-[--cyan]/10 p-4 mb-4"><UsersIcon size={24} className="text-[--cyan]" /></div>
531
839
  <p className="text-sm font-mono font-medium mb-1">No agents configured</p>
532
840
  <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.
841
+ Create agent profiles in the <span className="text-[--cyan]">agents/</span> directory or click &ldquo;New Agent&rdquo; above.
534
842
  </p>
535
843
  </div>
536
844
  ) : filtered.length === 0 ? (
@@ -540,13 +848,7 @@ export function AgentsPage() {
540
848
  </div>
541
849
  ) : (
542
850
  filtered.map((agent, i) => (
543
- <AgentCard
544
- key={agent.id}
545
- agent={agent}
546
- onViewProfile={setViewingProfile}
547
- onAssignTask={setAssigningTask}
548
- index={i}
549
- />
851
+ <AgentCard key={agent.id} agent={agent} onViewProfile={setViewingProfile} onAssignTask={setAssigningTask} index={i} />
550
852
  ))
551
853
  )}
552
854
  </div>
@@ -554,17 +856,13 @@ export function AgentsPage() {
554
856
  {/* Profile panel */}
555
857
  <AnimatePresence>
556
858
  {viewingProfile && (
557
- <div className="mt-4">
558
- <AgentProfilePanel agentId={viewingProfile} onClose={() => setViewingProfile(null)} />
559
- </div>
859
+ <div className="mt-4"><AgentProfilePanel agentId={viewingProfile} onClose={() => setViewingProfile(null)} /></div>
560
860
  )}
561
861
  </AnimatePresence>
562
862
 
563
863
  {/* Assign task dialog */}
564
864
  <AnimatePresence>
565
- {assigningTask && (
566
- <AssignTaskDialog agent={assigningTask} onClose={() => setAssigningTask(null)} />
567
- )}
865
+ {assigningTask && <AssignTaskDialog agent={assigningTask} onClose={() => setAssigningTask(null)} />}
568
866
  </AnimatePresence>
569
867
  </>
570
868
  );