import React from 'react'; import { MainLayout } from '@/components/layout/MainLayout'; import { ChatView } from '@/components/views/ChatView'; import { FireworksProvider } from '@/contexts/FireworksContext'; import { Toaster } from '@/components/ui/sonner'; import { Button } from '@/components/ui/button'; import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel'; import { setStreamPerfEnabled } from '@/stores/utils/streamDebug'; import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; // useEventStream removed — replaced by SyncProvider + SyncBridge import { useMenuActions } from '@/hooks/useMenuActions'; import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap'; import { useTraySync } from '@/hooks/useTraySync'; import { useRouter } from '@/hooks/useRouter'; import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon'; import { useWebNotificationStream } from '@/hooks/useWebNotificationStream'; import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt'; import { useWindowTitle } from '@/hooks/useWindowTitle'; import { useConfigStore } from '@/stores/useConfigStore'; import { hasModifier } from '@/lib/utils'; import { isDesktopLocalOriginActive, isDesktopShell, restartDesktopApp, invokeDesktop } from '@/lib/desktop'; import { getInjectedBootOutcome, getBootInjectionStatus, resolveDesktopBootView, canDismissInitialLoading, shouldRestartDesktopBootFlow, type BootInjectionStatus, type DesktopBootView, } from '@/lib/desktopBoot'; import type { RecoveryVariant } from '@/components/onboarding/DesktopConnectionRecovery'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { markSessionViewed } from '@/sync/notification-store'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { opencodeClient } from '@/lib/opencode/client'; import { runtimeFetch } from '@/lib/runtime-fetch'; import { getRuntimeKey, subscribeRuntimeEndpointChanged } from '@/lib/runtime-switch'; import { useAutoReviewStore } from '@/stores/useAutoReviewStore'; import { resumeAutoReviewRun } from '@/lib/reviewFlow'; import { SyncProvider } from '@/sync/sync-context'; import { useSync } from '@/sync/use-sync'; import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay'; import { AboutDialog } from '@/components/ui/AboutDialog'; import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider'; import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { useUIStore } from '@/stores/useUIStore'; import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore'; import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore'; import type { RuntimeAPIs } from '@/lib/api/types'; import { TooltipProvider } from '@/components/ui/tooltip'; import { McpOAuthCallbackPage } from '@/components/sections/mcp/McpOAuthCallbackPage'; import { MCP_OAUTH_CALLBACK_PATH } from '@/components/sections/mcp/mcpOAuth'; import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery'; import { useI18n } from '@/lib/i18n'; import { applyMobileKeyboardMode } from '@/lib/mobileKeyboardMode'; import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat'; import { SyncAppEffects } from '@/apps/AppEffects'; import { resetAppForRuntimeEndpointChange } from '@/apps/runtimeEndpointReset'; import { useAppFontEffects } from '@/apps/useAppFontEffects'; import { OpenCodeUpdateToast } from '@/components/update/OpenCodeUpdateToast'; import { markStartupTrace, startupTraceEnabled } from '@/lib/startupTrace'; // Lazy-loaded heavy views — loaded on demand to reduce initial bundle size. const OnboardingScreen = lazyWithChunkRecovery(() => import('@/components/onboarding/OnboardingScreen').then((m) => ({ default: m.OnboardingScreen })), ); const AboutDialogWrapper: React.FC = () => { const isAboutDialogOpen = useUIStore((s) => s.isAboutDialogOpen); const setAboutDialogOpen = useUIStore((s) => s.setAboutDialogOpen); return ( ); }; const StartupInitializationRecovery: React.FC<{ onRetry: () => void; isRetrying: boolean; }> = ({ onRetry, isRetrying }) => { const { t } = useI18n(); return (

{t('startup.initRecovery.title')}

