chore: checkpoint before DAG views UX overhaul

This commit is contained in:
zenchantlive 2026-02-22 20:43:59 -08:00
parent 5695125a75
commit a03def1ca1
125 changed files with 40711 additions and 581 deletions

View file

@ -34,6 +34,7 @@ interface AgentRosterEntry {
interface ActivityPanelProps {
issues: BeadIssue[];
collapsed?: boolean;
projectRoot: string;
}
const AGENT_LABEL = 'gt:agent';
@ -243,7 +244,7 @@ function getInitials(name: string): string {
return name.split(/[-_\s]/).map(p => p[0]).join('').toUpperCase().slice(0, 2);
}
export function ActivityPanel({ issues, collapsed = false }: ActivityPanelProps) {
export function ActivityPanel({ issues, collapsed = false, projectRoot }: ActivityPanelProps) {
const [activities, setActivities] = useState<ActivityEvent[]>([]);
const [isLoading, setIsLoading] = useState(true);
@ -270,13 +271,16 @@ export function ActivityPanel({ issues, collapsed = false }: ActivityPanelProps)
// Subscribe to real-time activity
useEffect(() => {
const source = new EventSource('/api/events');
console.log('[ActivityPanel] Connecting to SSE for:', projectRoot);
const source = new EventSource(`/api/events?projectRoot=${encodeURIComponent(projectRoot)}`);
const onActivity = (event: MessageEvent) => {
try {
const data = JSON.parse(event.data);
if (data?.event) {
setActivities(prev => [data.event, ...prev].slice(0, 50));
console.log('[ActivityPanel] Received activity event:', data);
// data IS the activity event directly (not wrapped in { event: ... })
if (data?.beadId) {
setActivities(prev => [data, ...prev].slice(0, 50));
}
} catch (e) {
// Ignore parse errors
@ -286,10 +290,11 @@ export function ActivityPanel({ issues, collapsed = false }: ActivityPanelProps)
source.addEventListener('activity', onActivity as EventListener);
return () => {
console.log('[ActivityPanel] Closing SSE connection');
source.removeEventListener('activity', onActivity as EventListener);
source.close();
};
}, []);
}, [projectRoot]);
const activeAgents = agentRoster.filter(a => a.status === 'active').length;
if (collapsed) {

View file

@ -27,47 +27,47 @@ export interface MissionCardProps {
}
const STATUS_CONFIG = {
planning: {
color: 'text-blue-400',
border: 'border-blue-500/30',
bg: 'bg-blue-500/5',
planning: {
color: 'text-blue-400',
border: 'border-blue-500/30',
bg: 'bg-blue-500/5',
label: 'PLANNING',
icon: Circle
},
active: {
color: 'text-emerald-400',
border: 'border-emerald-500/30',
bg: 'bg-emerald-500/5',
active: {
color: 'text-emerald-400',
border: 'border-emerald-500/30',
bg: 'bg-emerald-500/5',
label: 'ACTIVE',
icon: Activity
},
blocked: {
color: 'text-rose-400',
border: 'border-rose-500/30',
bg: 'bg-rose-500/5',
blocked: {
color: 'text-rose-400',
border: 'border-rose-500/30',
bg: 'bg-rose-500/5',
label: 'BLOCKED',
icon: AlertTriangle
},
completed: {
color: 'text-slate-400',
border: 'border-slate-500/30',
bg: 'bg-slate-500/5',
completed: {
color: 'text-slate-400',
border: 'border-slate-500/30',
bg: 'bg-slate-500/5',
label: 'COMPLETE',
icon: CheckCircle2
},
};
export function MissionCard({ id, projectRoot, title, description, status, stats, agents, onDeploy, onClick }: MissionCardProps) {
export function MissionCard({ id, projectRoot, title, description, status, agents, onDeploy, onClick }: MissionCardProps) {
const config = STATUS_CONFIG[status] || STATUS_CONFIG.planning;
const StatusIcon = config.icon;
const { topology, isLoading } = useSwarmTopology(projectRoot, id);
const isUnstaffed = agents.length === 0;
const isWorking = agents.some(a => a.status === 'working');
const showPulse = status === 'active' || isWorking;
return (
<Card
<Card
onClick={onClick}
className="group relative flex flex-col h-[320px] cursor-pointer overflow-hidden rounded-2xl border border-[var(--ui-border-soft)] bg-[var(--ui-bg-card)] hover:border-[var(--ui-accent-info)] hover:shadow-xl hover:shadow-black/20 transition-all duration-300"
>
@ -106,7 +106,7 @@ export function MissionCard({ id, projectRoot, title, description, status, stats
{/* GRAPH VISUALIZATION */}
<div className="px-5 py-2 flex-1 flex flex-col justify-end">
<SwarmGraph topology={topology} isLoading={isLoading} />
<SwarmGraph topology={topology} isLoading={isLoading} />
</div>
{/* FOOTER: SQUAD */}
@ -129,14 +129,14 @@ export function MissionCard({ id, projectRoot, title, description, status, stats
)}
</div>
<Button
size="sm"
<Button
size="sm"
variant="ghost"
onClick={(e) => { e.stopPropagation(); onDeploy(); }}
className={cn(
"h-7 px-3 text-[10px] font-bold uppercase tracking-wider border transition-all",
isUnstaffed
? "border-blue-500/20 text-blue-400 bg-blue-500/5 hover:bg-blue-500/10 hover:border-blue-500/40"
isUnstaffed
? "border-blue-500/20 text-blue-400 bg-blue-500/5 hover:bg-blue-500/10 hover:border-blue-500/40"
: "border-slate-700 text-slate-400 hover:text-white hover:bg-white/5 hover:border-slate-500"
)}
>

View file

@ -1,7 +1,6 @@
'use client';
import { useMemo } from 'react';
import { motion } from 'framer-motion';
import type { SwarmTopologyData } from '../../hooks/use-swarm-topology';
interface SwarmGraphProps {
@ -12,27 +11,26 @@ interface SwarmGraphProps {
export function SwarmGraph({ topology, isLoading }: SwarmGraphProps) {
const nodes = useMemo(() => {
if (!topology) return [];
// Simple layout strategy: Clusters
// Done: Left side
// Active: Center
// Ready: Right
// Blocked: Bottom Right
const output: React.ReactNode[] = [];
const scale = 0.5;
// 1. Completed (Green Cluster)
topology.completed.forEach((item, i) => {
const col = i % 5;
const row = Math.floor(i / 5);
output.push(
<circle
<circle
key={`done-${item.id}`}
cx={20 + (col * 8)}
cy={20 + (row * 8)}
r={2.5}
fill="#34d399"
cx={20 + (col * 8)}
cy={20 + (row * 8)}
r={2.5}
fill="#34d399"
opacity={0.5}
/>
);
@ -40,32 +38,32 @@ export function SwarmGraph({ topology, isLoading }: SwarmGraphProps) {
// 2. Active (Pulsing Center)
topology.active.forEach((item, i) => {
const cx = 140 + (i * 20);
const cy = 30 + (i % 2) * 10;
output.push(
<g key={`active-${item.id}`}>
<circle cx={cx} cy={cy} r={6} fill="#10b981" className="animate-pulse" />
<circle cx={cx} cy={cy} r={3} fill="#ecfdf5" />
</g>
);
const cx = 140 + (i * 20);
const cy = 30 + (i % 2) * 10;
output.push(
<g key={`active-${item.id}`}>
<circle cx={cx} cy={cy} r={6} fill="#10b981" className="animate-pulse" />
<circle cx={cx} cy={cy} r={3} fill="#ecfdf5" />
</g>
);
});
// 3. Ready (White Pipeline)
topology.ready.forEach((item, i) => {
const cx = 220 + (i * 10);
const cy = 30;
output.push(
<circle key={`ready-${item.id}`} cx={cx} cy={cy} r={3} fill="#94a3b8" />
);
const cx = 220 + (i * 10);
const cy = 30;
output.push(
<circle key={`ready-${item.id}`} cx={cx} cy={cy} r={3} fill="#94a3b8" />
);
});
// 4. Blocked (Red Hazard)
topology.blocked.forEach((item, i) => {
const cx = 220 + (i * 10);
const cy = 50;
output.push(
<circle key={`blocked-${item.id}`} cx={cx} cy={cy} r={3} fill="#f43f5e" />
);
const cx = 220 + (i * 10);
const cy = 50;
output.push(
<circle key={`blocked-${item.id}`} cx={cx} cy={cy} r={3} fill="#f43f5e" />
);
});
return output;
@ -80,11 +78,11 @@ export function SwarmGraph({ topology, isLoading }: SwarmGraphProps) {
}
if (!topology || (topology.completed.length === 0 && topology.active.length === 0 && topology.ready.length === 0)) {
return (
<div className="h-16 w-full flex items-center justify-center bg-black/20 rounded-lg border border-dashed border-slate-800">
<span className="text-[10px] text-slate-600 font-mono">EMPTY SIGNAL</span>
</div>
);
return (
<div className="h-16 w-full flex items-center justify-center bg-black/20 rounded-lg border border-dashed border-slate-800">
<span className="text-[10px] text-slate-600 font-mono">EMPTY SIGNAL</span>
</div>
);
}
return (
@ -93,9 +91,9 @@ export function SwarmGraph({ topology, isLoading }: SwarmGraphProps) {
{/* Connection Lines (Abstract) */}
<path d="M 60 30 L 130 30" stroke="#334155" strokeWidth="1" strokeDasharray="4 4" />
<path d="M 180 30 L 210 30" stroke="#334155" strokeWidth="1" strokeDasharray="4 4" />
{nodes}
{/* Labels */}
<text x="30" y="55" fontSize="8" fill="#475569" textAnchor="middle" fontFamily="monospace">DONE</text>
<text x="150" y="55" fontSize="8" fill="#10b981" textAnchor="middle" fontFamily="monospace" fontWeight="bold">ACTIVE</text>

View file

@ -93,8 +93,6 @@ export function ThreadDrawer({
const [saveError, setSaveError] = useState<string | null>(null);
const [saveState, setSaveState] = useState<'ready' | 'saving' | 'saved' | 'error'>('ready');
const [comments, setComments] = useState<CommentFromApi[]>([]);
const [commentsLoading, setCommentsLoading] = useState(false);
// Fetch comments when drawer opens
useEffect(() => {
if (!isOpen || !id || !projectRoot) {
@ -103,7 +101,6 @@ export function ThreadDrawer({
}
const fetchComments = async () => {
setCommentsLoading(true);
try {
const response = await fetch(`/api/beads/${id}/comments?projectRoot=${encodeURIComponent(projectRoot)}`);
const payload = (await response.json()) as { ok: boolean; comments?: CommentFromApi[] };
@ -112,8 +109,6 @@ export function ThreadDrawer({
}
} catch (error) {
console.error('Failed to fetch comments:', error);
} finally {
setCommentsLoading(false);
}
};
@ -239,12 +234,12 @@ export function ThreadDrawer({
const frameShellStyle = takeover
? undefined
: {
width: embedded ? '100%' : '26rem',
background: 'linear-gradient(180deg, var(--ui-bg-card), var(--ui-bg-shell))',
borderLeft: embedded ? 'none' : '1px solid var(--color-border-default)',
boxShadow: embedded ? 'none' : '-20px 0 48px rgba(0,0,0,0.45)',
overscrollBehavior: 'contain' as const,
};
width: embedded ? '100%' : '26rem',
background: 'linear-gradient(180deg, var(--ui-bg-card), var(--ui-bg-shell))',
borderLeft: embedded ? 'none' : '1px solid var(--color-border-default)',
boxShadow: embedded ? 'none' : '-20px 0 48px rgba(0,0,0,0.45)',
overscrollBehavior: 'contain' as const,
};
const conversationSection = (
<section className="rounded-xl border border-[var(--ui-border-soft)] bg-[var(--ui-bg-shell)] p-3 shadow-[0_12px_28px_-22px_rgba(0,0,0,0.7)]">
@ -406,34 +401,34 @@ export function ThreadDrawer({
style={
isMobile
? {
paddingTop: takeover ? 'max(1rem, env(safe-area-inset-top))' : undefined,
paddingBottom: takeover ? 'max(1rem, env(safe-area-inset-bottom))' : undefined,
}
paddingTop: takeover ? 'max(1rem, env(safe-area-inset-top))' : undefined,
paddingBottom: takeover ? 'max(1rem, env(safe-area-inset-bottom))' : undefined,
}
: undefined
}
>
<div className={frameShellClass} style={frameShellStyle}>
<header className="border-b border-[var(--ui-border-soft)] bg-[var(--ui-bg-shell)] px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="mb-1 flex items-center gap-2">
<p className="font-mono text-xs font-semibold text-[var(--ui-accent-info)]">#{id}</p>
<span className="rounded-full border border-[var(--ui-accent-ready)]/45 bg-[var(--ui-accent-ready)]/20 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] text-[#d8ffe8]">
In Progress
</span>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="mb-1 flex items-center gap-2">
<p className="font-mono text-xs font-semibold text-[var(--ui-accent-info)]">#{id}</p>
<span className="rounded-full border border-[var(--ui-accent-ready)]/45 bg-[var(--ui-accent-ready)]/20 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.1em] text-[#d8ffe8]">
In Progress
</span>
</div>
<h2 className="truncate text-[40px] font-semibold leading-[1.12] tracking-[-0.02em] text-[var(--ui-text-primary)]" title={title}>{title}</h2>
<p className="mt-1 text-xs text-[var(--ui-text-muted)]">{threadItems.length} events</p>
</div>
<h2 className="truncate text-[40px] font-semibold leading-[1.12] tracking-[-0.02em] text-[var(--ui-text-primary)]" title={title}>{title}</h2>
<p className="mt-1 text-xs text-[var(--ui-text-muted)]">{threadItems.length} events</p>
<Button
onClick={onClose}
variant="ghost"
className="h-8 w-8 rounded-full p-0 text-[var(--ui-text-muted)] hover:bg-white/10 hover:text-[var(--ui-text-primary)]"
aria-label="Close thread"
>
<X className="h-4 w-4" />
</Button>
</div>
<Button
onClick={onClose}
variant="ghost"
className="h-8 w-8 rounded-full p-0 text-[var(--ui-text-muted)] hover:bg-white/10 hover:text-[var(--ui-text-primary)]"
aria-label="Close thread"
>
<X className="h-4 w-4" />
</Button>
</div>
</header>
<ScrollArea className="flex-1">
@ -457,11 +452,11 @@ export function ThreadDrawer({
style={
isMobile
? {
paddingBottom: 'max(0.75rem, env(safe-area-inset-bottom))',
position: 'sticky',
bottom: 0,
zIndex: 10,
}
paddingBottom: 'max(0.75rem, env(safe-area-inset-bottom))',
position: 'sticky',
bottom: 0,
zIndex: 10,
}
: undefined
}
>

View file

@ -1,6 +1,6 @@
'use client';
import { useMemo, useState } from 'react';
import { useMemo, useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import type { BeadIssue } from '../../lib/types';
import type { ProjectScopeOption } from '../../lib/project-scope';
@ -17,6 +17,7 @@ import { SwarmMissionPicker } from '../swarm/swarm-mission-picker';
import { buildSocialCards } from '../../lib/social-cards';
import { ActivityPanel } from '../activity/activity-panel';
import { useSwarmList } from '../../hooks/use-swarm-list';
import { useBeadsSubscription } from '../../hooks/use-beads-subscription';
export interface UnifiedShellProps {
issues: BeadIssue[];
@ -27,13 +28,16 @@ export interface UnifiedShellProps {
}
export function UnifiedShell({
issues,
issues: initialIssues,
projectRoot,
projectScopeOptions,
}: UnifiedShellProps) {
const router = useRouter();
const { view, taskId, setTaskId, swarmId, setSwarmId, graphTab, setGraphTab, panel, drawer, setDrawer, epicId, setEpicId } = useUrlState();
// Subscribe to SSE for real-time updates on ALL views
const { issues, refresh } = useBeadsSubscription(initialIssues, projectRoot);
const [filters, setFilters] = useState<LeftPanelFilters>({
query: '',
status: 'all',
@ -116,6 +120,7 @@ export function UnifiedShell({
<SwarmWorkspace
selectedMissionId={swarmId ?? undefined}
issues={filteredIssues}
projectRoot={projectRoot}
/>
);
}
@ -157,7 +162,7 @@ export function UnifiedShell({
{/* RIGHT PANEL: Activity or Custom */}
<RightPanel isOpen={panel === 'open'}>
{customRightPanel || <ActivityPanel issues={issues} />}
{customRightPanel || <ActivityPanel issues={issues} projectRoot={projectRoot} />}
</RightPanel>
</div>

View file

@ -1,108 +1,262 @@
import React from 'react';
import { X, Save, ShieldAlert } from 'lucide-react';
"use client";
import React, { useState, useEffect } from 'react';
import { X, Save, ShieldAlert, Trash2, Plus } from 'lucide-react';
import type { AgentArchetype } from '../../lib/types-swarm';
interface ArchetypeInspectorProps {
archetype: AgentArchetype;
archetype?: AgentArchetype;
onClose: () => void;
onSave: (data: Partial<AgentArchetype>) => Promise<void>;
onDelete?: (id: string) => Promise<void>;
}
export function ArchetypeInspector({ archetype, onClose }: ArchetypeInspectorProps) {
if (!archetype) return null;
export function ArchetypeInspector({ archetype, onClose, onSave, onDelete }: ArchetypeInspectorProps) {
const isNew = !archetype;
const [name, setName] = useState(archetype?.name || '');
const [description, setDescription] = useState(archetype?.description || '');
const [systemPrompt, setSystemPrompt] = useState(archetype?.systemPrompt || '');
const [capabilities, setCapabilities] = useState<string[]>(archetype?.capabilities || []);
const [color, setColor] = useState(archetype?.color || '#3b82f6');
const [newCapability, setNewCapability] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (archetype) {
setName(archetype.name);
setDescription(archetype.description);
setSystemPrompt(archetype.systemPrompt);
setCapabilities(archetype.capabilities);
setColor(archetype.color);
}
}, [archetype]);
const handleAddCapability = () => {
if (newCapability.trim()) {
setCapabilities([...capabilities, newCapability.trim().toLowerCase()]);
setNewCapability('');
}
};
const handleRemoveCapability = (index: number) => {
setCapabilities(capabilities.filter((_, i) => i !== index));
};
const handleSave = async () => {
if (!name.trim() || !systemPrompt.trim()) {
setError('Name and System Prompt are required');
return;
}
setIsSaving(true);
setError(null);
try {
await onSave({
id: archetype?.id,
name: name.trim(),
description: description.trim(),
systemPrompt: systemPrompt.trim(),
capabilities,
color,
isBuiltIn: archetype?.isBuiltIn
});
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save');
} finally {
setIsSaving(false);
}
};
const handleDelete = async () => {
if (!archetype || !onDelete) return;
if (!confirm(`Delete archetype "${archetype.name}"? This cannot be undone.`)) return;
setIsDeleting(true);
setError(null);
try {
await onDelete(archetype.id);
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete');
} finally {
setIsDeleting(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
<div className="flex flex-col h-[85vh] w-full max-w-2xl overflow-hidden rounded-xl border border-[var(--ui-border-soft)] bg-[#0f1824] shadow-2xl animate-in zoom-in-95 duration-200">
{/* Header */}
<div className="flex items-center justify-between border-b border-[var(--ui-border-soft)] px-5 py-4 bg-[#14202e]">
<div className="flex items-center gap-3">
<div
className="h-10 w-10 rounded-lg flex items-center justify-center font-bold text-lg border"
style={{ backgroundColor: `${archetype.color}15`, color: archetype.color, borderColor: `${archetype.color}30` }}
style={{ backgroundColor: `${color}15`, color: color, borderColor: `${color}30` }}
>
{archetype.name.charAt(0)}
{name.charAt(0) || '?'}
</div>
<div>
<h2 className="text-lg font-bold text-[var(--ui-text-primary)] leading-tight">{archetype.name}</h2>
<p className="font-mono uppercase tracking-wider text-[10px] text-[var(--ui-text-muted)] mt-0.5">{archetype.id}</p>
<h2 className="text-lg font-bold text-[var(--ui-text-primary)] leading-tight">
{isNew ? 'New Archetype' : name || 'Edit Archetype'}
</h2>
{!isNew && (
<p className="font-mono uppercase tracking-wider text-[10px] text-[var(--ui-text-muted)] mt-0.5">{archetype.id}</p>
)}
</div>
</div>
<button
onClick={onClose}
className="rounded-full p-2 text-[var(--ui-text-muted)] hover:bg-white/5 hover:text-white transition-colors"
>
<button onClick={onClose} className="rounded-full p-2 text-[var(--ui-text-muted)] hover:bg-white/5 hover:text-white transition-colors">
<X className="w-5 h-5" />
</button>
</div>
{/* Body Content */}
<div className="flex-1 overflow-y-auto p-5 space-y-6 custom-scrollbar">
{error && (
<div className="mx-5 mt-4 p-3 bg-rose-500/10 border border-rose-500/20 rounded-lg text-rose-400 text-sm">
{error}
</div>
)}
{/* ReadOnly Warning if builtin */}
{archetype.isBuiltIn && (
<div className="flex items-start gap-3 bg-[var(--ui-accent-warning)]/10 border border-[var(--ui-accent-warning)]/20 p-3 rounded-lg text-[var(--ui-accent-warning)]">
<ShieldAlert className="w-5 h-5 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<span className="font-semibold">Built-in Archetype.</span> This is a core system role. You cannot delete it, but you can override its system prompt.
</div>
{archetype?.isBuiltIn && (
<div className="mx-5 mt-4 flex items-start gap-3 bg-[var(--ui-accent-warning)]/10 border border-[var(--ui-accent-warning)]/20 p-3 rounded-lg text-[var(--ui-accent-warning)]">
<ShieldAlert className="w-5 h-5 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<span className="font-semibold">Built-in Archetype.</span> This is a core system role. You cannot delete it.
</div>
)}
</div>
)}
{/* Metadata Section */}
<div className="space-y-4">
<div>
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Description</label>
<div className="flex-1 overflow-y-auto p-5 space-y-6 custom-scrollbar">
<div>
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Name *</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., System Architect"
className="w-full bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)]"
/>
</div>
<div>
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Description</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Brief description of this archetype's role"
className="w-full bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)]"
/>
</div>
<div>
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Color</label>
<div className="flex items-center gap-3">
<input
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
className="w-10 h-10 rounded cursor-pointer border border-[var(--ui-border-soft)]"
/>
<input
type="text"
defaultValue={archetype.description}
readOnly
className="w-full bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)]"
value={color}
onChange={(e) => setColor(e.target.value)}
className="flex-1 bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)]"
/>
</div>
<div>
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Capabilities</label>
<div className="flex flex-wrap gap-2">
{archetype.capabilities.map((cap, idx) => (
<span key={idx} className="px-2 py-1 rounded-md bg-white/5 text-[11px] uppercase font-semibold text-[var(--ui-text-muted)] border border-white/10">
{cap}
</span>
))}
</div>
</div>
</div>
<div className="border-t border-[var(--ui-border-soft)] pt-6">
<div className="flex flex-col h-[300px]">
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 flex items-center justify-between">
<span>System Prompt</span>
<span className="text-[10px] text-emerald-400 normal-case tracking-normal">Syntax: Markdown</span>
</label>
<textarea
defaultValue={archetype.systemPrompt}
readOnly
className="flex-1 w-full bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md p-4 text-sm text-[var(--ui-text-primary)] font-mono resize-none focus:outline-none focus:border-[var(--ui-accent-info)] custom-scrollbar leading-relaxed"
<div>
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">System Prompt *</label>
<textarea
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
placeholder="You are an expert software engineer..."
rows={6}
className="w-full bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] font-mono resize-y focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)] custom-scrollbar"
/>
</div>
<div>
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Capabilities</label>
<div className="flex gap-2 mb-3">
<input
type="text"
value={newCapability}
onChange={(e) => setNewCapability(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleAddCapability())}
placeholder="e.g., execute_code"
className="flex-1 bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)]"
/>
<button
type="button"
onClick={handleAddCapability}
disabled={!newCapability.trim()}
className="px-3 py-2 bg-[var(--ui-border-soft)] hover:bg-[var(--ui-border-hover)] text-white rounded-md transition-colors disabled:opacity-50 flex items-center gap-1 text-sm font-medium"
>
<Plus className="w-4 h-4" /> Add
</button>
</div>
{capabilities.length > 0 ? (
<div className="flex flex-wrap gap-2">
{capabilities.map((cap, i) => (
<div key={i} className="flex items-center gap-1.5 bg-[#14202e] border border-[var(--ui-border-soft)] px-2.5 py-1 rounded-full text-xs text-[var(--ui-text-primary)] isolate">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: color }}></div>
<span>{cap}</span>
<button
type="button"
onClick={() => handleRemoveCapability(i)}
className="ml-1 text-[var(--ui-text-muted)] hover:text-rose-400 transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
) : (
<div className="text-sm text-[var(--ui-text-muted)] italic py-2">No specific capabilities defined.</div>
)}
</div>
</div>
{/* Footer Controls */}
<div className="border-t border-[var(--ui-border-soft)] px-5 py-4 bg-[#14202e] flex justify-end gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-semibold text-[var(--ui-text-primary)] hover:bg-white/5 rounded-md transition-colors"
>
Cancel
</button>
<button
disabled
className="flex items-center gap-2 px-4 py-2 text-sm font-bold bg-[var(--ui-accent-info)] hover:bg-[var(--ui-accent-info)]/90 text-white rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save className="w-4 h-4" />
Save Changes
</button>
<div className="border-t border-[var(--ui-border-soft)] bg-[#0A111A] p-4 flex items-center justify-between flex-shrink-0 rounded-b-xl">
<div>
{!isNew && !archetype?.isBuiltIn && (
<button
type="button"
onClick={handleDelete}
disabled={isDeleting}
className="px-4 py-2 border border-rose-500/20 text-rose-400 hover:bg-rose-500/10 rounded-md text-sm font-medium transition-colors flex items-center gap-2 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<Trash2 className="w-4 h-4" />
{isDeleting ? 'Deleting...' : 'Delete'}
</button>
)}
</div>
<div className="flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-[var(--ui-text-muted)] hover:text-white transition-colors cursor-pointer"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={isSaving}
className="px-5 py-2 bg-[var(--ui-accent-info)] hover:bg-[var(--ui-accent-info)]/90 text-white rounded-md text-sm font-medium transition-colors flex items-center gap-2 shadow-lg shadow-blue-500/20 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save className="w-4 h-4" />
{isSaving ? 'Saving...' : (isNew ? 'Create Archetype' : 'Save Changes')}
</button>
</div>
</div>
</div>
</div>

View file

@ -20,7 +20,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Loader2, Plus, Rocket } from 'lucide-react';
import { Loader2, Rocket } from 'lucide-react';
interface LaunchSwarmDialogProps {
projectRoot: string;
@ -51,7 +51,7 @@ export function LaunchSwarmDialog({ projectRoot, onSuccess }: LaunchSwarmDialogP
} else {
setError(json.error);
}
} catch (e) {
} catch {
setError('Failed to fetch formulas');
} finally {
setLoading(false);
@ -82,7 +82,7 @@ export function LaunchSwarmDialog({ projectRoot, onSuccess }: LaunchSwarmDialogP
proto: selectedFormula,
}),
});
const json = await res.json();
if (json.ok) {
setOpen(false);
@ -92,7 +92,7 @@ export function LaunchSwarmDialog({ projectRoot, onSuccess }: LaunchSwarmDialogP
} else {
setError(json.error);
}
} catch (e) {
} catch {
setError('Failed to launch swarm');
} finally {
setLoading(false);
@ -102,9 +102,9 @@ export function LaunchSwarmDialog({ projectRoot, onSuccess }: LaunchSwarmDialogP
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
<Button
variant="outline"
size="sm"
className="gap-2 border-emerald-500/20 bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20 hover:text-emerald-300"
>
<Rocket className="h-4 w-4" />
@ -127,7 +127,7 @@ export function LaunchSwarmDialog({ projectRoot, onSuccess }: LaunchSwarmDialogP
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200">
{formulas.length === 0 && !loading && (
<div className="p-2 text-xs text-slate-500 text-center">No formulas found</div>
<div className="p-2 text-xs text-slate-500 text-center">No formulas found</div>
)}
{formulas.map((f) => (
<SelectItem key={f.name} value={f.name} className="focus:bg-slate-700 focus:text-slate-100">
@ -148,7 +148,7 @@ export function LaunchSwarmDialog({ projectRoot, onSuccess }: LaunchSwarmDialogP
disabled={loading}
/>
</div>
{error && (
<div className="text-xs text-rose-400 bg-rose-950/20 p-2 rounded border border-rose-900/30">
{error}
@ -156,9 +156,9 @@ export function LaunchSwarmDialog({ projectRoot, onSuccess }: LaunchSwarmDialogP
)}
</form>
<DialogFooter>
<Button
type="submit"
onClick={handleSubmit}
<Button
type="submit"
onClick={handleSubmit}
disabled={loading || !title || !selectedFormula}
className="bg-emerald-600 hover:bg-emerald-500 text-white"
>

View file

@ -5,7 +5,7 @@ import { Card } from '../../../components/ui/card';
import { Badge } from '../../../components/ui/badge';
import { Button } from '@/components/ui/button';
import { cn } from '../../lib/utils';
import { CheckCircle2, PlayCircle, Clock, AlertCircle, UserPlus, UserMinus, Activity } from 'lucide-react';
import { CheckCircle2, PlayCircle, Clock, AlertCircle, UserPlus, Activity } from 'lucide-react';
import { AgentAvatar } from '../shared/agent-avatar';
import { useAgentPool } from '../../hooks/use-agent-pool';
@ -13,7 +13,6 @@ interface SwarmControlCardProps {
card: SwarmCardData;
projectRoot: string;
onJoin?: () => void;
onLeave?: () => void;
isJoining?: boolean;
}
@ -21,17 +20,17 @@ function MiniGraph({ progress }: { progress: number }) {
// A simple visual indicator of progress complexity (mocked for now, but implies graph structure)
return (
<div className="flex h-8 items-end gap-0.5 opacity-50">
{[...Array(10)].map((_, i) => {
const height = Math.max(20, Math.random() * 80);
const active = (i * 10) < progress;
return (
<div
key={i}
{[...Array(10)].map((_, i) => {
const height = Math.max(20, Math.random() * 80);
const active = (i * 10) < progress;
return (
<div
key={i}
className={cn("w-1 rounded-t-sm transition-all", active ? "bg-emerald-500" : "bg-slate-700")}
style={{ height: `${active ? height : 20}%` }}
/>
)
})}
/>
)
})}
</div>
);
}
@ -42,14 +41,14 @@ const STATUS_COLORS: Record<string, string> = {
in_progress: 'text-amber-400 border-amber-400/30',
};
export function SwarmControlCard({ card, projectRoot, onJoin, onLeave, isJoining }: SwarmControlCardProps) {
export function SwarmControlCard({ card, projectRoot, onJoin, isJoining }: SwarmControlCardProps) {
const { getAgentsBySwarm } = useAgentPool(projectRoot);
const agents = getAgentsBySwarm(card.swarmId);
return (
<Card className="group relative overflow-hidden rounded-xl border border-[var(--ui-border-soft)] bg-[var(--ui-bg-card)] p-0 shadow-lg transition-all hover:border-[var(--ui-accent-info)] hover:shadow-xl">
{/* Background Decoration */}
<div className="absolute right-0 top-0 h-32 w-32 -translate-y-16 translate-x-16 rounded-full bg-emerald-500/5 blur-3xl transition-opacity group-hover:opacity-20" />
{/* Background Decoration */}
<div className="absolute right-0 top-0 h-32 w-32 -translate-y-16 translate-x-16 rounded-full bg-emerald-500/5 blur-3xl transition-opacity group-hover:opacity-20" />
<div className="flex flex-col h-full p-4 space-y-4">
{/* Header */}
@ -75,11 +74,11 @@ export function SwarmControlCard({ card, projectRoot, onJoin, onLeave, isJoining
{/* Visualizer */}
<div className="rounded-lg bg-black/20 p-2">
<div className="flex justify-between items-end mb-1">
<span className="text-[10px] text-slate-500 font-mono">ACTIVITY</span>
<span className="text-[10px] text-emerald-400 font-mono">{card.progressPercent}%</span>
</div>
<MiniGraph progress={card.progressPercent} />
<div className="flex justify-between items-end mb-1">
<span className="text-[10px] text-slate-500 font-mono">ACTIVITY</span>
<span className="text-[10px] text-emerald-400 font-mono">{card.progressPercent}%</span>
</div>
<MiniGraph progress={card.progressPercent} />
</div>
{/* Stats */}
@ -107,10 +106,10 @@ export function SwarmControlCard({ card, projectRoot, onJoin, onLeave, isJoining
<div className="flex -space-x-2">
{agents.slice(0, 3).map(agent => (
<div key={agent.agent_id} className="ring-2 ring-[var(--ui-bg-card)] rounded-full z-10">
<AgentAvatar
name={agent.display_name}
status={agent.status as any}
size="sm"
<AgentAvatar
name={agent.display_name}
status={agent.status as any}
size="sm"
/>
</div>
))}
@ -120,24 +119,24 @@ export function SwarmControlCard({ card, projectRoot, onJoin, onLeave, isJoining
</div>
)}
{agents.length === 0 && (
<span className="text-[10px] text-slate-500 italic pl-1">No agents</span>
<span className="text-[10px] text-slate-500 italic pl-1">No agents</span>
)}
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="h-7 px-2 text-[10px] gap-1 border-emerald-500/20 hover:bg-emerald-500/10 hover:text-emerald-400"
onClick={(e) => {
e.stopPropagation();
onJoin?.();
}}
disabled={isJoining}
>
<UserPlus className="h-3 w-3" />
Join
</Button>
<Button
size="sm"
variant="outline"
className="h-7 px-2 text-[10px] gap-1 border-emerald-500/20 hover:bg-emerald-500/10 hover:text-emerald-400"
onClick={(e) => {
e.stopPropagation();
onJoin?.();
}}
disabled={isJoining}
>
<UserPlus className="h-3 w-3" />
Join
</Button>
</div>
</div>
</div>

View file

@ -1,9 +1,8 @@
'use client';
import { useEffect, useState } from 'react';
import type { SwarmCardData, SwarmStatusFromApi } from '../../lib/swarm-api';
import type { SwarmStatusFromApi } from '../../lib/swarm-api';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { CheckCircle2, PlayCircle, Clock, AlertCircle, Loader2, Users } from 'lucide-react';
import { AgentAvatar } from '../shared/agent-avatar';
import { useAgentPool } from '../../hooks/use-agent-pool';
@ -36,7 +35,7 @@ export function SwarmInspector({ swarmId, projectRoot }: SwarmInspectorProps) {
const [status, setStatus] = useState<SwarmStatusFromApi | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { agents, getAgentsBySwarm } = useAgentPool(projectRoot);
const { getAgentsBySwarm } = useAgentPool(projectRoot);
useEffect(() => {
async function fetchStatus() {
@ -52,7 +51,7 @@ export function SwarmInspector({ swarmId, projectRoot }: SwarmInspectorProps) {
} else {
setError(payload.error?.message || 'Failed to load swarm status');
}
} catch (e) {
} catch {
setError('Failed to fetch swarm status');
} finally {
setIsLoading(false);
@ -100,7 +99,7 @@ export function SwarmInspector({ swarmId, projectRoot }: SwarmInspectorProps) {
{/* Agent Roster */}
<section>
<div className="flex items-center justify-between mb-3">
<h4 className="text-xs font-bold uppercase tracking-widest text-slate-500 flex items-center gap-2">
<h4 className="text-xs font-bold uppercase tracking-widest text-slate-500 flex items-center gap-2">
<Users className="h-3 w-3" />
Assigned Agents
</h4>
@ -108,21 +107,21 @@ export function SwarmInspector({ swarmId, projectRoot }: SwarmInspectorProps) {
{assignedAgents.length}
</span>
</div>
{assignedAgents.length === 0 ? (
<div className="text-xs text-slate-500 italic p-3 border border-dashed border-slate-800 rounded-lg text-center">
No agents currently assigned.
<br/>
<span className="text-[10px]">Use "Join" on the main card.</span>
</div>
<div className="text-xs text-slate-500 italic p-3 border border-dashed border-slate-800 rounded-lg text-center">
No agents currently assigned.
<br />
<span className="text-[10px]">Use &quot;Join&quot; on the main card.</span>
</div>
) : (
<div className="space-y-2">
{assignedAgents.map(agent => (
<div key={agent.agent_id} className="flex items-center gap-3 p-2 rounded-lg bg-slate-800/50 border border-slate-800">
<AgentAvatar
name={agent.display_name}
status={agent.status as any}
size="sm"
<AgentAvatar
name={agent.display_name}
status={agent.status as any}
size="sm"
/>
<div>
<p className="text-xs font-medium text-slate-300">{agent.display_name}</p>

View file

@ -11,15 +11,15 @@ import { useTemplates } from '../../hooks/use-templates';
import { ArchetypeInspector } from './archetype-inspector';
import { TemplateInspector } from './template-inspector';
export function SwarmWorkspace({ selectedMissionId, issues = [] }: { selectedMissionId?: string, issues?: BeadIssue[] }) {
export function SwarmWorkspace({ selectedMissionId, issues = [], projectRoot }: { selectedMissionId?: string, issues?: BeadIssue[], projectRoot: string }) {
const [activeTab, setActiveTab] = useState<'operations' | 'archetypes' | 'templates'>('operations');
// Inspector State
const [inspectingArchetypeId, setInspectingArchetypeId] = useState<string | null>(null);
const [inspectingTemplateId, setInspectingTemplateId] = useState<string | null>(null);
const { archetypes, isLoading: archetypesLoading } = useArchetypes();
const { templates, isLoading: templatesLoading } = useTemplates();
const { archetypes, isLoading: archetypesLoading, saveArchetype, deleteArchetype } = useArchetypes(projectRoot);
const { templates, isLoading: templatesLoading, saveTemplate, deleteTemplate } = useTemplates(projectRoot);
// Simulation State
const [isSimulating, setIsSimulating] = useState(false);
@ -152,7 +152,15 @@ export function SwarmWorkspace({ selectedMissionId, issues = [] }: { selectedMis
case 'archetypes':
return (
<div className="p-6 bg-[#0f1824]/30 rounded-xl border border-[var(--ui-border-soft)] h-full animate-in fade-in slide-in-from-bottom-4 duration-500 overflow-y-auto custom-scrollbar">
<h3 className="text-xl font-bold text-[var(--ui-text-primary)] mb-2">Agent Archetypes</h3>
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-bold text-[var(--ui-text-primary)]">Agent Archetypes</h3>
<button
onClick={() => setInspectingArchetypeId('')}
className="px-3 py-1.5 bg-[var(--ui-accent-info)] hover:bg-[var(--ui-accent-info)]/90 text-white rounded-md text-xs font-semibold shadow-md transition-colors"
>
+ Create Archetype
</button>
</div>
<p className="text-[var(--ui-text-muted)] text-sm mb-6">Manage the base roles and system prompts available to your swarms.</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{archetypesLoading ? (
@ -207,7 +215,15 @@ export function SwarmWorkspace({ selectedMissionId, issues = [] }: { selectedMis
case 'templates':
return (
<div className="p-6 bg-[#0f1824]/30 rounded-xl border border-[var(--ui-border-soft)] h-full animate-in fade-in slide-in-from-bottom-4 duration-500 overflow-y-auto custom-scrollbar">
<h3 className="text-xl font-bold text-[var(--ui-text-primary)] mb-2">Swarm Templates</h3>
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-bold text-[var(--ui-text-primary)]">Swarm Templates</h3>
<button
onClick={() => setInspectingTemplateId('')}
className="px-3 py-1.5 bg-[var(--ui-accent-info)] hover:bg-[var(--ui-accent-info)]/90 text-white rounded-md text-xs font-semibold shadow-md transition-colors"
>
+ Create Template
</button>
</div>
<p className="text-[var(--ui-text-muted)] text-sm mb-6">Define predefined teams and formulas for rapid mission deployment.</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{templatesLoading ? (
@ -324,18 +340,22 @@ export function SwarmWorkspace({ selectedMissionId, issues = [] }: { selectedMis
</main>
{/* Popups */}
{inspectingArchetypeId && (
{inspectingArchetypeId !== null && (
<ArchetypeInspector
archetype={archetypes.find(a => a.id === inspectingArchetypeId)!}
archetype={archetypes.find(a => a.id === inspectingArchetypeId)}
onClose={() => setInspectingArchetypeId(null)}
onSave={saveArchetype}
onDelete={deleteArchetype}
/>
)}
{inspectingTemplateId && (
{inspectingTemplateId !== null && (
<TemplateInspector
template={templates.find(t => t.id === inspectingTemplateId)!}
template={templates.find(t => t.id === inspectingTemplateId)}
archetypes={archetypes}
onClose={() => setInspectingTemplateId(null)}
onSave={saveTemplate}
onDelete={deleteTemplate}
/>
)}
</div>

View file

@ -1,23 +1,110 @@
import React from 'react';
import { X, Save, Edit, Link, Network } from 'lucide-react';
"use client";
import React, { useState, useEffect } from 'react';
import { X, Save, Trash2, Plus, Network, ShieldAlert } from 'lucide-react';
import type { SwarmTemplate, AgentArchetype } from '../../lib/types-swarm';
interface TemplateInspectorProps {
template: SwarmTemplate;
template?: SwarmTemplate;
archetypes: AgentArchetype[];
onClose: () => void;
onSave: (data: Partial<SwarmTemplate>) => Promise<void>;
onDelete?: (id: string) => Promise<void>;
}
export function TemplateInspector({ template, archetypes, onClose }: TemplateInspectorProps) {
if (!template) return null;
export function TemplateInspector({ template, archetypes, onClose, onSave, onDelete }: TemplateInspectorProps) {
const isNew = !template;
const totalAgents = template.team.reduce((acc, curr) => acc + curr.count, 0);
const [name, setName] = useState(template?.name || '');
const [description, setDescription] = useState(template?.description || '');
const [team, setTeam] = useState<{ archetypeId: string; count: number }[]>(template?.team || []);
const [protoFormula, setProtoFormula] = useState(template?.protoFormula || '');
const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (template) {
setName(template.name);
setDescription(template.description);
setTeam(template.team);
setProtoFormula(template.protoFormula || '');
}
}, [template]);
const updateTeamMember = (index: number, field: 'archetypeId' | 'count', value: string | number) => {
const newTeam = [...team];
if (field === 'count') {
newTeam[index] = { ...newTeam[index], count: Math.max(1, Number(value)) };
} else {
newTeam[index] = { ...newTeam[index], archetypeId: value as string };
}
setTeam(newTeam);
};
const addTeamMember = () => {
const firstAvailableArchetype = archetypes[0]?.id || '';
setTeam([...team, { archetypeId: firstAvailableArchetype, count: 1 }]);
};
const removeTeamMember = (index: number) => {
setTeam(team.filter((_, i) => i !== index));
};
const handleSave = async () => {
if (!name.trim()) {
setError('Name is required');
return;
}
if (team.length === 0) {
setError('At least one team member is required');
return;
}
setIsSaving(true);
setError(null);
try {
await onSave({
id: template?.id,
name: name.trim(),
description: description.trim(),
team,
protoFormula: protoFormula.trim() || undefined,
isBuiltIn: template?.isBuiltIn
});
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save');
} finally {
setIsSaving(false);
}
};
const handleDelete = async () => {
if (!template || !onDelete) return;
if (!confirm(`Delete template "${template.name}"? This cannot be undone.`)) return;
setIsDeleting(true);
setError(null);
try {
await onDelete(template.id);
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete');
} finally {
setIsDeleting(false);
}
};
const totalAgents = team.reduce((acc, curr) => acc + curr.count, 0);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
<div className="flex flex-col h-[75vh] w-full max-w-2xl overflow-hidden rounded-xl border border-[var(--ui-border-soft)] bg-[#0f1824] shadow-2xl animate-in zoom-in-95 duration-200">
<div className="flex flex-col h-[80vh] w-full max-w-2xl overflow-hidden rounded-xl border border-[var(--ui-border-soft)] bg-[#0f1824] shadow-2xl animate-in zoom-in-95 duration-200">
{/* Header */}
<div className="flex items-center justify-between border-b border-[var(--ui-border-soft)] px-5 py-4 bg-[#14202e]">
<div className="flex items-center gap-4">
<div className="h-10 w-10 flex-shrink-0 rounded-full bg-amber-500/10 border border-amber-500/20 flex items-center justify-center text-amber-500 font-bold text-lg">
@ -25,113 +112,159 @@ export function TemplateInspector({ template, archetypes, onClose }: TemplateIns
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="text-lg font-bold text-[var(--ui-text-primary)] leading-tight">{template.name}</h2>
{template.isBuiltIn && (
<h2 className="text-lg font-bold text-[var(--ui-text-primary)] leading-tight">
{isNew ? 'New Template' : name || 'Edit Template'}
</h2>
{template?.isBuiltIn && (
<span className="px-1.5 py-0.5 rounded-full bg-white/10 text-[9px] uppercase font-bold text-[var(--ui-text-muted)] border border-white/10">Built-in</span>
)}
</div>
<p className="font-mono uppercase tracking-wider text-[10px] text-[var(--ui-text-muted)] mt-0.5">{template.id}</p>
{!isNew && (
<p className="font-mono uppercase tracking-wider text-[10px] text-[var(--ui-text-muted)] mt-0.5">{template.id}</p>
)}
</div>
</div>
<button
onClick={onClose}
className="rounded-full p-2 text-[var(--ui-text-muted)] hover:bg-white/5 hover:text-white transition-colors"
>
<button onClick={onClose} className="rounded-full p-2 text-[var(--ui-text-muted)] hover:bg-white/5 hover:text-white transition-colors">
<X className="w-5 h-5" />
</button>
</div>
{/* Body Content */}
<div className="flex-1 overflow-y-auto p-5 space-y-6 custom-scrollbar">
{error && (
<div className="mx-5 mt-4 p-3 bg-rose-500/10 border border-rose-500/20 rounded-lg text-rose-400 text-sm">
{error}
</div>
)}
{/* Metadata Section */}
{template?.isBuiltIn && (
<div className="mx-5 mt-4 flex items-start gap-3 bg-[var(--ui-accent-warning)]/10 border border-[var(--ui-accent-warning)]/20 p-3 rounded-lg text-[var(--ui-accent-warning)]">
<ShieldAlert className="w-5 h-5 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<span className="font-semibold">Built-in Template.</span> This is a core system template. You cannot delete it.
</div>
</div>
)}
<div className="flex-1 overflow-y-auto p-5 space-y-6 custom-scrollbar">
<div>
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Purpose / Description</label>
<textarea
defaultValue={template.description}
readOnly
rows={2}
className="w-full bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)] resize-none"
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Name *</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., Standard Application Swarm"
className="w-full bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)]"
/>
</div>
<div>
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Description</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={2}
placeholder="Describe the purpose of this swarm template..."
className="w-full bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] resize-none"
/>
</div>
{/* Team Composition Builder */}
<div className="border-t border-[var(--ui-border-soft)] pt-5">
<div className="flex items-center justify-between mb-4">
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider flex items-center gap-2">
<Network className="w-4 h-4 text-emerald-500" />
Roster Composition
</label>
<button className="text-[11px] font-semibold text-[var(--ui-accent-info)] hover:text-white bg-[var(--ui-accent-info)]/10 px-2 py-1 rounded transition-colors disabled:opacity-50">
+ Add Member
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider block">Team Composition *</label>
<button
type="button"
onClick={addTeamMember}
className="px-2 py-1 bg-[var(--ui-border-soft)] hover:bg-[var(--ui-border-hover)] text-white rounded transition-colors flex items-center gap-1 text-xs font-medium"
>
<Plus className="w-3.5 h-3.5" /> Add Member
</button>
</div>
<div className="space-y-2">
{template.team.map((member, idx) => {
const arch = archetypes.find(a => a.id === member.archetypeId);
return (
<div key={idx} className="flex items-center gap-3 bg-[#111f2b] border border-[var(--ui-border-soft)] p-3 rounded-lg">
<div className="h-8 w-8 rounded text-sm flex items-center justify-center font-bold" style={{ backgroundColor: `${arch?.color || '#888'}20`, color: arch?.color || '#888' }}>
{arch?.name.charAt(0) || '?'}
</div>
<div className="flex-1">
<div className="font-semibold text-sm text-[var(--ui-text-primary)]">{arch?.name || member.archetypeId}</div>
<div className="text-[11px] text-[var(--ui-text-muted)]">{arch?.description || 'Unknown Archetype'}</div>
</div>
<div className="flex items-center gap-2 bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md p-1">
<span className="text-xs font-mono text-[var(--ui-text-muted)] px-2">Count:</span>
<input
type="number"
defaultValue={member.count}
readOnly
className="w-12 bg-transparent text-sm font-bold text-center text-[var(--ui-text-primary)] focus:outline-none"
/>
</div>
</div>
);
})}
<div className="space-y-3">
{team.map((member, index) => (
<div key={index} className="flex items-center gap-2">
<select
value={member.archetypeId}
onChange={(e) => updateTeamMember(index, 'archetypeId', e.target.value)}
className="flex-1 bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)]"
>
{archetypes.map(a => (
<option key={a.id} value={a.id}>{a.name}</option>
))}
</select>
<input
type="number"
min="1"
value={member.count}
onChange={(e) => updateTeamMember(index, 'count', e.target.value)}
className="w-20 bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)]"
/>
<button
type="button"
onClick={() => removeTeamMember(index)}
className="p-2 text-[var(--ui-text-muted)] hover:text-rose-400 hover:bg-white/5 rounded-md transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
{team.length === 0 && (
<div className="text-sm text-[var(--ui-text-muted)] italic py-4 text-center border border-dashed border-[var(--ui-border-soft)] rounded-md">
No agents assigned. Add a member to build your team.
</div>
)}
</div>
</div>
{/* Advanced: Proto-formula */}
<div className="border-t border-[var(--ui-border-soft)] pt-5">
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-2 flex items-center gap-2">
<Link className="w-4 h-4 text-amber-500" />
MOL Proto-Formula (Optional)
<div>
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block flex items-center gap-1.5">
<Network className="w-3 h-3" /> Proto-Formula (Optional)
</label>
<div className="flex items-center gap-3">
<input
type="text"
defaultValue={template.protoFormula || ''}
placeholder="e.g. 'release' or 'bugfix'"
readOnly
className="flex-1 bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 font-mono text-sm text-amber-500 focus:outline-none focus:border-amber-500/50 focus:ring-1 focus:ring-amber-500/50"
/>
<div className="text-[11px] text-[var(--ui-text-muted)] max-w-[200px] leading-tight">
Specifies a Gastown Formula to execute (`bd mol pour`) when launching this swarm.
</div>
</div>
<textarea
value={protoFormula}
onChange={(e) => setProtoFormula(e.target.value)}
rows={3}
placeholder="Optional default interaction rules or steps..."
className="w-full bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm font-mono text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] resize-y custom-scrollbar"
/>
</div>
</div>
{/* Footer Controls */}
<div className="border-t border-[var(--ui-border-soft)] px-5 py-4 bg-[#14202e] flex justify-end gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-semibold text-[var(--ui-text-primary)] hover:bg-white/5 rounded-md transition-colors"
>
Close
</button>
<button
disabled
className="flex items-center gap-2 px-4 py-2 text-sm font-bold bg-[var(--ui-accent-info)] hover:bg-[var(--ui-accent-info)]/90 text-white rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save className="w-4 h-4" />
Save Template
</button>
<div className="border-t border-[var(--ui-border-soft)] bg-[#0A111A] p-4 flex items-center justify-between flex-shrink-0 rounded-b-xl">
<div>
{!isNew && !template?.isBuiltIn && (
<button
type="button"
onClick={handleDelete}
disabled={isDeleting}
className="px-4 py-2 border border-rose-500/20 text-rose-400 hover:bg-rose-500/10 rounded-md text-sm font-medium transition-colors flex items-center gap-2 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<Trash2 className="w-4 h-4" />
{isDeleting ? 'Deleting...' : 'Delete'}
</button>
)}
</div>
<div className="flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-[var(--ui-text-muted)] hover:text-white transition-colors cursor-pointer"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={isSaving}
className="px-5 py-2 bg-[var(--ui-accent-info)] hover:bg-[var(--ui-accent-info)]/90 text-white rounded-md text-sm font-medium transition-colors flex items-center gap-2 shadow-lg shadow-blue-500/20 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save className="w-4 h-4" />
{isSaving ? 'Saving...' : (isNew ? 'Create Template' : 'Save Changes')}
</button>
</div>
</div>
</div>
</div>
);