"use client"; import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { AgentSession } from "@mosaic/shared"; import { ChevronRight, Loader2, X } from "lucide-react"; import { KillAllDialog } from "@/components/mission-control/KillAllDialog"; import { Badge } from "@/components/ui/badge"; import type { BadgeVariant } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Collapsible } from "@/components/ui/collapsible"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Skeleton } from "@/components/ui/skeleton"; import { apiGet, apiPost } from "@/lib/api/client"; const SESSIONS_QUERY_KEY = ["mission-control", "sessions"] as const; const SESSIONS_POLL_INTERVAL_MS = 5_000; type MissionControlSessionStatus = AgentSession["status"] | "killed"; interface MissionControlSession extends Omit { status: MissionControlSessionStatus; createdAt: string | Date; updatedAt: string | Date; } interface SessionsPayload { sessions: MissionControlSession[]; } interface ProviderSessionGroup { providerId: string; providerType: string; sessions: MissionControlSession[]; } export interface GlobalAgentRosterProps { onSelectSession?: (sessionId: string) => void; selectedSessionId?: string | undefined; } function getStatusVariant(status: MissionControlSessionStatus): BadgeVariant { switch (status) { case "active": return "status-success"; case "paused": return "status-warning"; case "killed": return "status-error"; default: return "status-neutral"; } } function truncateSessionId(sessionId: string): string { return sessionId.slice(0, 8); } function resolveProviderName(providerId: string, providerType: string): string { return providerId === providerType ? providerId : `${providerId} (${providerType})`; } function groupByProvider(sessions: MissionControlSession[]): ProviderSessionGroup[] { const grouped = new Map(); for (const session of sessions) { const existing = grouped.get(session.providerId); if (existing) { existing.sessions.push(session); continue; } grouped.set(session.providerId, { providerId: session.providerId, providerType: session.providerType, sessions: [session], }); } return Array.from(grouped.values()).sort((a, b) => a.providerId.localeCompare(b.providerId)); } async function fetchSessions(): Promise { const payload = await apiGet( "/api/mission-control/sessions" ); return Array.isArray(payload) ? payload : payload.sessions; } function toKillAllSessions(sessions: MissionControlSession[]): AgentSession[] { return sessions .filter( (session): session is MissionControlSession & { status: AgentSession["status"] } => session.status !== "killed" ) .map((session) => ({ ...session, createdAt: session.createdAt instanceof Date ? session.createdAt : new Date(session.createdAt), updatedAt: session.updatedAt instanceof Date ? session.updatedAt : new Date(session.updatedAt), })); } export function GlobalAgentRoster({ onSelectSession, selectedSessionId, }: GlobalAgentRosterProps): React.JSX.Element { const queryClient = useQueryClient(); const [openProviders, setOpenProviders] = useState>({}); const sessionsQuery = useQuery({ queryKey: SESSIONS_QUERY_KEY, queryFn: fetchSessions, refetchInterval: SESSIONS_POLL_INTERVAL_MS, }); const killMutation = useMutation({ mutationFn: async (sessionId: string): Promise => { await apiPost<{ message: string }>(`/api/mission-control/sessions/${sessionId}/kill`, { force: false, }); return sessionId; }, onSuccess: (): void => { void queryClient.invalidateQueries({ queryKey: SESSIONS_QUERY_KEY }); }, }); const groupedSessions = useMemo( () => groupByProvider(sessionsQuery.data ?? []), [sessionsQuery.data] ); const killAllSessions = useMemo( () => toKillAllSessions(sessionsQuery.data ?? []), [sessionsQuery.data] ); const totalSessionCount = sessionsQuery.data?.length ?? 0; const pendingKillSessionId = killMutation.isPending ? killMutation.variables : undefined; const toggleProvider = (providerId: string): void => { setOpenProviders((prev) => ({ ...prev, [providerId]: !(prev[providerId] ?? true), })); }; const isProviderOpen = (providerId: string): boolean => openProviders[providerId] ?? true; const handleKillAllComplete = (): void => { void queryClient.invalidateQueries({ queryKey: SESSIONS_QUERY_KEY }); }; return ( Agent Roster
{totalSessionCount > 0 ? ( ) : null} {sessionsQuery.isFetching && !sessionsQuery.isLoading ? (
{sessionsQuery.isLoading ? (
{Array.from({ length: 6 }).map((_, index) => ( ))}
) : sessionsQuery.error ? (
Failed to load agents: {sessionsQuery.error.message}
) : groupedSessions.length === 0 ? (
No active agents
) : (
{groupedSessions.map((group) => { const providerOpen = isProviderOpen(group.providerId); return ( {providerOpen ? (
{group.sessions.map((session) => { const isSelected = selectedSessionId === session.id; const isKilling = pendingKillSessionId === session.id; return (
{ onSelectSession?.(session.id); }} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onSelectSession?.(session.id); } }} className="flex items-center justify-between gap-2 rounded-md border border-transparent px-2 py-1.5 transition-colors hover:bg-muted/40" style={ isSelected ? { borderColor: "rgba(47, 128, 255, 0.35)", backgroundColor: "rgba(47, 128, 255, 0.08)", } : undefined } >
{truncateSessionId(session.id)} {session.status}
); })}
) : null}
); })}
)}
); }