"use client"; import { useEffect, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import type { AgentMessageRole, AgentSessionStatus } from "@mosaic/shared"; import { apiGet } from "@/lib/api/client"; const MISSION_CONTROL_SESSIONS_QUERY_KEY = ["mission-control", "sessions"] as const; const SESSIONS_REFRESH_INTERVAL_MS = 15_000; export type MissionControlMessageRole = AgentMessageRole; export interface MissionControlSession { id: string; providerId: string; providerType: string; label?: string; status: AgentSessionStatus; parentSessionId?: string; createdAt: string; updatedAt: string; metadata?: Record; } interface MissionControlSessionsResponse { sessions: MissionControlSession[]; } export interface MissionControlStreamMessage { id: string; sessionId: string; role: MissionControlMessageRole; content: string; timestamp: string; metadata?: Record; } export type MissionControlConnectionStatus = "connecting" | "connected" | "error"; export interface UseSessionsResult { sessions: MissionControlSession[]; loading: boolean; error: Error | null; } export interface UseSessionStreamResult { messages: MissionControlStreamMessage[]; status: MissionControlConnectionStatus; error: string | null; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function isMessageRole(value: unknown): value is MissionControlMessageRole { return value === "assistant" || value === "system" || value === "tool" || value === "user"; } function isMissionControlStreamMessage(value: unknown): value is MissionControlStreamMessage { if (!isRecord(value)) { return false; } const { id, sessionId, role, content, timestamp, metadata } = value; if ( typeof id !== "string" || typeof sessionId !== "string" || !isMessageRole(role) || typeof content !== "string" || typeof timestamp !== "string" ) { return false; } if (metadata !== undefined && !isRecord(metadata)) { return false; } return true; } /** * Fetches Mission Control sessions. */ export function useSessions(): UseSessionsResult { const query = useQuery({ queryKey: MISSION_CONTROL_SESSIONS_QUERY_KEY, queryFn: async (): Promise => { return apiGet("/api/mission-control/sessions"); }, refetchInterval: SESSIONS_REFRESH_INTERVAL_MS, }); return { sessions: query.data?.sessions ?? [], loading: query.isLoading, error: query.error ?? null, }; } /** * Backward-compatible alias for early Mission Control integration. */ export function useMissionControl(): UseSessionsResult { return useSessions(); } /** * Streams Mission Control session messages over SSE. */ export function useSessionStream(sessionId: string): UseSessionStreamResult { const [messages, setMessages] = useState([]); const [status, setStatus] = useState("connecting"); const [error, setError] = useState(null); const eventSourceRef = useRef(null); useEffect(() => { if (eventSourceRef.current !== null) { eventSourceRef.current.close(); eventSourceRef.current = null; } setMessages([]); setError(null); if (!sessionId) { setStatus("connecting"); return; } if (typeof EventSource === "undefined") { setStatus("error"); setError("Mission Control stream is not supported by this browser."); return; } setStatus("connecting"); const source = new EventSource( `/api/mission-control/sessions/${encodeURIComponent(sessionId)}/stream` ); eventSourceRef.current = source; source.onopen = (): void => { setStatus("connected"); setError(null); }; source.onmessage = (event: MessageEvent): void => { try { const parsed = JSON.parse(event.data) as unknown; if (!isMissionControlStreamMessage(parsed)) { return; } setMessages((previousMessages) => [...previousMessages, parsed]); } catch { // Ignore malformed events from the stream. } }; source.onerror = (): void => { if (source.readyState === EventSource.CONNECTING) { setStatus("connecting"); setError(null); return; } setStatus("error"); setError("Mission Control stream disconnected."); }; return (): void => { source.close(); if (eventSourceRef.current === source) { eventSourceRef.current = null; } }; }, [sessionId]); return { messages, status, error, }; }