feat(swarm): implement Swarm View remake with Operations, Archetypes, and Templates

This commit includes the new SwarmWorkspace with its 3 sub-tabs, the LeftPanel mission picker, and the comprehensive Operations Command Dashboard featuring the live interactive DAG telemetry and task assignment prep flow.
This commit is contained in:
zenchantlive 2026-02-20 22:19:38 -08:00
parent 409a7e7256
commit dfaf523029
74 changed files with 11066 additions and 2046 deletions

View file

@ -0,0 +1,148 @@
'use client';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Users, AlertTriangle, Activity, CheckCircle2, Circle } from 'lucide-react';
import { AgentAvatar } from '../shared/agent-avatar';
import type { AgentRecord } from '../../lib/agent-registry';
import { SwarmGraph } from './swarm-graph';
import { useSwarmTopology } from '../../hooks/use-swarm-topology';
export interface MissionCardProps {
id: string;
projectRoot: string;
title: string;
description?: string;
status: 'planning' | 'active' | 'blocked' | 'completed';
stats: {
total: number;
done: number;
blocked: number;
};
agents: AgentRecord[];
onDeploy: () => void;
onClick: () => void;
}
const STATUS_CONFIG = {
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',
label: 'ACTIVE',
icon: Activity
},
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',
label: 'COMPLETE',
icon: CheckCircle2
},
};
export function MissionCard({ id, projectRoot, title, description, status, stats, 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
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"
>
{/* Decorative Top Glow */}
<div className={cn("absolute top-0 left-0 right-0 h-1 opacity-50 group-hover:opacity-100 transition-opacity", config.bg.replace('/5', '/40'))} />
{/* HEADER */}
<div className="p-5 flex flex-col gap-3 min-h-0">
<div className="flex justify-between items-start">
<div className="flex items-center gap-2">
<span className="font-mono text-[10px] tracking-wider text-slate-500">{id}</span>
<Badge variant="outline" className={cn("text-[9px] px-2 py-0.5 border h-5 flex items-center gap-1.5", config.color, config.border, config.bg)}>
<StatusIcon className="h-3 w-3" />
{config.label}
</Badge>
</div>
{showPulse && (
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
</span>
</div>
)}
</div>
<div className="space-y-2">
<h3 className="font-bold text-lg text-slate-100 leading-tight group-hover:text-white transition-colors line-clamp-2">
{title}
</h3>
<p className="text-xs text-slate-400 line-clamp-2 leading-relaxed">
{description || "No mission brief available."}
</p>
</div>
</div>
{/* GRAPH VISUALIZATION */}
<div className="px-5 py-2 flex-1 flex flex-col justify-end">
<SwarmGraph topology={topology} isLoading={isLoading} />
</div>
{/* FOOTER: SQUAD */}
<div className="px-5 py-3 border-t border-white/5 flex items-center justify-between bg-[var(--ui-bg-shell)] mt-auto">
<div className="flex items-center gap-3">
<div className="flex -space-x-2">
{agents.slice(0, 4).map(agent => (
<div key={agent.agent_id} className="ring-2 ring-[var(--ui-bg-shell)] rounded-full transition-transform hover:scale-110 z-0 hover:z-10 relative" title={`${agent.display_name} (${agent.role})`}>
<AgentAvatar name={agent.display_name} status={agent.status as any} size="sm" />
</div>
))}
{agents.length === 0 && (
<div className="h-7 w-7 rounded-full bg-slate-800 border border-slate-700 border-dashed flex items-center justify-center text-slate-600">
<Users className="h-3 w-3" />
</div>
)}
</div>
{agents.length === 0 && (
<span className="text-[10px] font-medium text-slate-500 uppercase tracking-wide">Unstaffed</span>
)}
</div>
<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"
: "border-slate-700 text-slate-400 hover:text-white hover:bg-white/5 hover:border-slate-500"
)}
>
{isUnstaffed ? 'Deploy' : 'Manage'}
</Button>
</div>
</Card>
);
}

View file