{t('startup.initRecovery.description')}

); }; type AppProps = { apis: RuntimeAPIs; }; type EmbeddedSessionChatConfig = { sessionId: string; directory: string | null; readOnly: boolean; }; type EmbeddedVisibilityPayload = { visible?: unknown; }; const normalizeEmbeddedDirectory = (value: string | null | undefined): string => { if (!value) return ''; return value.replace(/\\/g, '/').replace(/\/+$/g, ''); }; const readEmbeddedSessionChatConfig = (): EmbeddedSessionChatConfig | null => { if (typeof window === 'undefined' || !isEmbeddedSessionChat()) { return null; } const params = new URLSearchParams(window.location.search); const sessionIdRaw = params.get('sessionId'); const sessionId = typeof sessionIdRaw === 'string' ? sessionIdRaw.trim() : ''; if (!sessionId) { return null; } const directoryRaw = params.get('directory'); const directory = typeof directoryRaw === 'string' && directoryRaw.trim().length > 0 ? directoryRaw.trim() : null; return { sessionId, directory, readOnly: params.get('readOnly') === '1' || params.get('readOnly') === 'true', }; }; const isMcpOAuthCallbackPath = (): boolean => { if (typeof window === 'undefined') { return false; } return window.location.pathname === MCP_OAUTH_CALLBACK_PATH; }; const EmbeddedSessionChatContent: React.FC<{ embeddedSessionChat: EmbeddedSessionChatConfig; isVSCodeRuntime: boolean; embeddedBackgroundWorkEnabled: boolean; }> = ({ embeddedSessionChat, isVSCodeRuntime, embeddedBackgroundWorkEnabled }) => { const currentDirectory = useDirectoryStore((state) => state.currentDirectory); const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession); const sync = useSync(); const bootstrapKeyRef = React.useRef(null); const expectedDirectory = normalizeEmbeddedDirectory(embeddedSessionChat.directory); const activeDirectory = normalizeEmbeddedDirectory(currentDirectory); React.useEffect(() => { if (isVSCodeRuntime) return; if (expectedDirectory && activeDirectory !== expectedDirectory) return; const bootstrapKey = `${expectedDirectory}\n${embeddedSessionChat.sessionId}`; // Skip if this session was already bootstrapped and a session is still // active — allows in-place navigation (e.g. "Open subtask") to change // currentSessionId without this effect forcing it back. Only re-bootstrap // when currentSessionId was cleared (store init, draft, delete/archive, // runtime-switch remount). if (bootstrapKeyRef.current === bootstrapKey && currentSessionId) { return; } bootstrapKeyRef.current = bootstrapKey; setCurrentSession(embeddedSessionChat.sessionId, embeddedSessionChat.directory); void sync.ensureSessionRenderable(embeddedSessionChat.sessionId, true); }, [ activeDirectory, currentSessionId, embeddedSessionChat.directory, embeddedSessionChat.sessionId, expectedDirectory, isVSCodeRuntime, setCurrentSession, sync, ]); if (expectedDirectory && activeDirectory !== expectedDirectory) { return null; } return ( <> ); }; function App({ apis }: AppProps) { React.useEffect(() => { markStartupTrace('App:mounted'); if (startupTraceEnabled()) { console.info('[startup-trace] enabled. Run console.table(window.__OPENCHAMBER_STARTUP_TRACE__) after startup.'); } }, []); const initializeApp = useConfigStore((s) => s.initializeApp); const isInitialized = useConfigStore((s) => s.isInitialized); const isConnected = useConfigStore((s) => s.isConnected); const providersCount = useConfigStore((state) => state.providers.length); const agentsCount = useConfigStore((state) => state.agents.length); const loadProviders = useConfigStore((state) => state.loadProviders); const loadAgents = useConfigStore((state) => state.loadAgents); const error = useSessionUIStore((s) => s.error); const clearError = useSessionUIStore((s) => s.clearError); const currentDirectory = useDirectoryStore((state) => state.currentDirectory); const setDirectory = useDirectoryStore((state) => state.setDirectory); const isSwitchingDirectory = useDirectoryStore((state) => state.isSwitchingDirectory); const [showMemoryDebug, setShowMemoryDebug] = React.useState(false); const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus); const [isVSCodeRuntime, setIsVSCodeRuntime] = React.useState(() => apis.runtime.isVSCode); const [isEmbeddedVisible, setIsEmbeddedVisible] = React.useState(true); const [initRetryExhausted, setInitRetryExhausted] = React.useState(false); const [initRetryEpoch, setInitRetryEpoch] = React.useState(0); const [runtimeEndpointEpoch, setRuntimeEndpointEpoch] = React.useState(0); const [manualInitRetrying, setManualInitRetrying] = React.useState(false); const wideChatLayoutEnabled = useUIStore((state) => state.wideChatLayoutEnabled); const mobileKeyboardMode = useUIStore((state) => state.mobileKeyboardMode); const isDesktopRuntime = React.useMemo(() => isDesktopShell(), []); const setPlanModeEnabled = useFeatureFlagsStore((state) => state.setPlanModeEnabled); const [bootInjectionStatus, setBootInjectionStatus] = React.useState(() => { return getBootInjectionStatus(); }); const [bootView, setBootView] = React.useState(() => { const outcome = getInjectedBootOutcome(); return outcome !== null ? resolveDesktopBootView({ isDesktopShell: true, bootOutcome: outcome }) : null; }); const appReadyDispatchedRef = React.useRef(false); const embeddedSessionChat = React.useMemo(() => readEmbeddedSessionChatConfig(), []); const embeddedBackgroundWorkEnabled = !embeddedSessionChat || isEmbeddedVisible; const isMcpOAuthCallback = React.useMemo(() => isMcpOAuthCallbackPath(), []); React.useEffect(() => { setStreamPerfEnabled(showMemoryDebug); return () => { setStreamPerfEnabled(false); }; }, [showMemoryDebug]); React.useEffect(() => { applyMobileKeyboardMode(mobileKeyboardMode); }, [mobileKeyboardMode]); React.useEffect(() => { setIsVSCodeRuntime(apis.runtime.isVSCode); }, [apis.runtime.isVSCode]); React.useEffect(() => { return subscribeRuntimeEndpointChanged((detail) => { resetAppForRuntimeEndpointChange(detail); setRuntimeEndpointEpoch((epoch) => epoch + 1); setInitRetryExhausted(false); setInitRetryEpoch((epoch) => epoch + 1); }); }, []); const autoReviewResumeSignature = useAutoReviewStore((state) => { const runtimeKey = getRuntimeKey(); return Object.values(state.runsByOriginalSessionID) .filter((run) => run.status === 'running' && run.runtimeKey === runtimeKey) .map((run) => `${run.originalSessionID}:${run.phase}:${run.lastForwardedMessageID ?? ''}:${run.expectedAssistantParentID ?? ''}`) .sort() .join('|'); }); React.useEffect(() => { if (embeddedSessionChat) { return; } const runtimeKey = getRuntimeKey(); const runs = Object.values(useAutoReviewStore.getState().runsByOriginalSessionID) .filter((run) => run.status === 'running' && run.runtimeKey === runtimeKey); for (const run of runs) { resumeAutoReviewRun(run.originalSessionID); } }, [autoReviewResumeSignature, embeddedSessionChat, runtimeEndpointEpoch]); React.useEffect(() => { document.documentElement.classList.toggle('wide-chat-layout', wideChatLayoutEnabled); return () => { document.documentElement.classList.remove('wide-chat-layout'); }; }, [wideChatLayoutEnabled]); React.useEffect(() => { registerRuntimeAPIs(apis); return () => registerRuntimeAPIs(null); }, [apis]); React.useEffect(() => { if (embeddedSessionChat) { return; } void refreshGitHubAuthStatus(apis.github, { force: true }); }, [apis.github, embeddedSessionChat, refreshGitHubAuthStatus]); useAppFontEffects(); const bootOutcomeKnown = bootInjectionStatus === 'valid'; const bootViewIsMain = bootView?.screen === 'main'; // Splash dismissal: use the authoritative loading gate from desktopBoot. // Desktop shells strictly require a valid boot outcome before dismissing. // Non-main outcomes (chooser/recovery) can dismiss without waiting for init. React.useEffect(() => { if (!canDismissInitialLoading({ isDesktopShell: isDesktopRuntime, isInitialized, bootOutcomeKnown, bootViewIsMain, })) { return; } const timer = setTimeout(() => { const loadingElement = document.getElementById('initial-loading'); if (loadingElement) { loadingElement.classList.add('fade-out'); setTimeout(() => { loadingElement.remove(); }, 300); } }, 150); return () => clearTimeout(timer); }, [isDesktopRuntime, isInitialized, bootOutcomeKnown, bootViewIsMain]); // Deterministic malformed handling: update splash text so the user // sees a specific error instead of a generic spinner, but do NOT // dismiss the splash (that only happens on a valid outcome). React.useEffect(() => { if (!isDesktopRuntime || bootInjectionStatus !== 'malformed') { return; } const loadingElement = document.getElementById('initial-loading'); if (loadingElement) { loadingElement.textContent = 'Desktop startup failed — please restart the app.'; } }, [isDesktopRuntime, bootInjectionStatus]); // Non-desktop fallback: remove splash after 5 seconds even if init stalls. React.useEffect(() => { if (isDesktopRuntime) { return; } const fallbackTimer = setTimeout(() => { const loadingElement = document.getElementById('initial-loading'); if (loadingElement && !isInitialized) { loadingElement.classList.add('fade-out'); setTimeout(() => { loadingElement.remove(); }, 300); } }, 5000); return () => clearTimeout(fallbackTimer); }, [isDesktopRuntime, isInitialized]); React.useEffect(() => { let cancelled = false; const run = async () => { const res = await runtimeFetch('/health', { method: 'GET' }).catch(() => null); if (!res || !res.ok || cancelled) return; const data = (await res.json().catch(() => null)) as null | { planModeExperimentalEnabled?: unknown; }; if (!data || cancelled) return; const raw = data.planModeExperimentalEnabled; const enabled = raw === true || raw === 1 || raw === '1' || raw === 'true'; setPlanModeEnabled(enabled); }; void run(); return () => { cancelled = true; }; }, [setPlanModeEnabled]); React.useEffect(() => { // VS Code runtime bootstraps config + sessions after the managed OpenCode instance reports "connected". // Doing the default initialization here can race with startup and lead to one-shot failures. if (isVSCodeRuntime) { return; } void initializeApp(); }, [initializeApp, isVSCodeRuntime]); React.useEffect(() => { if (isVSCodeRuntime || isInitialized) return; let active = true; let retryTimer: ReturnType | undefined; let retryCount = 0; const MAX_RETRIES = 10; const BASE_DELAY_MS = 1000; const retryInitialization = async () => { if (!active) return; if (retryCount >= MAX_RETRIES) { setInitRetryExhausted(true); return; } const state = useConfigStore.getState(); if (state.isInitialized) { setInitRetryExhausted(false); return; } retryCount += 1; await state.initializeApp(); const next = useConfigStore.getState(); if (!active) return; if (next.isInitialized) { setInitRetryExhausted(false); return; } if (retryCount >= MAX_RETRIES) { setInitRetryExhausted(true); return; } const delay = Math.min(BASE_DELAY_MS * Math.pow(2, retryCount - 1), 16000); retryTimer = setTimeout(retryInitialization, delay); }; retryTimer = setTimeout(retryInitialization, BASE_DELAY_MS); return () => { active = false; if (retryTimer) clearTimeout(retryTimer); }; }, [initRetryEpoch, isInitialized, isVSCodeRuntime]); React.useEffect(() => { if (isInitialized) { setInitRetryExhausted(false); } }, [isInitialized]); React.useEffect(() => { if (!initRetryExhausted) return; const loadingElement = document.getElementById('initial-loading'); if (loadingElement) { loadingElement.classList.add('fade-out'); setTimeout(() => { loadingElement.remove(); }, 300); } }, [initRetryExhausted]); // Startup recovery: poll until providers AND agents are loaded. // loadProviders/loadAgents resolve normally even on failure (errors swallowed), // so a reactive effect can't detect failure — we need an interval. React.useEffect(() => { if (isVSCodeRuntime || !isConnected) return; if (providersCount > 0 && agentsCount > 0) return; let active = true; let retries = 0; const MAX_RETRIES = 15; const attempt = async () => { const state = useConfigStore.getState(); if (state.providers.length > 0 && state.agents.length > 0) return; try { if (state.providers.length === 0) await loadProviders({ source: 'startupRecovery' }); if (useConfigStore.getState().agents.length === 0) await loadAgents({ source: 'startupRecovery' }); } catch { /* retry next interval */ } }; void attempt(); const id = setInterval(() => { if (!active) return; if (++retries >= MAX_RETRIES) { clearInterval(id); return; } void attempt(); }, 2000); return () => { active = false; clearInterval(id); }; }, [isConnected, isVSCodeRuntime, loadAgents, loadProviders, providersCount, agentsCount]); React.useEffect(() => { if (isSwitchingDirectory) { return; } // VS Code runtime loads sessions via VSCodeLayout bootstrap to avoid startup races. if (isVSCodeRuntime) { return; } if (!isConnected) { return; } opencodeClient.setDirectory(currentDirectory); // Session loading is handled by the sync system's bootstrap — no manual loadSessions needed. }, [currentDirectory, isSwitchingDirectory, isConnected, isVSCodeRuntime]); React.useEffect(() => { if (!embeddedSessionChat || typeof window === 'undefined') { return; } const applyVisibility = (payload?: EmbeddedVisibilityPayload) => { const nextVisible = payload?.visible === true; setIsEmbeddedVisible(nextVisible); }; const handleMessage = (event: MessageEvent) => { if (event.origin !== window.location.origin) { return; } const data = event.data as { type?: unknown; payload?: EmbeddedVisibilityPayload }; if (data?.type !== 'openchamber:embedded-visibility') { return; } applyVisibility(data.payload); }; const scopedWindow = window as unknown as { __openchamberSetEmbeddedVisibility?: (payload?: EmbeddedVisibilityPayload) => void; }; scopedWindow.__openchamberSetEmbeddedVisibility = applyVisibility; window.addEventListener('message', handleMessage); return () => { window.removeEventListener('message', handleMessage); if (scopedWindow.__openchamberSetEmbeddedVisibility === applyVisibility) { delete scopedWindow.__openchamberSetEmbeddedVisibility; } }; }, [embeddedSessionChat]); React.useEffect(() => { if (!embeddedSessionChat?.directory || isVSCodeRuntime) { return; } if (currentDirectory === embeddedSessionChat.directory) { return; } setDirectory(embeddedSessionChat.directory, { showOverlay: false }); }, [currentDirectory, embeddedSessionChat, isVSCodeRuntime, setDirectory]); React.useEffect(() => { if (!embeddedSessionChat || typeof window === 'undefined') { return; } const handleStorage = (event: StorageEvent) => { if (event.storageArea !== window.localStorage) { return; } if (event.key !== 'ui-store') { return; } void useUIStore.persist.rehydrate(); }; window.addEventListener('storage', handleStorage); return () => { window.removeEventListener('storage', handleStorage); }; }, [embeddedSessionChat]); React.useEffect(() => { if (typeof window === 'undefined') return; const handler = (event: Event) => { const detail = (event as CustomEvent<{ sessionId?: string; directory?: string }>).detail; const sessionId = typeof detail?.sessionId === 'string' ? detail.sessionId.trim() : ''; if (!sessionId) return; const directory = typeof detail?.directory === 'string' && detail.directory.trim().length > 0 ? detail.directory.trim() : null; useUIStore.getState().setActiveMainTab('chat'); void useSessionUIStore.getState().setCurrentSession(sessionId, directory); }; window.addEventListener('openchamber:open-session', handler as EventListener); return () => window.removeEventListener('openchamber:open-session', handler as EventListener); }, []); // Open a draft Mini Chat window from the native File menu / tray. Uses a // dedicated single-fire event (not the menu-action channel) because draft // mini-chat windows are NOT deduplicated — a double dispatch would open two. React.useEffect(() => { if (typeof window === 'undefined') return; const onOpenMiniChat = () => { const currentDir = useDirectoryStore.getState().currentDirectory; const { activeProjectId, projects } = useProjectsStore.getState(); const activeProject = projects.find((p) => p.id === activeProjectId) ?? null; void invokeDesktop('desktop_open_draft_mini_chat_window', { directory: currentDir || activeProject?.path || '', projectId: activeProject?.id ?? null, }); }; window.addEventListener('openchamber:open-mini-chat', onOpenMiniChat); return () => window.removeEventListener('openchamber:open-mini-chat', onOpenMiniChat); }, []); // When the window regains focus, mark the currently-selected session as seen. // Turn-completes that arrive while the app is backgrounded are intentionally // left unseen (see isViewedInCurrentSession); coming back to the window is the // signal that the user has now looked at it, so the marker clears. React.useEffect(() => { if (typeof window === 'undefined') return; const onFocus = () => { const sessionId = useSessionUIStore.getState().currentSessionId; if (sessionId) markSessionViewed(sessionId); }; window.addEventListener('focus', onFocus); return () => window.removeEventListener('focus', onFocus); }, []); React.useEffect(() => { if (typeof window === 'undefined') return; const handler = (event: Event) => { const detail = (event as CustomEvent<{ directory?: string; projectId?: string }>).detail; const directory = typeof detail?.directory === 'string' && detail.directory.trim().length > 0 ? detail.directory.trim() : null; const projectId = typeof detail?.projectId === 'string' && detail.projectId.trim().length > 0 ? detail.projectId.trim() : null; useUIStore.getState().setActiveMainTab('chat'); useUIStore.getState().setSessionSwitcherOpen(false); useSessionUIStore.getState().openNewSessionDraft({ selectedProjectId: projectId, directoryOverride: directory, preserveDirectoryOverride: Boolean(directory), }); }; window.addEventListener('openchamber:open-draft-session', handler as EventListener); return () => window.removeEventListener('openchamber:open-draft-session', handler as EventListener); }, []); React.useEffect(() => { if (typeof window === 'undefined') return; if (!isInitialized || isSwitchingDirectory) return; if (appReadyDispatchedRef.current) return; appReadyDispatchedRef.current = true; (window as unknown as { __openchamberAppReady?: boolean }).__openchamberAppReady = true; window.dispatchEvent(new Event('openchamber:app-ready')); }, [isInitialized, isSwitchingDirectory]); // useEventStream replaced by SyncProvider + SyncBridge // Session attention now handled by notification-store via SSE events (session.idle/session.error) usePushVisibilityBeacon({ enabled: embeddedBackgroundWorkEnabled }); useWebNotificationStream({ enabled: embeddedBackgroundWorkEnabled }); usePwaInstallPrompt(); useWindowTitle(); useRouter(); const handleToggleMemoryDebug = React.useCallback(() => { setShowMemoryDebug(prev => !prev); }, []); useMenuActions(handleToggleMemoryDebug); useTraySync(); useSessionStatusBootstrap({ enabled: embeddedBackgroundWorkEnabled }); React.useEffect(() => { if (embeddedSessionChat) { return; } const handleKeyDown = (e: KeyboardEvent) => { const isDebugShortcut = hasModifier(e) && e.shiftKey && !e.altKey && (e.code === 'KeyD' || e.key.toLowerCase() === 'd'); if (isDebugShortcut) { e.preventDefault(); setShowMemoryDebug(prev => !prev); } }; window.addEventListener('keydown', handleKeyDown, true); return () => window.removeEventListener('keydown', handleKeyDown, true); }, [embeddedSessionChat]); React.useEffect(() => { if (embeddedSessionChat) { return; } if (error) { setTimeout(() => clearError(), 5000); } }, [clearError, embeddedSessionChat, error]); // Poll for the injected boot outcome until it becomes available (desktop only). // The Rust backend sets window.__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__ once the // sidecar reaches a stable state. We poll with exponential backoff to handle // potential race conditions during startup and config writes. React.useEffect(() => { if (!isDesktopRuntime || bootInjectionStatus !== 'not-injected') { return; } let cancelled = false; let attempts = 0; const BASE_INTERVAL = 200; const MAX_INTERVAL = 2000; const MAX_ATTEMPTS = 50; // 10 seconds total (200ms * 50 with exponential backoff cap) const pollWithBackoff = () => { if (cancelled) return; attempts++; const status = getBootInjectionStatus(); if (status !== 'not-injected') { cancelled = true; setBootInjectionStatus(status); if (status === 'valid') { const outcome = getInjectedBootOutcome(); if (outcome) { setBootView(resolveDesktopBootView({ isDesktopShell: true, bootOutcome: outcome })); } } // If status is 'malformed', we keep the splash visible with error text // handled by the separate useEffect below return; } // Exponential backoff with cap const nextInterval = Math.min(BASE_INTERVAL * Math.pow(1.1, attempts), MAX_INTERVAL); if (attempts >= MAX_ATTEMPTS) { // Max attempts reached - keep polling but show error const loadingElement = document.getElementById('initial-loading'); if (loadingElement && !loadingElement.textContent?.includes('taking longer')) { loadingElement.textContent = 'Desktop startup is taking longer than expected...'; } } window.setTimeout(pollWithBackoff, nextInterval); }; // Start polling window.setTimeout(pollWithBackoff, BASE_INTERVAL); return () => { cancelled = true; }; }, [isDesktopRuntime, bootInjectionStatus]); const handleDesktopBootDismiss = React.useCallback(async () => { if (shouldRestartDesktopBootFlow({ isDesktopShell: isDesktopShell(), isDesktopLocalOriginActive: isDesktopLocalOriginActive(), })) { await restartDesktopApp(); return; } window.location.reload(); }, []); const handleManualInitRetry = React.useCallback(async () => { if (manualInitRetrying) return; setInitRetryExhausted(false); setManualInitRetrying(true); try { await useConfigStore.getState().initializeApp(); } finally { setManualInitRetrying(false); } if (!useConfigStore.getState().isInitialized) { setInitRetryEpoch((value) => value + 1); } }, [manualInitRetrying]); // Map boot outcome kind to recovery variant const mapBootViewToRecoveryVariant = (view: DesktopBootView): RecoveryVariant | undefined => { if (view.screen === 'recovery') { return view.variant; } return undefined; }; // Desktop boot view routing. // When the boot outcome resolves to a non-main screen (chooser, recovery), // render OnboardingScreen with appropriate mode/variant. if (isDesktopRuntime && bootView && bootView.screen !== 'main') { // First-launch chooser if (bootView.screen === 'chooser') { return (
}> { // Switch to remote tab - handled internally by OnboardingScreen }} />
); } // Recovery screens const recoveryVariant = mapBootViewToRecoveryVariant(bootView); const hostUrl = bootView.screen === 'recovery' && 'url' in bootView ? bootView.url : undefined; return (
}>
); } if (embeddedSessionChat) { return (
); } if (isMcpOAuthCallback) { return ( ); } if (initRetryExhausted && !isInitialized && !isVSCodeRuntime && !embeddedSessionChat) { return ( { void handleManualInitRetry(); }} isRetrying={manualInitRetrying} /> ); } // Always mount the full provider tree to avoid remounts when isInitialized // flips from false → true. FireworksProvider is a lightweight shell; its // heavy children are only activated when actually needed. const isBootShell = !isInitialized && !isDesktopRuntime; return (
{!isBootShell && ( <> {showMemoryDebug && ( setShowMemoryDebug(false)} /> )} )}
); } export default App;