Files
kandev/apps/web/lib/prepare/summarize.ts
T
José Almeida 1f9ead5dc5 refactor(web): lift prepare-summary reducer + isFallbackNoticeStep into lib/prepare
Move the popover's prepare-summary reducer (and the
`isFallbackNoticeStep` helper duplicated word-for-word in
`prepare-progress.tsx`) into `apps/web/lib/prepare/summarize.ts`. The
hook moves to `hooks/domains/session/use-prepare-summary.ts` to match
the project's domain-organized hook layout.

The popover's PrepareSummary type and the chat panel's EffectiveStatus
serve different UIs, so the panel keeps its own `deriveStatus` — only
the literal duplicate (`isFallbackNoticeStep`) is shared.
2026-05-07 00:44:44 +01:00

117 lines
3.7 KiB
TypeScript

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;
};
export const IDLE_PREPARE_SUMMARY: PrepareSummary = {
phase: "idle",
current: null,
currentIndex: 0,
totalSteps: 0,
fallbackWarning: null,
failedStep: null,
durationMs: null,
};
export function isPreparingPhase(phase: PreparePhase): boolean {
return phase === "preparing" || phase === "preparing_fallback" || phase === "resuming";
}
// isFallbackNoticeStep returns true when a step is the "previous sandbox went
// away, we provisioned a fresh one" notice. It carries a warning rather than
// an error, so the only signal that the prepare succeeded *with a recovery*
// (not just a generic warning) is matching this row by name + status.
export function isFallbackNoticeStep(step: PrepareStepInfo): boolean {
return step.status === "skipped" && step.name === "Reconnecting cloud sandbox";
}
/**
* summarizePrepare reduces the prepare-progress slice + session state for one
* session into a UI-friendly summary used by the executor popover. 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 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 lastIndexWhere<T>(items: T[], pred: (item: T) => boolean): number {
for (let i = items.length - 1; i >= 0; i--) {
if (pred(items[i])) return i;
}
return -1;
}