@ -0,0 +1,141 @@
'use client';
import { useState } from 'react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Loader2, Map, MessageSquare, Users, X, Activity } from 'lucide-react';
import { WorkflowGraph } from '../shared/workflow-graph';
import { AgentAvatar } from '../shared/agent-avatar';
import { useMissionGraph } from '../../hooks/use-mission-graph';
import type { AgentRecord } from '../../lib/agent-registry';
interface MissionInspectorProps {
missionId: string;
missionTitle: string; // Passed in or fetched? Better to pass in for instant header
projectRoot: string;
assignedAgents: AgentRecord[];
onClose: () => void;
onAssign: (agentId: string, action: 'join' | 'leave') => void;
}
export function MissionInspector({
missionId,
missionTitle,
projectRoot,
assignedAgents,
onClose,
onAssign
}: MissionInspectorProps) {
const { nodes, isLoading: isGraphLoading } = useMissionGraph(projectRoot, missionId);
const [activeTab, setActiveTab] = useState('map');
return (
<div className="flex flex-col h-full bg-[#08111d] border-l border-slate-800 text-slate-200">
{/* Header */}
<div className="flex items-start justify-between p-4 border-b border-slate-800 bg-[#0d1621]">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 font-mono text-[10px]">
{missionId}
</Badge>
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-bold">Active Operation</span>
</div>
<h2 className="text-sm font-semibold text-white line-clamp-2">{missionTitle}</h2>
</div>
<Button variant="ghost" size="icon" className="h-6 w-6 text-slate-500 hover:text-white" onClick={onClose}>
<X className="h-4 w-4" />
</Button>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col min-h-0">
<div className="px-4 pt-2 bg-[#0d1621] border-b border-slate-800">
<TabsList className="bg-transparent p-0 h-auto gap-4">
<TabsTrigger
value="map"
className="rounded-none border-b-2 border-transparent px-2 py-2 text-[10px] uppercase tracking-wider data-[state=active]:border-emerald-500 data-[state=active]:bg-transparent data-[state=active]:text-emerald-400"
>
<Map className="h-3 w-3 mr-1.5" />
Map
</TabsTrigger>
<TabsTrigger
value="comms"
className="rounded-none border-b-2 border-transparent px-2 py-2 text-[10px] uppercase tracking-wider data-[state=active]:border-emerald-500 data-[state=active]:bg-transparent data-[state=active]:text-emerald-400"
>
<MessageSquare className="h-3 w-3 mr-1.5" />
Comms
</TabsTrigger>
<TabsTrigger
value="squad"
className="rounded-none border-b-2 border-transparent px-2 py-2 text-[10px] uppercase tracking-wider data-[state=active]:border-emerald-500 data-[state=active]:bg-transparent data-[state=active]:text-emerald-400"
>
<Users className="h-3 w-3 mr-1.5" />
Squad <span className="ml-1 text-slate-500">{assignedAgents.length}</span>
</TabsTrigger>
</TabsList>
</div>
{/* Content */}
<div className="flex-1 overflow-hidden relative">
<TabsContent value="map" className="h-full m-0 p-0 data-[state=active]:flex flex-col">
{isGraphLoading ? (
<div className="flex h-full items-center justify-center"><Loader2 className="h-6 w-6 animate-spin text-slate-600" /></div>
) : (
<div className="flex-1 relative bg-slate-950">
<WorkflowGraph beads={nodes} selectedId={undefined} hideClosed={false} className="h-full w-full border-0 rounded-none" />
<div className="absolute bottom-4 right-4 pointer-events-none">
<Badge variant="outline" className="bg-black/50 border-white/10 backdrop-blur text-xs">
{nodes.length} Nodes
</Badge>
</div>
</div>
)}
</TabsContent>
<TabsContent value="comms" className="h-full m-0 p-4 overflow-y-auto">
<div className="flex flex-col items-center justify-center h-full text-slate-500 space-y-2 opacity-60">
<Activity className="h-8 w-8" />
<p className="text-xs">Secure Uplink Offline</p>
<p className="text-[10px] italic">Inter-agent communication feed coming in Phase 3.2</p>
</div>
</TabsContent>
<TabsContent value="squad" className="h-full m-0">
<ScrollArea className="h-full">
<div className="p-4 space-y-3">
{assignedAgents.length === 0 ? (
<div className="text-center py-8 text-slate-500 text-xs">
No agents deployed.
</div>
) : (
assignedAgents.map(agent => (
<div key={agent.agent_id} className="flex items-center justify-between p-3 rounded-lg bg-slate-800/40 border border-slate-800">
<div className="flex items-center gap-3">
<AgentAvatar name={agent.display_name} status={agent.status as any} />
<div>
<p className="text-sm font-medium text-slate-200">{agent.display_name}</p>
<div className="flex items-center gap-2 text-[10px] text-slate-500">
<span className="uppercase font-bold tracking-wider">{agent.role}</span>
<span></span>
<span className="font-mono">{agent.status}</span>
</div>
</div>
</div>
<Button variant="ghost" size="sm" className="h-7 text-xs text-rose-400 hover:text-rose-300 hover:bg-rose-950/30" onClick={() => onAssign(agent.agent_id, 'leave')}>
Dismiss
</Button>
</div>
))
)}
</div>
</ScrollArea>
</TabsContent>
</div>
</Tabs>
</div>
);
}

View file

