diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx
index 5b1891b3..351fa229 100644
--- a/packages/ui/src/App.tsx
+++ b/packages/ui/src/App.tsx
@@ -8,16 +8,11 @@ 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';
@@ -37,11 +32,6 @@ 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';
@@ -57,20 +47,14 @@ 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 { SyncAppEffects } from '@/apps/AppEffects';
+import { useAppFontEffects } from '@/apps/useAppFontEffects';
// 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 VSCodeLayoutLazy = lazyWithChunkRecovery(() =>
- import('@/components/layout/VSCodeLayout').then((m) => ({ default: m.VSCodeLayout })),
-);
-
-const AgentManagerViewLazy = lazyWithChunkRecovery(() =>
- import('@/components/views/agent-manager').then((m) => ({ default: m.AgentManagerView })),
-);
-
const AboutDialogWrapper: React.FC = () => {
const isAboutDialogOpen = useUIStore((s) => s.isAboutDialogOpen);
const setAboutDialogOpen = useUIStore((s) => s.setAboutDialogOpen);
@@ -178,35 +162,6 @@ const EmbeddedSessionSelectionGate: React.FC<{
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);
@@ -221,7 +176,6 @@ function App({ apis }: AppProps) {
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);
@@ -281,27 +235,7 @@ function App({ apis }: AppProps) {
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]);
+ useAppFontEffects();
const bootOutcomeKnown = bootInjectionStatus === 'valid';
const bootViewIsMain = bootView?.screen === 'main';
@@ -848,54 +782,6 @@ function App({ apis }: AppProps) {
);
}
- // 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.
diff --git a/packages/ui/src/apps/AppEffects.tsx b/packages/ui/src/apps/AppEffects.tsx
new file mode 100644
index 00000000..e0db7d61
--- /dev/null
+++ b/packages/ui/src/apps/AppEffects.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
+import { usePwaManifestSync } from '@/hooks/usePwaManifestSync';
+import { useQueuedMessageAutoSend } from '@/hooks/useQueuedMessageAutoSend';
+import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup';
+import { useWindowControlsOverlayLayout } from '@/hooks/useWindowControlsOverlayLayout';
+import { setOptimisticRefs } from '@/sync/session-actions';
+import { useSync } from '@/sync/use-sync';
+
+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;
+};
+
+export function SyncAppEffects({ embeddedBackgroundWorkEnabled }: {
+ embeddedBackgroundWorkEnabled: boolean;
+}) {
+ usePwaManifestSync();
+ useWindowControlsOverlayLayout();
+ useSessionAutoCleanup(embeddedBackgroundWorkEnabled);
+ useQueuedMessageAutoSend(embeddedBackgroundWorkEnabled);
+ useKeyboardShortcuts();
+
+ return ;
+}
diff --git a/packages/ui/src/apps/VSCodeApp.tsx b/packages/ui/src/apps/VSCodeApp.tsx
new file mode 100644
index 00000000..16b7e74e
--- /dev/null
+++ b/packages/ui/src/apps/VSCodeApp.tsx
@@ -0,0 +1,134 @@
+import React from 'react';
+import { AgentManagerView } from '@/components/views/agent-manager';
+import { FireworksProvider } from '@/contexts/FireworksContext';
+import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider';
+import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
+import { TooltipProvider } from '@/components/ui/tooltip';
+import { Toaster } from '@/components/ui/sonner';
+import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
+import { VSCodeLayout } from '@/components/layout/VSCodeLayout';
+import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
+import { useRouter } from '@/hooks/useRouter';
+import { useWindowTitle } from '@/hooks/useWindowTitle';
+import { opencodeClient } from '@/lib/opencode/client';
+import type { RuntimeAPIs } from '@/lib/api/types';
+import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
+import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
+import { useDirectoryStore } from '@/stores/useDirectoryStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { useSessionUIStore } from '@/sync/session-ui-store';
+import { SyncProvider } from '@/sync/sync-context';
+import { SyncAppEffects } from './AppEffects';
+import { useAppFontEffects } from './useAppFontEffects';
+
+type VSCodePanelType = 'chat' | 'agentManager';
+
+declare global {
+ interface Window {
+ __OPENCHAMBER_PANEL_TYPE__?: VSCodePanelType;
+ }
+}
+
+type VSCodeAppProps = {
+ apis: RuntimeAPIs;
+};
+
+export function VSCodeApp({ apis }: VSCodeAppProps) {
+ const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
+ const error = useSessionUIStore((state) => state.error);
+ const clearError = useSessionUIStore((state) => state.clearError);
+ const wideChatLayoutEnabled = useUIStore((state) => state.wideChatLayoutEnabled);
+ const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus);
+ const setPlanModeEnabled = useFeatureFlagsStore((state) => state.setPlanModeEnabled);
+ const panelType = typeof window !== 'undefined'
+ ? window.__OPENCHAMBER_PANEL_TYPE__
+ : 'chat';
+
+ React.useEffect(() => {
+ registerRuntimeAPIs(apis);
+ return () => registerRuntimeAPIs(null);
+ }, [apis]);
+
+ useAppFontEffects();
+ usePushVisibilityBeacon({ enabled: true });
+ useWindowTitle();
+ useRouter();
+
+ React.useEffect(() => {
+ document.documentElement.classList.toggle('wide-chat-layout', wideChatLayoutEnabled);
+ return () => {
+ document.documentElement.classList.remove('wide-chat-layout');
+ };
+ }, [wideChatLayoutEnabled]);
+
+ React.useEffect(() => {
+ void refreshGitHubAuthStatus(apis.github, { force: true });
+ }, [apis.github, refreshGitHubAuthStatus]);
+
+ 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(() => {
+ if (!error) {
+ return;
+ }
+
+ const timeout = window.setTimeout(() => clearError(), 5000);
+ return () => window.clearTimeout(timeout);
+ }, [clearError, error]);
+
+ if (panelType === 'agentManager') {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/ui/src/apps/renderVSCodeApp.tsx b/packages/ui/src/apps/renderVSCodeApp.tsx
new file mode 100644
index 00000000..bcec6c2e
--- /dev/null
+++ b/packages/ui/src/apps/renderVSCodeApp.tsx
@@ -0,0 +1,58 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import '@/styles/fonts';
+import '@/index.css';
+import '@/lib/debug';
+import { SessionAuthGate } from '@/components/auth/SessionAuthGate';
+import { ThemeProvider } from '@/components/providers/ThemeProvider';
+import { ThemeSystemProvider } from '@/contexts/ThemeSystemContext';
+import type { RuntimeAPIs } from '@/lib/api/types';
+import { startAppearanceAutoSave } from '@/lib/appearanceAutoSave';
+import { applyPersistedDirectoryPreferences } from '@/lib/directoryPersistence';
+import { initializeLocale, I18nProvider } from '@/lib/i18n';
+import { initializeAppearancePreferences, syncDesktopSettings } from '@/lib/persistence';
+import { startModelPrefsAutoSave } from '@/lib/modelPrefsAutoSave';
+import { startTypographyWatcher } from '@/lib/typographyWatcher';
+import { VSCodeApp } from './VSCodeApp';
+
+const initializeSharedPreferences = () => {
+ initializeLocale();
+
+ void initializeAppearancePreferences().then(() => {
+ void Promise.all([
+ syncDesktopSettings(),
+ applyPersistedDirectoryPreferences(),
+ ]).catch((err) => {
+ console.error('[vscode-main] settings init failed:', err);
+ });
+
+ startAppearanceAutoSave();
+ startModelPrefsAutoSave();
+ startTypographyWatcher();
+ }).catch((err) => {
+ console.error('[vscode-main] appearance init failed:', err);
+ });
+};
+
+export function renderVSCodeApp(apis: RuntimeAPIs) {
+ initializeSharedPreferences();
+
+ const rootElement = document.getElementById('root');
+ if (!rootElement) {
+ throw new Error('Root element not found');
+ }
+
+ createRoot(rootElement).render(
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+}
diff --git a/packages/ui/src/apps/useAppFontEffects.ts b/packages/ui/src/apps/useAppFontEffects.ts
new file mode 100644
index 00000000..61e1194e
--- /dev/null
+++ b/packages/ui/src/apps/useAppFontEffects.ts
@@ -0,0 +1,31 @@
+import React from 'react';
+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';
+
+export function useAppFontEffects() {
+ const { uiFont, monoFont } = useFontPreferences();
+
+ 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]);
+}
diff --git a/packages/vscode/webview/main.tsx b/packages/vscode/webview/main.tsx
index 8a121bb1..7be6ee43 100644
--- a/packages/vscode/webview/main.tsx
+++ b/packages/vscode/webview/main.tsx
@@ -60,7 +60,7 @@ const bootstrapConnectionStatus = () => {
bootstrapConnectionStatus();
-// Expose panel type globally for App.tsx to conditionally render
+// Expose panel type globally for the VS Code app root to conditionally render.
window.__OPENCHAMBER_PANEL_TYPE__ = (window.__VSCODE_CONFIG__?.panelType as PanelType) || 'chat';
const handleConnectionMessage = (event: MessageEvent) => {
@@ -1242,8 +1242,9 @@ onCommand('activeEditorFile', (payload) => {
});
});
-import('@/main')
- .then(async () => {
+import('@openchamber/ui/apps/renderVSCodeApp')
+ .then(async ({ renderVSCodeApp }) => {
+ renderVSCodeApp(window.__OPENCHAMBER_RUNTIME_APIS__ ?? createVSCodeAPIs());
await waitForUiMount();
uiMounted = true;
maybeHideLoadingOverlay();