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
19
src/app/api/agents/list/route.ts
Normal file
19
src/app/api/agents/list/route.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { listAgents } from '../../../../lib/agent-registry';
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
const result = await listAgents({}, { projectRoot });
|
||||
|
||||
if (!result.ok) {
|
||||
return NextResponse.json({ ok: false, error: result.error?.message }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, data: result.data });
|
||||
}
|
||||
28
src/app/api/beads/[id]/comments/route.ts
Normal file
28
src/app/api/beads/[id]/comments/route.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { readInteractionsViaBd } from '../../../../../lib/read-interactions';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
): Promise<NextResponse> {
|
||||
const { id } = await params;
|
||||
const projectRoot = request.nextUrl.searchParams.get('projectRoot');
|
||||
|
||||
if (!projectRoot) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { message: 'projectRoot is required' } },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const comments = await readInteractionsViaBd(projectRoot, id);
|
||||
return NextResponse.json({ ok: true, comments });
|
||||
} catch (error) {
|
||||
console.error('[API] Failed to fetch comments:', error);
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { message: 'Failed to fetch comments' } },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
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 });
|
||||
}
|
||||
}
|
||||
70
src/app/api/swarm/close/route.ts
Normal file
70
src/app/api/swarm/close/route.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { runBdCommand } from '../../../../lib/bridge';
|
||||
|
||||
interface CloseSwarmPayload {
|
||||
projectRoot: string;
|
||||
swarmId: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
let body: unknown;
|
||||
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'bad_args', message: 'Invalid JSON body' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!body || typeof body !== 'object') {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'bad_args', message: 'Payload must be a JSON object' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const data = body as Record<string, unknown>;
|
||||
|
||||
if (typeof data.projectRoot !== 'string' || !data.projectRoot.trim()) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'bad_args', message: 'projectRoot is required' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof data.swarmId !== 'string' || !data.swarmId.trim()) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'bad_args', message: 'swarmId is required' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const payload: CloseSwarmPayload = {
|
||||
projectRoot: data.projectRoot.trim(),
|
||||
swarmId: data.swarmId.trim(),
|
||||
reason: typeof data.reason === 'string' ? data.reason.trim() : undefined,
|
||||
};
|
||||
|
||||
const args = ['close', payload.swarmId, '--json'];
|
||||
if (payload.reason) {
|
||||
args.push('--reason', payload.reason);
|
||||
}
|
||||
|
||||
const result = await runBdCommand({
|
||||
projectRoot: payload.projectRoot,
|
||||
args,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: result.classification ?? 'unknown', message: result.error ?? result.stderr } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, swarmId: payload.swarmId });
|
||||
}
|
||||
78
src/app/api/swarm/create/route.ts
Normal file
78
src/app/api/swarm/create/route.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { runBdCommand } from '../../../../lib/bridge';
|
||||
|
||||
interface CreateSwarmPayload {
|
||||
projectRoot: string;
|
||||
epicId: string;
|
||||
coordinator?: string;
|
||||
}
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
let body: unknown;
|
||||
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'bad_args', message: 'Invalid JSON body' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!body || typeof body !== 'object') {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'bad_args', message: 'Payload must be a JSON object' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const data = body as Record<string, unknown>;
|
||||
|
||||
if (typeof data.projectRoot !== 'string' || !data.projectRoot.trim()) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'bad_args', message: 'projectRoot is required' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof data.epicId !== 'string' || !data.epicId.trim()) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'bad_args', message: 'epicId is required' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const payload: CreateSwarmPayload = {
|
||||
projectRoot: data.projectRoot.trim(),
|
||||
epicId: data.epicId.trim(),
|
||||
coordinator: typeof data.coordinator === 'string' ? data.coordinator.trim() : undefined,
|
||||
};
|
||||
|
||||
const args = ['swarm', 'create', payload.epicId, '--json'];
|
||||
if (payload.coordinator) {
|
||||
args.push('--coordinator', payload.coordinator);
|
||||
}
|
||||
|
||||
const result = await runBdCommand({
|
||||
projectRoot: payload.projectRoot,
|
||||
args,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: result.classification ?? 'unknown', message: result.error ?? result.stderr } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const swarm = JSON.parse(result.stdout);
|
||||
return NextResponse.json({ ok: true, swarm });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'unknown', message: 'Failed to parse swarm create output' } },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
29
src/app/api/swarm/formulas/route.ts
Normal file
29
src/app/api/swarm/formulas/route.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
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');
|
||||
|
||||
if (!projectRoot) {
|
||||
return NextResponse.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// bd formula list --json
|
||||
const result = await runBdCommand({
|
||||
projectRoot,
|
||||
args: ['formula', 'list', '--json', '--allow-stale'],
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json({ ok: false, error: result.error }, { status: 500 });
|
||||
}
|
||||
|
||||
try {
|
||||
// If output is empty or not JSON array, handle gracefully
|
||||
const json = JSON.parse(result.stdout || '[]');
|
||||
return NextResponse.json({ ok: true, data: json });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ ok: false, error: 'Failed to parse formulas' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
45
src/app/api/swarm/graph/route.ts
Normal file
45
src/app/api/swarm/graph/route.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
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 epicId = searchParams.get('epic');
|
||||
|
||||
if (!projectRoot || !epicId) {
|
||||
return NextResponse.json({ ok: false, error: 'projectRoot and epic are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 1. Get the epic itself
|
||||
const epicResult = await runBdCommand({
|
||||
projectRoot,
|
||||
args: ['show', epicId, '--json'],
|
||||
});
|
||||
|
||||
// 2. Get children
|
||||
const childrenResult = await runBdCommand({
|
||||
projectRoot,
|
||||
args: ['list', '--parent', epicId, '--json'],
|
||||
});
|
||||
|
||||
if (!epicResult.success) {
|
||||
return NextResponse.json({ ok: false, error: 'Failed to fetch epic' }, { status: 500 });
|
||||
}
|
||||
|
||||
try {
|
||||
const epic = JSON.parse(epicResult.stdout);
|
||||
// Handle list returning empty or error gracefully
|
||||
let children = [];
|
||||
if (childrenResult.success && childrenResult.stdout.trim()) {
|
||||
children = JSON.parse(childrenResult.stdout);
|
||||
// bd list returns array, bd show returns object (or array of 1)
|
||||
}
|
||||
|
||||
const epicObj = Array.isArray(epic) ? epic[0] : epic;
|
||||
const issues = [epicObj, ...children];
|
||||
|
||||
return NextResponse.json({ ok: true, data: issues });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ ok: false, error: 'Failed to parse graph data' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
36
src/app/api/swarm/join/route.ts
Normal file
36
src/app/api/swarm/join/route.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { joinSwarm } from '../../../../lib/swarm-molecules';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { projectRoot, agentId, swarmId } = body;
|
||||
|
||||
if (!projectRoot || !agentId || !swarmId) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const result = await joinSwarm(
|
||||
{ agent: agentId, epicId: swarmId },
|
||||
{ projectRoot }
|
||||
);
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
42
src/app/api/swarm/launch/route.ts
Normal file
42
src/app/api/swarm/launch/route.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { runBdCommand } from '../../../../lib/bridge';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { projectRoot, title, proto } = body;
|
||||
|
||||
if (!projectRoot || !title || !proto) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: 'Missing required fields: projectRoot, title, proto' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// bd mol pour "Title" --proto <proto> --json
|
||||
const args = ['mol', 'pour', title, '--proto', proto, '--json'];
|
||||
|
||||
// Safety: Ensure proto doesn't contain shell injection
|
||||
if (!/^[a-zA-Z0-9_\-.]+$/.test(proto)) {
|
||||
return NextResponse.json({ ok: false, error: 'Invalid proto name' }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await runBdCommand({
|
||||
projectRoot,
|
||||
args,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json({ ok: false, error: result.error || result.stderr }, { status: 500 });
|
||||
}
|
||||
|
||||
const data = JSON.parse(result.stdout);
|
||||
return NextResponse.json({ ok: true, data });
|
||||
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: e instanceof Error ? e.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
36
src/app/api/swarm/leave/route.ts
Normal file
36
src/app/api/swarm/leave/route.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { leaveSwarm } from '../../../../lib/swarm-molecules';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { projectRoot, agentId } = body;
|
||||
|
||||
if (!projectRoot || !agentId) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const result = await leaveSwarm(
|
||||
{ agent: agentId },
|
||||
{ projectRoot }
|
||||
);
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
44
src/app/api/swarm/list/route.ts
Normal file
44
src/app/api/swarm/list/route.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { runBdCommand } from '../../../../lib/bridge';
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
|
||||
if (!projectRoot) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'bad_args', message: 'projectRoot is required' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const result = await runBdCommand({
|
||||
projectRoot,
|
||||
args: ['swarm', 'list', '--json'],
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: result.classification ?? 'unknown', message: result.error ?? result.stderr } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const rawData = JSON.parse(result.stdout);
|
||||
// Filter out items that look like agents (start with "Agent:" or have gt:agent style IDs if discernible)
|
||||
// Real swarms/molecules usually don't start with "Agent:".
|
||||
const swarms = (rawData.swarms || []).filter((s: any) =>
|
||||
!s.title.startsWith('Agent: ') &&
|
||||
!s.title.startsWith('Agent:')
|
||||
);
|
||||
|
||||
return NextResponse.json({ ok: true, data: { swarms } });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'unknown', message: 'Failed to parse swarm list output' } },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
29
src/app/api/swarm/prep/route.ts
Normal file
29
src/app/api/swarm/prep/route.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { beadId, archetypeId } = await request.json();
|
||||
|
||||
if (!beadId || !archetypeId) {
|
||||
return NextResponse.json({ error: 'Missing beadId or archetypeId' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Use bd CLI to add the archetype label. We leave it 'open' because Prep just assigns the agent.
|
||||
const cmd = `bd label add ${beadId} "agent:${archetypeId}"`;
|
||||
const { stdout, stderr } = await execAsync(cmd);
|
||||
|
||||
if (stderr && !stderr.includes('Warning')) {
|
||||
console.error('bd edit stderr:', stderr);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, message: `Prepped ${beadId} for ${archetypeId}`, output: stdout });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Prep task failed:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
45
src/app/api/swarm/status/route.ts
Normal file
45
src/app/api/swarm/status/route.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { runBdCommand } from '../../../../lib/bridge';
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
const epicId = searchParams.get('epic');
|
||||
|
||||
if (!projectRoot) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'bad_args', message: 'projectRoot is required' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!epicId) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'bad_args', message: 'epic is required' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const result = await runBdCommand({
|
||||
projectRoot,
|
||||
args: ['swarm', 'status', epicId, '--json'],
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: result.classification ?? 'unknown', message: result.error ?? result.stderr } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(result.stdout);
|
||||
return NextResponse.json({ ok: true, data });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: { classification: 'unknown', message: 'Failed to parse swarm status output' } },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
7
src/app/api/swarm/templates/route.ts
Normal file
7
src/app/api/swarm/templates/route.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { getTemplates } from '../../../../lib/server/beads-fs';
|
||||
|
||||
export async function GET() {
|
||||
const data = await getTemplates();
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
|
|
@ -3,45 +3,64 @@
|
|||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
/* ========== EARTHY-DARK DESIGN SYSTEM TOKENS (PRD v2.0) ========== */
|
||||
|
||||
/* Backgrounds */
|
||||
--color-bg-base: #2D2D2D;
|
||||
--color-bg-card: #363636;
|
||||
--color-bg-input: #404040;
|
||||
/* ========== VISUAL-TRUTH UI TOKEN CONTRACT (bb-vt.1.1) ========== */
|
||||
--ui-bg-app: #070d16;
|
||||
--ui-bg-shell: #0c1420;
|
||||
--ui-bg-panel: #111c2a;
|
||||
--ui-bg-card: #1a2431;
|
||||
|
||||
/* Accents */
|
||||
--color-accent-green: #7CB97A;
|
||||
--color-accent-amber: #D4A574;
|
||||
--color-accent-teal: #5BA8A0;
|
||||
--ui-border-soft: rgba(153, 171, 190, 0.2);
|
||||
--ui-border-strong: rgba(153, 171, 190, 0.34);
|
||||
|
||||
/* Text */
|
||||
--color-text-primary: #FFFFFF;
|
||||
--color-text-secondary: #B8B8B8;
|
||||
--color-text-muted: #888888;
|
||||
--color-text-on-primary: #1A1A1A;
|
||||
--ui-text-primary: #e8edf5;
|
||||
--ui-text-muted: #8f9caf;
|
||||
|
||||
/* Borders */
|
||||
--color-border-default: #4A4A4A;
|
||||
--color-border-subtle: #3A3A3A;
|
||||
--ui-accent-ready: #35d98f;
|
||||
--ui-accent-blocked: #ff4c72;
|
||||
--ui-accent-warning: #ffb24a;
|
||||
--ui-accent-info: #35c9ff;
|
||||
--ui-accent-action-green: #35d98f;
|
||||
--ui-accent-action-red: #ff4c72;
|
||||
|
||||
--ui-font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
--ui-font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
|
||||
--ui-tracking-tight: -0.011em;
|
||||
--ui-numeric-tabular: tabular-nums;
|
||||
|
||||
/* ========== LEGACY-COMPATIBLE MAPPINGS ========== */
|
||||
--color-bg-base: var(--ui-bg-app);
|
||||
--color-bg-card: var(--ui-bg-shell);
|
||||
--color-bg-input: var(--ui-bg-panel);
|
||||
|
||||
--color-accent-green: var(--ui-accent-ready);
|
||||
--color-accent-amber: var(--ui-accent-warning);
|
||||
--color-accent-teal: var(--ui-accent-info);
|
||||
|
||||
--color-text-primary: var(--ui-text-primary);
|
||||
--color-text-secondary: #c4cfdb;
|
||||
--color-text-muted: var(--ui-text-muted);
|
||||
--color-text-on-primary: #10161d;
|
||||
|
||||
--color-border-default: var(--ui-border-strong);
|
||||
--color-border-subtle: var(--ui-border-soft);
|
||||
|
||||
/* Status colors */
|
||||
--status-open: #5BA8A0;
|
||||
--status-ready: #5BA8A0;
|
||||
--status-in-progress: #7CB97A;
|
||||
--status-progress: #7CB97A;
|
||||
--status-blocked: #D4A574;
|
||||
--status-blocked-earthy: #D4A574;
|
||||
--status-closed: #888888;
|
||||
--status-closed-earthy: #888888;
|
||||
--status-deferred: #888888;
|
||||
--status-open: var(--ui-accent-info);
|
||||
--status-ready: var(--ui-accent-ready);
|
||||
--status-in-progress: var(--ui-accent-warning);
|
||||
--status-progress: var(--ui-accent-warning);
|
||||
--status-blocked: var(--ui-accent-blocked);
|
||||
--status-blocked-earthy: var(--ui-accent-blocked);
|
||||
--status-closed: #7f8b98;
|
||||
--status-closed-earthy: #7f8b98;
|
||||
--status-deferred: #7f8b98;
|
||||
|
||||
/* Liveness colors */
|
||||
--liveness-active: #7CB97A;
|
||||
--liveness-stale: #D4A574;
|
||||
--liveness-stuck: #C97A7A;
|
||||
--liveness-dead: #C97A7A;
|
||||
--liveness-idle: #888888;
|
||||
--liveness-active: var(--ui-accent-ready);
|
||||
--liveness-stale: var(--ui-accent-warning);
|
||||
--liveness-stuck: var(--ui-accent-action-red);
|
||||
--liveness-dead: var(--ui-accent-action-red);
|
||||
--liveness-idle: #7f8b98;
|
||||
|
||||
/* Agent Role Colors */
|
||||
--agent-role-ui: #6B9BD2;
|
||||
|
|
@ -77,8 +96,8 @@
|
|||
--shadow-soft-xl: 0 20px 40px -10px rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* ========== TYPOGRAPHY ========== */
|
||||
--font-ui-stack: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
--font-mono-stack: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||
--font-ui-stack: var(--ui-font-sans);
|
||||
--font-mono-stack: var(--ui-font-mono);
|
||||
|
||||
--font-size-h1: 2rem;
|
||||
--font-size-h2: 1.5rem;
|
||||
|
|
@ -110,7 +129,17 @@
|
|||
/* ========== LAYOUT ========== */
|
||||
--sidebar-left-width: 13.75rem;
|
||||
--sidebar-right-width: 17.5rem;
|
||||
--topbar-height: 3rem;
|
||||
--topbar-height: 3.75rem;
|
||||
|
||||
--glass-base: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--ui-bg-card) 72%, black),
|
||||
color-mix(in srgb, var(--ui-bg-panel) 78%, black)
|
||||
);
|
||||
--edge-top: color-mix(in srgb, var(--ui-border-strong) 80%, white 20%);
|
||||
--edge-bottom: color-mix(in srgb, var(--ui-border-soft) 75%, black 25%);
|
||||
--elevation-ambient: 0 20px 40px -16px rgba(0, 0, 0, 0.78);
|
||||
--elevation-tight: 0 10px 24px -12px rgba(0, 0, 0, 0.7);
|
||||
|
||||
/* ========== LEGACY COMPATIBILITY TOKENS ========== */
|
||||
/* For existing components that reference these */
|
||||
|
|
@ -134,11 +163,10 @@ body {
|
|||
}
|
||||
|
||||
body {
|
||||
/* Earthy-dark base from PRD (replaces Aero Chrome) */
|
||||
background-color: var(--color-bg-base);
|
||||
color: var(--color-text-secondary);
|
||||
font-family: var(--font-ui-stack);
|
||||
letter-spacing: -0.011em;
|
||||
background-color: var(--ui-bg-app);
|
||||
color: var(--ui-text-primary);
|
||||
font-family: var(--ui-font-sans);
|
||||
letter-spacing: var(--ui-tracking-tight);
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
|
@ -266,24 +294,52 @@ body {
|
|||
|
||||
.ui-select option,
|
||||
.ui-option {
|
||||
background-color: #10141d;
|
||||
color: #e2e8f0;
|
||||
background-color: var(--ui-bg-panel);
|
||||
color: var(--ui-text-primary);
|
||||
}
|
||||
|
||||
.ui-text {
|
||||
font-family: var(--font-ui-stack);
|
||||
font-family: var(--ui-font-sans);
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
letter-spacing: var(--ui-tracking-tight);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.system-data {
|
||||
font-family: var(--font-mono-stack);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: var(--ui-font-mono);
|
||||
font-variant-numeric: var(--ui-numeric-tabular);
|
||||
font-weight: 450;
|
||||
letter-spacing: 0.015em;
|
||||
}
|
||||
|
||||
.ui-shell-app {
|
||||
background: var(--ui-bg-app);
|
||||
color: var(--ui-text-primary);
|
||||
font-family: var(--ui-font-sans);
|
||||
letter-spacing: var(--ui-tracking-tight);
|
||||
}
|
||||
|
||||
.ui-shell-topbar {
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--ui-bg-panel) 92%, black), var(--ui-bg-shell));
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--ui-accent-info) 22%, var(--ui-border-soft));
|
||||
box-shadow: 0 10px 24px -20px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.ui-shell-middle {
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--ui-bg-app) 74%, black), color-mix(in srgb, var(--ui-bg-app) 90%, black));
|
||||
border-left: 1px solid color-mix(in srgb, var(--ui-accent-info) 20%, var(--ui-border-soft));
|
||||
border-right: 1px solid color-mix(in srgb, var(--ui-accent-info) 20%, var(--ui-border-soft));
|
||||
}
|
||||
|
||||
.ui-shell-panel {
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--ui-bg-shell) 86%, black), color-mix(in srgb, var(--ui-bg-panel) 84%, black));
|
||||
border-left: 1px solid color-mix(in srgb, var(--ui-accent-info) 30%, var(--ui-border-soft));
|
||||
}
|
||||
|
||||
.ui-tabular-nums {
|
||||
font-variant-numeric: var(--ui-numeric-tabular);
|
||||
}
|
||||
|
||||
|
||||
.workflow-graph-legend {
|
||||
backdrop-filter: blur(12px);
|
||||
|
|
|
|||
|
|
@ -1,78 +1,5 @@
|
|||
import { SessionsPage } from '../../components/sessions/sessions-page';
|
||||
import type { SwarmGroup } from '../../components/sessions/sessions-header';
|
||||
import { readIssuesForScope } from '../../lib/aggregate-read';
|
||||
import { resolveProjectScope } from '../../lib/project-scope';
|
||||
import { listProjects } from '../../lib/registry';
|
||||
import { listAgents } from '../../lib/agent-registry';
|
||||
import { getSwarmMembers } from '../../lib/swarm-molecules';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
interface PageProps {
|
||||
searchParams?: Promise<Record<string, string | string[] | undefined>>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const params = (await searchParams) ?? {};
|
||||
const requestedProjectKey = typeof params.project === 'string' ? params.project : null;
|
||||
const requestedMode = typeof params.mode === 'string' ? params.mode : null;
|
||||
const registryProjects = await listProjects();
|
||||
const agentsResult = await listAgents({});
|
||||
const agents = agentsResult.data ?? [];
|
||||
|
||||
const scope = resolveProjectScope({
|
||||
currentProjectRoot: process.cwd(),
|
||||
registryProjects,
|
||||
requestedProjectKey,
|
||||
requestedMode,
|
||||
});
|
||||
|
||||
const issues = await readIssuesForScope({
|
||||
mode: scope.mode,
|
||||
selected: scope.selected,
|
||||
scopeOptions: scope.options,
|
||||
preferBd: true,
|
||||
});
|
||||
|
||||
const epics = issues.filter(i => i.issue_type === 'epic');
|
||||
const epicsWithSwarm = epics.filter(
|
||||
i => (i.labels || []).some(l => l.startsWith('swarm:'))
|
||||
);
|
||||
|
||||
const swarmGroups: SwarmGroup[] = [];
|
||||
const assignedAgentIds = new Set<string>();
|
||||
|
||||
for (const epic of epicsWithSwarm) {
|
||||
const swarmLabel = epic.labels?.find(l => l.startsWith('swarm:'));
|
||||
if (!swarmLabel) continue;
|
||||
|
||||
const swarmId = swarmLabel.replace('swarm:', '');
|
||||
const memberIds = await getSwarmMembers({ swarmId }, { projectRoot: scope.selected.root });
|
||||
|
||||
const members = agents.filter(a => memberIds.includes(a.agent_id));
|
||||
members.forEach(a => assignedAgentIds.add(a.agent_id));
|
||||
|
||||
if (members.length > 0) {
|
||||
swarmGroups.push({
|
||||
swarmId,
|
||||
swarmLabel: epic.id,
|
||||
members,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const unassignedAgents = agents.filter(a => !assignedAgentIds.has(a.agent_id));
|
||||
|
||||
return (
|
||||
<SessionsPage
|
||||
issues={issues}
|
||||
agents={agents}
|
||||
projectRoot={scope.selected.root}
|
||||
projectScopeKey={scope.selected.key}
|
||||
projectScopeOptions={scope.options}
|
||||
projectScopeMode={scope.mode}
|
||||
swarmGroups={swarmGroups}
|
||||
unassignedAgents={unassignedAgents}
|
||||
/>
|
||||
);
|
||||
export default function SessionsRedirectPage() {
|
||||
redirect('/?view=social');
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue