diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 6f7cca4d..5b1891b3 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -1,8 +1,6 @@ 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 { ChatView } from '@/components/views/ChatView'; import { FireworksProvider } from '@/contexts/FireworksContext'; import { Toaster } from '@/components/ui/sonner'; import { Button } from '@/components/ui/button'; @@ -65,6 +63,14 @@ 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); @@ -236,7 +242,6 @@ function App({ apis }: AppProps) { : 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(), []); @@ -383,24 +388,12 @@ function App({ apis }: AppProps) { }, [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(); + // 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(() => { @@ -423,18 +416,8 @@ function App({ apis }: AppProps) { 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; - } + await state.initializeApp(); const next = useConfigStore.getState(); if (!active) return; @@ -760,15 +743,13 @@ function App({ apis }: AppProps) { }, []); const handleManualInitRetry = React.useCallback(async () => { - if (manualInitRetrying || initializationInFlightRef.current) return; + if (manualInitRetrying) return; setInitRetryExhausted(false); setManualInitRetrying(true); - initializationInFlightRef.current = true; try { await useConfigStore.getState().initializeApp(); } finally { - initializationInFlightRef.current = false; setManualInitRetrying(false); } @@ -882,7 +863,9 @@ function App({ apis }: AppProps) {
- + }> + +
@@ -900,7 +883,9 @@ function App({ apis }: AppProps) {
- + }> + +
diff --git a/packages/ui/src/components/layout/ContextPanel.tsx b/packages/ui/src/components/layout/ContextPanel.tsx index d827742a..def9bbf2 100644 --- a/packages/ui/src/components/layout/ContextPanel.tsx +++ b/packages/ui/src/components/layout/ContextPanel.tsx @@ -4,7 +4,9 @@ import { RiArrowLeftRightLine, RiChat4Line, RiCloseLine, RiDonutChartFill, RiFil import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; import { Button } from '@/components/ui/button'; import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip'; -import { DiffView, FilesView, PlanView } from '@/components/views'; +import { DiffView } from '@/components/views/DiffView'; +import { FilesView } from '@/components/views/FilesView'; +import { PlanView } from '@/components/views/PlanView'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { openExternalUrl } from '@/lib/url'; import { copyTextToClipboard } from '@/lib/clipboard'; diff --git a/packages/ui/src/components/layout/MainLayout.tsx b/packages/ui/src/components/layout/MainLayout.tsx index 23fb8eee..5fdb96b9 100644 --- a/packages/ui/src/components/layout/MainLayout.tsx +++ b/packages/ui/src/components/layout/MainLayout.tsx @@ -25,7 +25,7 @@ import { cn } from '@/lib/utils'; import { isDesktopShell } from '@/lib/desktop'; import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery'; -import { ChatView } from '@/components/views'; +import { ChatView } from '@/components/views/ChatView'; // Heavy views loaded on-demand to reduce initial bundle parse time. const PlanView = lazyWithChunkRecovery(() => import('@/components/views/PlanView').then(m => ({ default: m.PlanView }))); diff --git a/packages/ui/src/components/layout/RightSidebarTabs.tsx b/packages/ui/src/components/layout/RightSidebarTabs.tsx index 46052317..e2c270e5 100644 --- a/packages/ui/src/components/layout/RightSidebarTabs.tsx +++ b/packages/ui/src/components/layout/RightSidebarTabs.tsx @@ -3,7 +3,7 @@ import { RiBookletLine, RiFolder3Line, RiGitBranchLine } from '@remixicon/react' import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip'; import { ProjectNotesTodoPanel } from '@/components/session/ProjectNotesTodoPanel'; -import { GitView } from '@/components/views'; +import { GitView } from '@/components/views/GitView'; import { useGitStore } from '@/stores/useGitStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; diff --git a/packages/ui/src/components/layout/VSCodeLayout.tsx b/packages/ui/src/components/layout/VSCodeLayout.tsx index 8b643405..8cc41475 100644 --- a/packages/ui/src/components/layout/VSCodeLayout.tsx +++ b/packages/ui/src/components/layout/VSCodeLayout.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { ErrorBoundary } from '../ui/ErrorBoundary'; import { SessionSidebar } from '@/components/session/SessionSidebar'; -import { ChatView } from '@/components/views'; +import { ChatView } from '@/components/views/ChatView'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useViewportStore } from '@/sync/viewport-store'; import { useSessions, useDirectorySync, useSessionMessages, useSessionMessagesResolved } from '@/sync/sync-context'; diff --git a/packages/ui/src/components/views/index.ts b/packages/ui/src/components/views/index.ts index 16ae2517..7cf76564 100644 --- a/packages/ui/src/components/views/index.ts +++ b/packages/ui/src/components/views/index.ts @@ -1,9 +1,2 @@ +/** Entry-level views only. Avoid barrel re-exports that pull heavy modules into the main chunk. */ export { ChatView } from './ChatView'; -export { PlanView } from './PlanView'; -export { GitView } from './GitView'; -export { DiffView, useDiffFileCount } from './DiffView'; -export { TerminalView } from './TerminalView'; -export { FilesView } from './FilesView'; -export { SettingsView } from './SettingsView'; -export { SettingsWindow } from './SettingsWindow'; -export { MultiRunWindow } from './MultiRunWindow'; diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts index 14865b64..f65e1d9b 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -583,6 +583,7 @@ declare global { // In-flight dedup: prevent concurrent duplicate loadProviders/loadAgents calls for the same directory const _inFlightProviders = new Map>(); const _inFlightAgents = new Map>(); +let _initializeAppInFlight: Promise | null = null; export const useConfigStore = create()( devtools( @@ -1965,43 +1966,54 @@ export const useConfigStore = create()( }, initializeApp: async () => { - try { - const debug = streamDebugEnabled(); - if (debug) console.log("Starting app initialization..."); + if (_initializeAppInFlight) { + return _initializeAppInFlight; + } - const isConnected = await get().checkConnection(); - if (debug) console.log("Connection check result:", isConnected); + const run = (async () => { + try { + const debug = streamDebugEnabled(); + if (debug) console.log("Starting app initialization..."); - if (!isConnected) { - if (debug) console.log("Server not connected"); - // checkConnection already set lastDisconnectReason; do not overwrite. + const isConnected = await get().checkConnection(); + if (debug) console.log("Connection check result:", isConnected); + + if (!isConnected) { + if (debug) console.log("Server not connected"); + // checkConnection already set lastDisconnectReason; do not overwrite. + set({ + isConnected: false, + connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting", + }); + return; + } + + if (debug) console.log("Initializing app..."); + await opencodeClient.initApp(); + + if (debug) console.log("Loading providers..."); + await get().loadProviders(); + + if (debug) console.log("Loading agents..."); + await get().loadAgents(); + + set({ isInitialized: true, isConnected: true, hasEverConnected: true, connectionPhase: "connected" }); + if (debug) console.log("App initialized successfully"); + } catch (error) { + console.error("Failed to initialize app:", error); set({ + isInitialized: false, isConnected: false, connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting", + lastDisconnectReason: 'init_error', }); - return; } + })().finally(() => { + _initializeAppInFlight = null; + }); - if (debug) console.log("Initializing app..."); - await opencodeClient.initApp(); - - if (debug) console.log("Loading providers..."); - await get().loadProviders(); - - if (debug) console.log("Loading agents..."); - await get().loadAgents(); - - set({ isInitialized: true, isConnected: true, hasEverConnected: true, connectionPhase: "connected" }); - if (debug) console.log("App initialized successfully"); - } catch (error) { - console.error("Failed to initialize app:", error); - set({ - isInitialized: false, - isConnected: false, - connectionPhase: get().hasEverConnected ? "reconnecting" : "connecting", - lastDisconnectReason: 'init_error', - }); - } + _initializeAppInFlight = run; + return run; }, getCurrentProvider: () => {