/** * Authoritative desktop boot outcome types and UI-facing resolver. * * The Rust backend computes a `DesktopBootOutcome` at startup and injects * it as `window.__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__`. This module provides * pure functions to read that outcome and derive the minimal UI state * needed for the loading/chooser/recovery/main decision. */ // ── Boot outcome (must match Rust injection) ── /** * Structured boot outcome type. * * Instead of 8 magic string kinds, we use a structured type that clearly * separates the target (local/remote/null) from the status (ok/not-configured/error). * * This makes it easier to add new states without updating multiple files and * allows UI to reason about outcomes with simple status checks. */ type DesktopBootAvailability = { localAvailable?: boolean }; export type DesktopBootOutcome = // Main screens - CLI or remote connection is working | ({ target: 'local'; status: 'ok' } & DesktopBootAvailability) | ({ target: 'remote'; status: 'ok'; hostId: string; url: string } & DesktopBootAvailability) // First launch - user hasn't made a choice yet | ({ target: null; status: 'not-configured' } & DesktopBootAvailability) // Recovery screens - something is wrong | ({ target: 'local'; status: 'unreachable' } & DesktopBootAvailability) | ({ target: 'remote'; status: 'unreachable'; hostId: string; url: string } & DesktopBootAvailability) | ({ target: 'remote'; status: 'incompatible'; hostId: string; url: string } & DesktopBootAvailability) | ({ target: 'remote'; status: 'wrong-service'; hostId: string; url: string } & DesktopBootAvailability) | ({ target: 'remote'; status: 'missing'; hostId: string } & DesktopBootAvailability); // ── UI-facing view ── export type DesktopBootView = | ({ screen: 'main' } & DesktopBootAvailability) | ({ screen: 'main'; hostId: string; url: string } & DesktopBootAvailability) | ({ screen: 'chooser' } & DesktopBootAvailability) | ({ screen: 'recovery'; variant: 'local-unavailable' } & DesktopBootAvailability) | ({ screen: 'recovery'; variant: 'remote-unreachable'; hostId: string; url: string } & DesktopBootAvailability) | ({ screen: 'recovery'; variant: 'remote-incompatible'; hostId: string; url: string } & DesktopBootAvailability) | ({ screen: 'recovery'; variant: 'remote-wrong-service'; hostId: string; url: string } & DesktopBootAvailability) | ({ screen: 'recovery'; variant: 'remote-missing'; hostId: string } & DesktopBootAvailability); // ── Resolver inputs ── export type DesktopBootViewInput = { isDesktopShell: boolean; bootOutcome: DesktopBootOutcome | null; }; // ── Public API ── /** Valid target values */ const VALID_TARGETS = ['local', 'remote', null] as const; /** Valid status values */ const VALID_STATUSES = ['ok', 'not-configured', 'unreachable', 'incompatible', 'wrong-service', 'missing'] as const; /** Return type for `validateBootOutcome`. */ type ValidationResult = | { valid: true; outcome: DesktopBootOutcome } | { valid: false }; /** * Runtime-validate a raw injected payload. * Returns a tagged result so callers can distinguish "not set yet" (null raw) * from "set but malformed" (valid: false). */ function validateBootOutcome(raw: unknown): ValidationResult { if (!raw || typeof raw !== 'object') { return { valid: false }; } const record = raw as Record; const availability = record.localAvailable === false ? { localAvailable: false } : {}; const target = record.target; const status = record.status; // Validate target if (target !== null && (typeof target !== 'string' || !VALID_TARGETS.includes(target as never))) { return { valid: false }; } // Validate status if (typeof status !== 'string' || !VALID_STATUSES.includes(status as never)) { return { valid: false }; } // Validate required fields per combination if (target === 'remote' || target === 'local') { if (status === 'ok' && target === 'local') { // { target: 'local'; status: 'ok' } is valid return { valid: true, outcome: { target: 'local', status: 'ok', ...availability } }; } if (status === 'ok' && target === 'remote') { // { target: 'remote'; status: 'ok' } requires hostId and url if (typeof record.hostId !== 'string' || typeof record.url !== 'string') { return { valid: false }; } return { valid: true, outcome: { target: 'remote', status: 'ok', hostId: record.hostId, url: record.url, ...availability } }; } if (status === 'unreachable') { if (target === 'local') { // { target: 'local'; status: 'unreachable' } is valid return { valid: true, outcome: { target: 'local', status: 'unreachable', ...availability } }; } else { // { target: 'remote'; status: 'unreachable' } requires hostId and url if (typeof record.hostId !== 'string' || typeof record.url !== 'string') { return { valid: false }; } return { valid: true, outcome: { target: 'remote', status: 'unreachable', hostId: record.hostId, url: record.url, ...availability } }; } } if (status === 'incompatible' || status === 'wrong-service') { if (target !== 'remote') return { valid: false }; if (typeof record.hostId !== 'string' || typeof record.url !== 'string') { return { valid: false }; } return { valid: true, outcome: { target: 'remote', status, hostId: record.hostId, url: record.url, ...availability } }; } if (status === 'missing') { if (target !== 'remote') return { valid: false }; if (typeof record.hostId !== 'string') { return { valid: false }; } return { valid: true, outcome: { target: 'remote', status: 'missing', hostId: record.hostId, ...availability } }; } } if (target === null) { if (status === 'not-configured') { // { target: null; status: 'not-configured' } is valid (first launch) return { valid: true, outcome: { target: null, status: 'not-configured', ...availability } }; } if (status === 'missing') { // { target: null; status: 'missing' } would be redundant with not-configured return { valid: false }; } } return { valid: false }; } /** * Derive the minimal UI view from the injected boot outcome. * * Returns `null` when not in desktop shell, when the outcome is not yet * known, or when the injected payload is malformed. */ export function resolveDesktopBootView( input: DesktopBootViewInput, ): DesktopBootView | null { if (!input.isDesktopShell) { return null; } const outcome = input.bootOutcome; if (!outcome) { return null; } const availability = outcome.localAvailable === false ? { localAvailable: false } : {}; // Main screens - CLI or remote connection is working if (outcome.status === 'ok') { if (outcome.target === 'local') { return { screen: 'main', ...availability }; } else if (outcome.target === 'remote') { return { screen: 'main', hostId: outcome.hostId, url: outcome.url, ...availability }; } } // First launch - user hasn't made a choice yet if (outcome.target === null && outcome.status === 'not-configured') { return { screen: 'chooser', ...availability }; } // Recovery screens - something is wrong if (outcome.target === 'local' && outcome.status === 'unreachable') { return { screen: 'chooser', ...availability }; } if (outcome.target === 'remote') { if (outcome.status === 'unreachable') { return { screen: 'recovery', variant: 'remote-unreachable', hostId: outcome.hostId, url: outcome.url, ...availability }; } else if (outcome.status === 'incompatible') { return { screen: 'recovery', variant: 'remote-incompatible', hostId: outcome.hostId, url: outcome.url, ...availability }; } else if (outcome.status === 'wrong-service') { return { screen: 'recovery', variant: 'remote-wrong-service', hostId: outcome.hostId, url: outcome.url, ...availability }; } else if (outcome.status === 'missing') { return { screen: 'recovery', variant: 'remote-missing', hostId: outcome.hostId, ...availability }; } } // Unknown outcome — defensive null. return null; } // ── Loading gate ── export type BootInjectionStatus = | 'not-injected' | 'malformed' | 'valid'; export type InitialLoadingState = { isDesktopShell: boolean; isInitialized: boolean; bootOutcomeKnown: boolean; /** * Whether the resolved boot view is 'main'. * When false (chooser/recovery), splash dismisses on bootOutcomeKnown alone. * When true or absent, splash also requires isInitialized. */ bootViewIsMain?: boolean; }; export type DesktopBootFlowRestartInput = { isDesktopShell: boolean; isDesktopLocalOriginActive: boolean; }; /** * Whether the initial loading screen can be dismissed. * * Desktop shells must wait until a valid boot outcome is injected by the native host. * For non-main views (chooser, recovery), the splash can dismiss as soon as * the outcome is known — `isInitialized` is not required because OpenCode * may not be available in those flows. * For main views, both `isInitialized` and `bootOutcomeKnown` are required. * Non-desktop shells only need the app to be initialized. */ export function canDismissInitialLoading(state: InitialLoadingState): boolean { if (!state.isDesktopShell) { return state.isInitialized; } if (!state.bootOutcomeKnown) { return false; } // Non-main boot views (chooser, recovery) can dismiss without waiting for init. if (state.bootViewIsMain === false) { return true; } return state.isInitialized; } /** * Boot/recovery UI can render in the desktop startup window before the local * desktop HTTP origin is active. In that state, same-origin reloads and * `/api/*` requests cannot recover the app, so callers must restart desktop. */ export function shouldRestartDesktopBootFlow(input: DesktopBootFlowRestartInput): boolean { return input.isDesktopShell && !input.isDesktopLocalOriginActive; } /** * Read the boot outcome injected by the native desktop host. * Returns `null` when not in desktop, when the outcome has not been set yet, * or when the injected payload is malformed. */ export function getInjectedBootOutcome(): DesktopBootOutcome | null { const status = getBootInjectionStatus(); if (status !== 'valid') { return null; } const raw = (window as { __OPENCHAMBER_DESKTOP_BOOT_OUTCOME__?: unknown }) .__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__; const result = validateBootOutcome(raw); return result.valid ? result.outcome : null; } /** * Check the injection status of the desktop boot outcome. * * Distinguishes three states: * - `'not-injected'`: the global is absent or null (keep waiting) * - `'malformed'`: the global is present but failed validation (deterministic failure) * - `'valid'`: the global is present and passes validation */ export function getBootInjectionStatus(): BootInjectionStatus { if (typeof window === 'undefined') { return 'not-injected'; } const raw = (window as { __OPENCHAMBER_DESKTOP_BOOT_OUTCOME__?: unknown }) .__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__; if (raw === undefined || raw === null) { return 'not-injected'; } const result = validateBootOutcome(raw); return result.valid ? 'valid' : 'malformed'; }