Compare commits

..

1 Commits

Author SHA1 Message Date
52e7b0e6e7 feat(orchestrator): add mission control proxy api
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-03-07 13:34:57 -06:00
8 changed files with 0 additions and 177 deletions

View File

@@ -1,67 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { AgentSession } from "@mosaic/shared";
import type { PrismaService } from "../../prisma/prisma.service";
import { AgentProviderRegistry } from "../agents/agent-provider.registry";
import { MissionControlController } from "./mission-control.controller";
import { MissionControlService } from "./mission-control.service";
describe("MissionControlController", () => {
let controller: MissionControlController;
let registry: {
listAllSessions: ReturnType<typeof vi.fn>;
getProviderForSession: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
registry = {
listAllSessions: vi.fn(),
getProviderForSession: vi.fn(),
};
const prisma = {
operatorAuditLog: {
create: vi.fn().mockResolvedValue(undefined),
},
};
const service = new MissionControlService(
registry as unknown as AgentProviderRegistry,
prisma as unknown as PrismaService
);
controller = new MissionControlController(service);
});
it("Phase 1 gate: unified sessions endpoint returns internal provider sessions", async () => {
const internalSession: AgentSession = {
id: "session-internal-1",
providerId: "internal",
providerType: "internal",
status: "active",
createdAt: new Date("2026-03-07T20:00:00.000Z"),
updatedAt: new Date("2026-03-07T20:01:00.000Z"),
};
const externalSession: AgentSession = {
id: "session-openclaw-1",
providerId: "openclaw",
providerType: "external",
status: "active",
createdAt: new Date("2026-03-07T20:02:00.000Z"),
updatedAt: new Date("2026-03-07T20:03:00.000Z"),
};
registry.listAllSessions.mockResolvedValue([internalSession, externalSession]);
const response = await controller.listSessions();
expect(registry.listAllSessions).toHaveBeenCalledTimes(1);
expect(response.sessions).toEqual([internalSession, externalSession]);
expect(response.sessions).toContainEqual(
expect.objectContaining({
id: "session-internal-1",
providerId: "internal",
})
);
});
});

View File

@@ -1,5 +0,0 @@
import { MissionControlLayout } from "@/components/mission-control/MissionControlLayout";
export default function MissionControlPage(): React.JSX.Element {
return <MissionControlLayout />;
}

View File

@@ -156,26 +156,6 @@ function IconTerminal(): React.JSX.Element {
);
}
function IconMissionControl(): React.JSX.Element {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
aria-hidden="true"
>
<circle cx="8" cy="8" r="1.5" />
<path d="M11 5a4.25 4.25 0 0 1 0 6" />
<path d="M5 5a4.25 4.25 0 0 0 0 6" />
<path d="M13.5 2.5a7.75 7.75 0 0 1 0 11" />
<path d="M2.5 2.5a7.75 7.75 0 0 0 0 11" />
</svg>
);
}
function IconSettings(): React.JSX.Element {
return (
<svg
@@ -280,11 +260,6 @@ const NAV_GROUPS: NavGroup[] = [
label: "Terminal",
icon: <IconTerminal />,
},
{
href: "/mission-control",
label: "Mission Control",
icon: <IconMissionControl />,
},
],
},
{

View File

@@ -1,16 +0,0 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export function GlobalAgentRoster(): React.JSX.Element {
return (
<Card className="flex h-full min-h-0 flex-col">
<CardHeader>
<CardTitle className="text-base">Agent Roster</CardTitle>
</CardHeader>
<CardContent className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
No active agents
</CardContent>
</Card>
);
}

View File

@@ -1,21 +0,0 @@
"use client";
import { GlobalAgentRoster } from "@/components/mission-control/GlobalAgentRoster";
import { MissionControlPanel } from "@/components/mission-control/MissionControlPanel";
const DEFAULT_PANEL_SLOTS = ["panel-1", "panel-2", "panel-3", "panel-4"] as const;
export function MissionControlLayout(): React.JSX.Element {
return (
<section className="h-full min-h-0 overflow-hidden" aria-label="Mission Control">
<div className="grid h-full min-h-0 gap-4 xl:grid-cols-[280px_minmax(0,1fr)]">
<aside className="h-full min-h-0">
<GlobalAgentRoster />
</aside>
<main className="h-full min-h-0 overflow-hidden">
<MissionControlPanel panels={DEFAULT_PANEL_SLOTS} />
</main>
</div>
</section>
);
}

View File

@@ -1,17 +0,0 @@
"use client";
import { OrchestratorPanel } from "@/components/mission-control/OrchestratorPanel";
interface MissionControlPanelProps {
panels: readonly string[];
}
export function MissionControlPanel({ panels }: MissionControlPanelProps): React.JSX.Element {
return (
<div className="grid h-full min-h-0 auto-rows-fr grid-cols-1 gap-4 overflow-y-auto pr-1 md:grid-cols-2">
{panels.map((panelId) => (
<OrchestratorPanel key={panelId} />
))}
</div>
);
}

View File

@@ -1,16 +0,0 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export function OrchestratorPanel(): React.JSX.Element {
return (
<Card className="flex h-full min-h-[220px] flex-col">
<CardHeader>
<CardTitle className="text-base">Orchestrator Panel</CardTitle>
</CardHeader>
<CardContent className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
Select an agent
</CardContent>
</Card>
);
}

View File

@@ -1,10 +0,0 @@
interface UseMissionControlResult {
sessions: [];
loading: boolean;
error: null;
}
// Stub — will be wired in P2-002
export function useMissionControl(): UseMissionControlResult {
return { sessions: [], loading: false, error: null };
}