"use client"; import { useMemo } from "react"; import { IconAlertTriangle, IconCheck, IconLoader2 } from "@tabler/icons-react"; import { useAppStore } from "@/components/state-provider"; import type { PrepareStepInfo, SessionPrepareState, } from "@/lib/state/slices/session-runtime/types"; export type PreparePhase = | "idle" | "preparing" | "preparing_fallback" | "resuming" | "ready" | "failed"; export type PrepareSummary = { phase: PreparePhase; current: PrepareStepInfo | null; currentIndex: number; totalSteps: number; fallbackWarning: string | null; failedStep: PrepareStepInfo | null; durationMs: number | null; }; const IDLE_PREPARE_SUMMARY: PrepareSummary = { phase: "idle", current: null, currentIndex: 0, totalSteps: 0, fallbackWarning: null, failedStep: null, durationMs: null, }; /** * Reduces the prepare-progress slice + session state for one session into a * UI-friendly summary. The reducer is the only place that decides what counts * as "still preparing" vs "ready" vs "failed", and it surfaces: * - the missing-sandbox fallback as a first-class "preparing_fallback" phase * so the popover renders dedicated UI for the recovery path; * - a "resuming" phase synthesized from session.state == STARTING when the * backend skips prepare events on resume, so the popover still shows a * spinner instead of looking idle for the entire reconnect. */ export function usePrepareSummary(sessionId: string | null): PrepareSummary { const prepareState = useAppStore((state) => sessionId ? (state.prepareProgress?.bySessionId?.[sessionId] ?? null) : null, ); const sessionState = useAppStore((state) => sessionId ? (state.taskSessions?.items?.[sessionId]?.state ?? null) : null, ); return useMemo(() => summarizePrepare(prepareState, sessionState), [prepareState, sessionState]); } export function isPreparingPhase(phase: PreparePhase): boolean { return phase === "preparing" || phase === "preparing_fallback" || phase === "resuming"; } function summarizePrepare( state: SessionPrepareState | null, sessionState: string | null, ): PrepareSummary { if (!state) { if (sessionState === "STARTING") { return { ...IDLE_PREPARE_SUMMARY, phase: "resuming" }; } return IDLE_PREPARE_SUMMARY; } const steps = state.steps ?? []; const fallbackStep = steps.find(isFallbackNoticeStep) ?? null; const failedStep = steps.find((s) => s.status === "failed") ?? null; const runningStep = steps.find((s) => s.status === "running") ?? null; const { current, currentIndex } = pickCurrentStep(steps, runningStep); return { phase: derivePhase(state.status, fallbackStep, failedStep), current, currentIndex, totalSteps: steps.length, fallbackWarning: fallbackStep?.warning ?? null, failedStep, durationMs: state.durationMs ?? null, }; } function pickCurrentStep( steps: PrepareStepInfo[], runningStep: PrepareStepInfo | null, ): { current: PrepareStepInfo | null; currentIndex: number } { if (runningStep) { return { current: runningStep, currentIndex: steps.indexOf(runningStep) }; } const lastCompletedIdx = lastIndexWhere( steps, (s) => s.status === "completed" || s.status === "skipped" || s.status === "failed", ); if (lastCompletedIdx >= 0) { return { current: steps[lastCompletedIdx], currentIndex: lastCompletedIdx }; } return { current: steps[0] ?? null, currentIndex: 0 }; } function derivePhase( status: string, fallbackStep: PrepareStepInfo | null, failedStep: PrepareStepInfo | null, ): PreparePhase { if (status === "preparing") return fallbackStep ? "preparing_fallback" : "preparing"; if (status === "failed" || failedStep) return "failed"; if (status === "completed") return "ready"; return "idle"; } function isFallbackNoticeStep(step: PrepareStepInfo): boolean { return step.status === "skipped" && step.name === "Reconnecting cloud sandbox"; } function lastIndexWhere(items: T[], pred: (item: T) => boolean): number { for (let i = items.length - 1; i >= 0; i--) { if (pred(items[i])) return i; } return -1; } export function PrepareStatusSection({ summary }: { summary: PrepareSummary }) { if (summary.phase === "idle") return null; if (summary.phase === "resuming") return ; if (summary.phase === "preparing" || summary.phase === "preparing_fallback") { return ; } if (summary.phase === "failed") return ; return ; } function ResumingRow() { return (
Resuming session
Reconnecting to the existing environment…
); } function PreparingRow({ summary }: { summary: PrepareSummary }) { const stepLabel = summary.current?.name ?? "Preparing..."; const stepNumber = Math.min(summary.currentIndex + 1, summary.totalSteps); return (
Preparing environment
{summary.totalSteps > 0 ? `Step ${stepNumber} of ${summary.totalSteps}: ${stepLabel}` : stepLabel}
{summary.phase === "preparing_fallback" && summary.fallbackWarning && (
{summary.fallbackWarning}
)}
); } function FailedRow({ summary }: { summary: PrepareSummary }) { return (
Environment preparation failed
{summary.failedStep && (
Failed at: {summary.failedStep.name}
)} {summary.failedStep?.error && (
          {summary.failedStep.error}
        
)}
); } function ReadyRow({ summary }: { summary: PrepareSummary }) { return (
Environment ready {summary.durationMs ? ` · ${formatDuration(summary.durationMs)}` : ""}
); } function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; const secs = Math.round(ms / 1000); if (secs < 60) return `${secs}s`; const mins = Math.floor(secs / 60); const remSecs = secs % 60; return remSecs > 0 ? `${mins}m ${remSecs}s` : `${mins}m`; }