@ -0,0 +1,107 @@
'use client';
import { useMemo } from 'react';
import { motion } from 'framer-motion';
import type { SwarmTopologyData } from '../../hooks/use-swarm-topology';
interface SwarmGraphProps {
topology: SwarmTopologyData | null;
isLoading: boolean;
}
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
key={`done-${item.id}`}
cx={20 + (col * 8)}
cy={20 + (row * 8)}
r={2.5}
fill="#34d399"
opacity={0.5}
/>
);
});
// 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>
);
});
// 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" />
);
});
// 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" />
);
});
return output;
}, [topology]);
if (isLoading) {
return (
<div className="h-16 w-full flex items-center justify-center bg-black/20 rounded-lg animate-pulse">
<span className="text-[10px] text-slate-600 font-mono">SCANNING TOPOLOGY...</span>
</div>
);
}
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 bg-black/40 rounded-lg border border-white/5 overflow-hidden relative">
<svg width="100%" height="100%" viewBox="0 0 300 64" preserveAspectRatio="xMidYMid meet">
{/* 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>
<text x="240" y="15" fontSize="8" fill="#94a3b8" textAnchor="middle" fontFamily="monospace">READY</text>
<text x="240" y="60" fontSize="8" fill="#f43f5e" textAnchor="middle" fontFamily="monospace">BLOCKED</text>
</svg>
</div>
);
}

View file

@ -0,0 +1,255 @@
'use client';
import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Badge } from '@/components/ui/badge';
import { AgentAvatar } from '../shared/agent-avatar';
import { useAgentPool } from '../../hooks/use-agent-pool';
import { Loader2, Plus, Minus, ShieldCheck, Search, Users, ChevronLeft, Save } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import type { AgentRecord } from '../../lib/agent-registry';
interface TeamManagerDialogProps {
isOpen: boolean;
onClose: () => void;
missionId: string;
missionTitle: string;
projectRoot: string;
assignedAgents: AgentRecord[];
onAssign: (agentId: string, action: 'join' | 'leave') => Promise<void>;
}
export function TeamManagerDialog({
isOpen,
onClose,
missionId,
missionTitle,
projectRoot,
assignedAgents,
onAssign
}: TeamManagerDialogProps) {
const { agents, isLoading, refresh } = useAgentPool(projectRoot);
const [search, setSearch] = useState('');
const [pendingAction, setPendingAction] = useState<string | null>(null);
// Creation Mode State
const [isCreating, setIsCreating] = useState(false);
const [newName, setNewName] = useState('');
const [newRole, setNewRole] = useState('');
const [newInstructions, setNewInstructions] = useState('');
const [isSaving, setIsSaving] = useState(false);
const assignedIds = new Set(assignedAgents.map(a => a.agent_id));
const availableAgents = agents.filter(a =>
!assignedIds.has(a.agent_id) &&
(a.display_name.toLowerCase().includes(search.toLowerCase()) ||
a.role.toLowerCase().includes(search.toLowerCase()))
);
const handleAction = async (agentId: string, action: 'join' | 'leave') => {
setPendingAction(agentId);
try {
await onAssign(agentId, action);
} finally {
setPendingAction(null);
}
};
const handleCreateAgent = async () => {
if (!newName || !newRole) return;
setIsSaving(true);
try {
const res = await fetch('/api/agent/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectRoot, name: newName, role: newRole, instructions: newInstructions })
});
if (res.ok) {
await refresh();
setIsCreating(false);
setNewName('');
setNewRole('');
setNewInstructions('');
}
} catch (e) {
console.error(e);
} finally {
setIsSaving(false);
}
};
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-2xl bg-[#08111d] border-slate-800 text-slate-200">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ShieldCheck className="h-5 w-5 text-emerald-500" />
Manage Mission Squad
<Badge variant="outline" className="ml-2 border-slate-700 text-slate-400 font-mono font-normal">
{missionId}
</Badge>
</DialogTitle>
<p className="text-sm text-slate-400">{missionTitle}</p>
</DialogHeader>
<div className="grid grid-cols-2 gap-4 h-[450px] mt-4">
{/* Left: Available Pool / Creation Form */}
<div className="flex flex-col gap-2 rounded-lg border border-slate-800 bg-[#0d1621] p-3 transition-all relative overflow-hidden">
{isCreating ? (
// CREATION FORM
<div className="flex flex-col h-full animate-in slide-in-from-left-4 fade-in duration-200">
<div className="flex items-center justify-between mb-4">
<Button variant="ghost" size="sm" onClick={() => setIsCreating(false)} className="-ml-2 text-slate-400 hover:text-white">
<ChevronLeft className="h-4 w-4 mr-1" /> Back
</Button>
<h4 className="text-xs font-bold uppercase tracking-wider text-emerald-500">Draft New Agent</h4>
</div>
<div className="space-y-4 flex-1">
<div className="space-y-1">
<label className="text-[10px] uppercase text-slate-500 font-semibold">Codename</label>
<Input
placeholder="e.g. Data Miner"
className="bg-slate-900 border-slate-700"
value={newName}
onChange={e => setNewName(e.target.value)}
/>
</div>
<div className="space-y-1">
<label className="text-[10px] uppercase text-slate-500 font-semibold">Role</label>
<Input
placeholder="e.g. data-engineer"
className="bg-slate-900 border-slate-700 font-mono text-xs"
value={newRole}
onChange={e => setNewRole(e.target.value)}
/>
</div>
<div className="space-y-1 flex-1 flex flex-col">
<label className="text-[10px] uppercase text-slate-500 font-semibold">Directives / Instructions</label>
<Textarea
placeholder="Primary directive: Extract data from..."
className="bg-slate-900 border-slate-700 flex-1 resize-none text-xs leading-relaxed"
value={newInstructions}
onChange={e => setNewInstructions(e.target.value)}
/>
</div>
</div>
<Button
onClick={handleCreateAgent}
disabled={!newName || !newRole || isSaving}
className="mt-4 bg-emerald-600 hover:bg-emerald-500 text-white w-full"
>
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <><Save className="h-4 w-4 mr-2" /> Recruit Agent</>}
</Button>
</div>
) : (
// LIST VIEW
<>
<div className="flex items-center justify-between mb-2">
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500">Available Resources</h4>
<Badge variant="secondary" className="bg-slate-800 text-slate-400">{availableAgents.length}</Badge>
</div>
<div className="flex gap-2 mb-2">
<div className="relative flex-1">
<Search className="absolute left-2 top-2.5 h-3 w-3 text-slate-500" />
<Input
placeholder="Search agents..."
className="h-8 pl-7 bg-slate-900 border-slate-700 text-xs"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<Button
size="icon"
variant="outline"
className="h-8 w-8 border-slate-700 border-dashed text-slate-400 hover:text-emerald-400 hover:border-emerald-500/50"
onClick={() => setIsCreating(true)}
title="Draft New Agent"
>
<Plus className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="flex-1 pr-3">
{isLoading ? (
<div className="flex justify-center p-4"><Loader2 className="h-5 w-5 animate-spin text-slate-500" /></div>
) : (
<div className="space-y-2">
{availableAgents.map(agent => (
<div key={agent.agent_id} className="flex items-center justify-between p-2 rounded hover:bg-white/5 transition-colors group">
<div className="flex items-center gap-2">
<AgentAvatar name={agent.display_name} status={agent.status as any} size="sm" />
<div>
<p className="text-xs font-medium text-slate-200">{agent.display_name}</p>
<p className="text-[10px] text-slate-500 uppercase">{agent.role}</p>
</div>
</div>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 opacity-0 group-hover:opacity-100 hover:bg-emerald-500/20 hover:text-emerald-400"
onClick={() => handleAction(agent.agent_id, 'join')}
disabled={!!pendingAction}
>
{pendingAction === agent.agent_id ? <Loader2 className="h-3 w-3 animate-spin" /> : <Plus className="h-3 w-3" />}
</Button>
</div>
))}
</div>
)}
</ScrollArea>
</>
)}
</div>
{/* Right: Assigned Squad */}
<div className="flex flex-col gap-2 rounded-lg border border-emerald-900/30 bg-emerald-950/10 p-3">
<div className="flex items-center justify-between mb-2">
<h4 className="text-xs font-bold uppercase tracking-wider text-emerald-500">Deployed Squad</h4>
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400">{assignedAgents.length}</Badge>
</div>
<ScrollArea className="flex-1 pr-3">
<div className="space-y-2">
{assignedAgents.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-slate-500 text-xs italic border border-dashed border-emerald-900/30 rounded bg-emerald-950/20">
<Users className="h-8 w-8 mb-2 opacity-20" />
No agents assigned
</div>
) : (
assignedAgents.map(agent => (
<div key={agent.agent_id} className="flex items-center justify-between p-2 rounded bg-emerald-500/5 border border-emerald-500/10">
<div className="flex items-center gap-2">
<AgentAvatar name={agent.display_name} status={agent.status as any} size="sm" />
<div>
<p className="text-xs font-medium text-emerald-100">{agent.display_name}</p>
<p className="text-[10px] text-emerald-500/70 uppercase">{agent.role}</p>
</div>
</div>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 hover:bg-rose-500/20 hover:text-rose-400"
onClick={() => handleAction(agent.agent_id, 'leave')}
disabled={!!pendingAction}
>
{pendingAction === agent.agent_id ? <Loader2 className="h-3 w-3 animate-spin" /> : <Minus className="h-3 w-3" />}
</Button>
</div>
))
)}
</div>
</ScrollArea>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose} className="border-slate-700 text-slate-300">Done</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}