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:
parent
409a7e7256
commit
dfaf523029
74 changed files with 11066 additions and 2046 deletions
31
src/app/api/mission/[id]/topology/route.ts
Normal file
31
src/app/api/mission/[id]/topology/route.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { runBdCommand } from '../../../../../lib/bridge';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
|
||||
if (!projectRoot) {
|
||||
return NextResponse.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await runBdCommand({
|
||||
projectRoot,
|
||||
args: ['swarm', 'status', id, '--json'],
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json({ ok: false, error: result.error }, { status: 500 });
|
||||
}
|
||||
|
||||
const topology = JSON.parse(result.stdout);
|
||||
return NextResponse.json({ ok: true, data: topology });
|
||||
} catch (e) {
|
||||
console.error(`Failed to fetch topology for ${id}:`, e);
|
||||
return NextResponse.json({ ok: false, error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
40
src/app/api/mission/assign/route.ts
Normal file
40
src/app/api/mission/assign/route.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { joinSwarm, leaveSwarm } from '../../../../lib/swarm-molecules';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { projectRoot, agentId, missionId, action } = body;
|
||||
|
||||
if (!projectRoot || !agentId || !missionId || !action) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
let result;
|
||||
if (action === 'join') {
|
||||
result = await joinSwarm({ agent: agentId, epicId: missionId }, { projectRoot });
|
||||
} else if (action === 'leave') {
|
||||
result = await leaveSwarm({ agent: agentId, projectRoot });
|
||||
} else {
|
||||
return NextResponse.json({ ok: false, error: 'Invalid action' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: result.error?.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, data: result.data });
|
||||
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: e instanceof Error ? e.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
46
src/app/api/mission/graph/route.ts
Normal file
46
src/app/api/mission/graph/route.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { runBdCommand } from '../../../../lib/bridge';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (!projectRoot || !id) {
|
||||
return NextResponse.json({ ok: false, error: 'projectRoot and id are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 1. Get the mission/epic bead itself
|
||||
const headResult = await runBdCommand({
|
||||
projectRoot,
|
||||
args: ['show', id, '--json'],
|
||||
});
|
||||
|
||||
// 2. Get children
|
||||
const childrenResult = await runBdCommand({
|
||||
projectRoot,
|
||||
args: ['list', '--parent', id, '--json'],
|
||||
});
|
||||
|
||||
if (!headResult.success) {
|
||||
return NextResponse.json({ ok: false, error: 'Failed to fetch mission head' }, { status: 500 });
|
||||
}
|
||||
|
||||
try {
|
||||
const head = JSON.parse(headResult.stdout);
|
||||
let children = [];
|
||||
if (childrenResult.success && childrenResult.stdout.trim()) {
|
||||
children = JSON.parse(childrenResult.stdout);
|
||||
}
|
||||
|
||||
const headObj = Array.isArray(head) ? head[0] : head;
|
||||
|
||||
// Transform for graph view (if needed, or just return raw issues and let UI handle it)
|
||||
// The WorkflowGraph component expects BeadIssue[]
|
||||
const nodes = [headObj, ...children];
|
||||
|
||||
return NextResponse.json({ ok: true, data: { nodes } });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ ok: false, error: 'Failed to parse graph data' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
88
src/app/api/mission/list/route.ts
Normal file
88
src/app/api/mission/list/route.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { runBdCommand } from '../../../../lib/bridge';
|
||||
import { listAgents, type AgentRecord } from '../../../../lib/agent-registry';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
interface SwarmTopology {
|
||||
completed: { id: string; title: string }[];
|
||||
active: { id: string; title: string; assignee?: string }[];
|
||||
ready: { id: string; title: string }[];
|
||||
blocked: { id: string; title: string; blocked_by: string[] }[];
|
||||
}
|
||||
|
||||
interface Mission {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status: 'planning' | 'active' | 'blocked' | 'completed';
|
||||
stats: {
|
||||
total: number;
|
||||
done: number;
|
||||
blocked: number;
|
||||
active: number;
|
||||
};
|
||||
topology?: SwarmTopology;
|
||||
agents: AgentRecord[];
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
|
||||
if (!projectRoot) {
|
||||
return NextResponse.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 1. Fetch Swarms (Molecules)
|
||||
const swarmResult = await runBdCommand({
|
||||
projectRoot,
|
||||
args: ['swarm', 'list', '--json'],
|
||||
});
|
||||
|
||||
if (!swarmResult.success) {
|
||||
console.warn('Swarm list failed, returning empty:', swarmResult.error);
|
||||
return NextResponse.json({ ok: true, data: { missions: [] } });
|
||||
}
|
||||
|
||||
// 2. Fetch All Agents
|
||||
const agentResult = await listAgents({}, { projectRoot });
|
||||
const allAgents = agentResult.ok ? agentResult.data! : [];
|
||||
|
||||
try {
|
||||
const rawData = JSON.parse(swarmResult.stdout);
|
||||
const rawSwarms = rawData.swarms || [];
|
||||
|
||||
// 3. Transform & Merge
|
||||
const missions: Mission[] = rawSwarms
|
||||
.filter((s: any) => !s.title.startsWith('Agent:'))
|
||||
.map((s: any) => {
|
||||
const assignedAgents = allAgents.filter(a => a.swarm_id === s.epic_id || a.swarm_id === s.id);
|
||||
|
||||
// Map status
|
||||
let status: Mission['status'] = 'planning';
|
||||
if (s.status === 'closed') status = 'completed';
|
||||
else if (assignedAgents.length > 0) status = 'active';
|
||||
else status = 'planning';
|
||||
|
||||
return {
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
description: s.description || s.epic_description || '',
|
||||
status,
|
||||
stats: {
|
||||
total: s.total_issues || 0,
|
||||
done: s.completed_issues || 0,
|
||||
active: s.active_issues || 0,
|
||||
blocked: 0
|
||||
},
|
||||
agents: assignedAgents
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, data: { missions } });
|
||||
} catch (e) {
|
||||
console.error('Mission list parsing error:', e);
|
||||
return NextResponse.json({ ok: false, error: 'Failed to parse mission data' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue