import React from 'react'; import { MainLayout } from '@/components/layout/MainLayout'; import { VSCodeLayout } from '@/components/layout/VSCodeLayout'; import { AgentManagerView } from '@/components/views/agent-manager'; import { ChatView } from '@/components/views'; 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 { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'; import { useMenuActions } from '@/hooks/useMenuActions'; import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap'; import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup'; import { useQueuedMessageAutoSend } from '@/hooks/useQueuedMessageAutoSend'; import { useRouter } from '@/hooks/useRouter'; import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon'; import { usePwaManifestSync } from '@/hooks/usePwaManifestSync'; import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt'; import { useWindowControlsOverlayLayout } from '@/hooks/useWindowControlsOverlayLayout'; import { useWindowTitle } from '@/hooks/useWindowTitle'; import { useConfigStore } from '@/stores/useConfigStore'; import { hasModifier } from '@/lib/utils'; import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell, restartDesktopApp } 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 { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { opencodeClient } from '@/lib/opencode/client'; import { SyncProvider, useSessions } from '@/sync/sync-context'; import { useSync } from '@/sync/use-sync'; import { setOptimisticRefs } from '@/sync/session-actions'; import { useFontPreferences } from '@/hooks/useFontPreferences'; import { CODE_FONT_OPTION_MAP, DEFAULT_MONO_FONT, DEFAULT_UI_FONT, UI_FONT_OPTION_MAP } from '@/lib/fontOptions'; import { loadMonoFont, loadUiFont } from '@/lib/fontLoader'; import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay'; import { AboutDialog } from '@/components/ui/AboutDialog'; import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider'; import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { VoiceProvider } from '@/components/voice'; 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'; // 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; }; type EmbeddedVisibilityPayload = { visible?: unknown; }; const readEmbeddedSessionChatConfig = (): EmbeddedSessionChatConfig | null => { if (typeof window === 'undefined') { return null; } const params = new URLSearchParams(window.location.search); if (params.get('ocPanel') !== 'session-chat') { return null; } 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, }; }; const isMcpOAuthCallbackPath = (): boolean => { if (typeof window === 'undefined') { return false; } return window.location.pathname === MCP_OAUTH_CALLBACK_PATH; }; const EmbeddedSessionSelectionGate: React.FC<{ embeddedSessionChat: EmbeddedSessionChatConfig | null; isVSCodeRuntime: boolean; }> = ({ embeddedSessionChat, isVSCodeRuntime }) => { const sessions = useSessions(); const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession); React.useEffect(() => { if (!embeddedSessionChat || isVSCodeRuntime) { return; } if (currentSessionId === embeddedSessionChat.sessionId) { return; } if (!sessions.some((session) => session.id === embeddedSessionChat.sessionId)) { return; } void setCurrentSession(embeddedSessionChat.sessionId); }, [currentSessionId, embeddedSessionChat, isVSCodeRuntime, sessions, setCurrentSession]); return null; }; const SyncOptimisticBridge: React.FC = () => { const sync = useSync(); const addRef = React.useRef(sync.optimistic.add); const removeRef = React.useRef(sync.optimistic.remove); addRef.current = sync.optimistic.add; removeRef.current = sync.optimistic.remove; React.useEffect(() => { setOptimisticRefs( (input) => addRef.current(input), (input) => removeRef.current(input), ); }, []); return null; }; function SyncAppEffects({ embeddedBackgroundWorkEnabled }: { embeddedBackgroundWorkEnabled: boolean; }) { usePwaManifestSync(); useWindowControlsOverlayLayout(); useSessionAutoCleanup(embeddedBackgroundWorkEnabled); useQueuedMessageAutoSend(embeddedBackgroundWorkEnabled); useKeyboardShortcuts(); return ; } function App({ apis }: AppProps) { 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 { uiFont, monoFont } = useFontPreferences(); 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 [manualInitRetrying, setManualInitRetrying] = React.useState(false); const wideChatLayoutEnabled = useUIStore((state) => state.wideChatLayoutEnabled); 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 initializationInFlightRef = 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(() => { setIsVSCodeRuntime(apis.runtime.isVSCode); }, [apis.runtime.isVSCode]); 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]); React.useEffect(() => { if (typeof document === 'undefined') { return; } const root = document.documentElement; const uiStack = UI_FONT_OPTION_MAP[uiFont]?.stack ?? UI_FONT_OPTION_MAP[DEFAULT_UI_FONT].stack; const monoStack = CODE_FONT_OPTION_MAP[monoFont]?.stack ?? CODE_FONT_OPTION_MAP[DEFAULT_MONO_FONT].stack; void loadUiFont(uiFont); void loadMonoFont(monoFont); root.style.setProperty('--font-sans', uiStack); root.style.setProperty('--font-heading', uiStack); root.style.setProperty('--font-family-sans', uiStack); root.style.setProperty('--font-mono', monoStack); root.style.setProperty('--font-family-mono', monoStack); root.style.setProperty('--ui-regular-font-weight', '400'); if (document.body) { document.body.style.fontFamily = uiStack; } }, [uiFont, monoFont]); 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 fetch('/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(() => { const init = async () => { // 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; } if (initializationInFlightRef.current) { return; } initializationInFlightRef.current = true; try { await initializeApp(); } finally { initializationInFlightRef.current = false; } }; init(); }, [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; } if (initializationInFlightRef.current) { retryTimer = setTimeout(retryInitialization, BASE_DELAY_MS); return; } retryCount += 1; initializationInFlightRef.current = true; try { await state.initializeApp(); } finally { initializationInFlightRef.current = false; } 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(); if (useConfigStore.getState().agents.length === 0) await loadAgents(); } 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 }>).detail; const sessionId = typeof detail?.sessionId === 'string' ? detail.sessionId.trim() : ''; if (!sessionId) return; void useSessionUIStore.getState().setCurrentSession(sessionId); }; window.addEventListener('openchamber:open-session', handler as EventListener); return () => window.removeEventListener('openchamber:open-session', handler as EventListener); }, []); React.useEffect(() => { if (typeof window === 'undefined') return; const handler = (event: Event) => { const detail = (event as CustomEvent<{ projectPath?: string }>).detail; const projectPath = typeof detail?.projectPath === 'string' ? detail.projectPath.trim() : ''; if (!projectPath) return; const projectsStore = useProjectsStore.getState(); const existing = projectsStore.projects.find((project) => project.path === projectPath); if (existing) { projectsStore.setActiveProject(existing.id); } else { projectsStore.addProject(projectPath); } }; window.addEventListener('openchamber:open-project', handler as EventListener); return () => window.removeEventListener('openchamber:open-project', 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 }); usePwaInstallPrompt(); useWindowTitle(); useRouter(); const handleToggleMemoryDebug = React.useCallback(() => { setShowMemoryDebug(prev => !prev); }, []); useMenuActions(handleToggleMemoryDebug); 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({ isTauriShell: isTauriShell(), isDesktopLocalOriginActive: isDesktopLocalOriginActive(), })) { await restartDesktopApp(); return; } window.location.reload(); }, []); const handleManualInitRetry = React.useCallback(async () => { if (manualInitRetrying || initializationInFlightRef.current) return; setInitRetryExhausted(false); setManualInitRetrying(true); initializationInFlightRef.current = true; try { await useConfigStore.getState().initializeApp(); } finally { initializationInFlightRef.current = false; 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} /> ); } // VS Code runtime - simplified layout without git/terminal views if (isVSCodeRuntime) { // Check if this is the Agent Manager panel const panelType = typeof window !== 'undefined' ? (window as { __OPENCHAMBER_PANEL_TYPE__?: 'chat' | 'agentManager' }).__OPENCHAMBER_PANEL_TYPE__ : 'chat'; if (panelType === 'agentManager') { return (
); } return (
); } // Always mount the full provider tree to avoid remounts when isInitialized // flips from false → true. FireworksProvider and VoiceProvider are lightweight // shells; their heavy children are only activated when actually needed. const isBootShell = !isInitialized && !isDesktopRuntime; return (
{!isBootShell && ( <> {showMemoryDebug && ( setShowMemoryDebug(false)} /> )} )}
); } export default App;