Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 132 additions & 72 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Menu } from "lucide-react";
import { useEffect, useCallback, useRef } from "react";
import React, { useCallback, useEffect, useRef } from "react";
import "./styles/globals.css";
import { useWorkspaceContext, toWorkspaceSelection } from "./contexts/WorkspaceContext";
import { useProjectContext } from "./contexts/ProjectContext";
import type { WorkspaceSelection } from "./components/ProjectSidebar";
import { LeftSidebar } from "./components/LeftSidebar";
import { ProjectCreateModal } from "./components/ProjectCreateModal";
import { AIView } from "./components/AIView";
import { ErrorBoundary } from "./components/ErrorBoundary";
import {
usePersistedState,
Expand All @@ -30,7 +28,6 @@ import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandR
import { useOpenTerminal } from "./hooks/useOpenTerminal";
import type { CommandAction } from "./contexts/CommandRegistryContext";
import { useTheme, type ThemeMode } from "./contexts/ThemeContext";
import { CommandPalette } from "./components/CommandPalette";
import { buildCoreSources, type BuildSourcesParams } from "./utils/commands/sources";

import { THINKING_LEVELS, type ThinkingLevel } from "@/common/types/thinking";
Expand All @@ -54,10 +51,8 @@ import { useStartWorkspaceCreation, getFirstProjectPath } from "./hooks/useStart
import { useAPI } from "@/browser/contexts/API";
import { AuthTokenModal } from "@/browser/components/AuthTokenModal";
import { Button } from "./components/ui/button";
import { ProjectPage } from "@/browser/components/ProjectPage";

import { SettingsProvider, useSettings } from "./contexts/SettingsContext";
import { SettingsModal } from "./components/Settings/SettingsModal";
import { SplashScreenProvider } from "./components/splashScreens/SplashScreenProvider";
import { TutorialProvider } from "./contexts/TutorialContext";
import { TooltipProvider } from "./components/ui/tooltip";
Expand All @@ -67,8 +62,57 @@ import { FeatureFlagsProvider } from "./contexts/FeatureFlagsContext";
import { ExperimentsProvider } from "./contexts/ExperimentsContext";
import { getWorkspaceSidebarKey } from "./utils/workspace";
import { WindowsToolchainBanner } from "./components/WindowsToolchainBanner";
import { isStorybook } from "@/browser/utils/storybook";
import { RosettaBanner } from "./components/RosettaBanner";

// Large, rarely-used screens/modals are lazily loaded to keep the initial renderer
// bundle small. These chunks are only requested when the UI is actually opened.
//
// In Storybook/Chromatic, proactively start loading these chunks so snapshots stabilize
// (React.lazy suspends while the chunk loads).
const aiViewImport = isStorybook()
? import("./components/AIView").then((m) => ({ default: m.AIView }))
: null;
const AIView = React.lazy(
() => aiViewImport ?? import("./components/AIView").then((m) => ({ default: m.AIView }))
);

const projectPageImport = isStorybook()
? import("./components/ProjectPage").then((m) => ({ default: m.ProjectPage }))
: null;
const ProjectPage = React.lazy(
() =>
projectPageImport ??
import("./components/ProjectPage").then((m) => ({ default: m.ProjectPage }))
);

const commandPaletteImport = isStorybook()
? import("./components/CommandPalette").then((m) => ({ default: m.CommandPalette }))
: null;
const CommandPalette = React.lazy(
() =>
commandPaletteImport ??
import("./components/CommandPalette").then((m) => ({ default: m.CommandPalette }))
);

const projectCreateModalImport = isStorybook()
? import("./components/ProjectCreateModal").then((m) => ({ default: m.ProjectCreateModal }))
: null;
const ProjectCreateModal = React.lazy(
() =>
projectCreateModalImport ??
import("./components/ProjectCreateModal").then((m) => ({ default: m.ProjectCreateModal }))
);

const settingsModalImport = isStorybook()
? import("./components/Settings/SettingsModal").then((m) => ({ default: m.SettingsModal }))
: null;
const SettingsModal = React.lazy(
() =>
settingsModalImport ??
import("./components/Settings/SettingsModal").then((m) => ({ default: m.SettingsModal }))
);

function AppInner() {
// Get workspace state from context
const {
Expand Down Expand Up @@ -763,18 +807,20 @@ function AppInner() {
<ErrorBoundary
workspaceInfo={`${selectedWorkspace.projectName}/${workspaceName}`}
>
<AIView
workspaceId={selectedWorkspace.workspaceId}
projectPath={selectedWorkspace.projectPath}
projectName={selectedWorkspace.projectName}
leftSidebarCollapsed={sidebarCollapsed}
onToggleLeftSidebarCollapsed={handleToggleSidebar}
workspaceName={workspaceName}
namedWorkspacePath={workspacePath}
runtimeConfig={currentMetadata.runtimeConfig}
incompatibleRuntime={currentMetadata.incompatibleRuntime}
status={currentMetadata.status}
/>
<React.Suspense fallback={null}>
<AIView
workspaceId={selectedWorkspace.workspaceId}
projectPath={selectedWorkspace.projectPath}
projectName={selectedWorkspace.projectName}
leftSidebarCollapsed={sidebarCollapsed}
onToggleLeftSidebarCollapsed={handleToggleSidebar}
workspaceName={workspaceName}
namedWorkspacePath={workspacePath}
runtimeConfig={currentMetadata.runtimeConfig}
incompatibleRuntime={currentMetadata.incompatibleRuntime}
status={currentMetadata.status}
/>
</React.Suspense>
</ErrorBoundary>
);
})()
Expand All @@ -784,44 +830,46 @@ function AppInner() {
const projectName =
projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "Project";
return (
<ProjectPage
projectPath={projectPath}
projectName={projectName}
leftSidebarCollapsed={sidebarCollapsed}
onToggleLeftSidebarCollapsed={handleToggleSidebar}
pendingSectionId={pendingNewWorkspaceSectionId}
onProviderConfig={handleProviderConfig}
onWorkspaceCreated={(metadata) => {
// IMPORTANT: Add workspace to store FIRST (synchronous) to ensure
// the store knows about it before React processes the state updates.
// This prevents race conditions where the UI tries to access the
// workspace before the store has created its aggregator.
workspaceStore.addWorkspace(metadata);

// Add to workspace metadata map (triggers React state update)
setWorkspaceMetadata((prev) => new Map(prev).set(metadata.id, metadata));

// Only switch to new workspace if user hasn't selected another one
// during the creation process (selectedWorkspace was null when creation started)
setSelectedWorkspace((current) => {
if (current !== null) {
// User has already selected another workspace - don't override
return current;
}
return toWorkspaceSelection(metadata);
});

// Track telemetry
telemetry.workspaceCreated(
metadata.id,
getRuntimeTypeForTelemetry(metadata.runtimeConfig)
);

// Note: No need to call clearPendingWorkspaceCreation() here.
// Navigating to the workspace URL automatically clears the pending
// state since pendingNewWorkspaceProject is derived from the URL.
}}
/>
<React.Suspense fallback={null}>
<ProjectPage
projectPath={projectPath}
projectName={projectName}
leftSidebarCollapsed={sidebarCollapsed}
onToggleLeftSidebarCollapsed={handleToggleSidebar}
pendingSectionId={pendingNewWorkspaceSectionId}
onProviderConfig={handleProviderConfig}
onWorkspaceCreated={(metadata) => {
// IMPORTANT: Add workspace to store FIRST (synchronous) to ensure
// the store knows about it before React processes the state updates.
// This prevents race conditions where the UI tries to access the
// workspace before the store has created its aggregator.
workspaceStore.addWorkspace(metadata);

// Add to workspace metadata map (triggers React state update)
setWorkspaceMetadata((prev) => new Map(prev).set(metadata.id, metadata));

// Only switch to new workspace if user hasn't selected another one
// during the creation process (selectedWorkspace was null when creation started)
setSelectedWorkspace((current) => {
if (current !== null) {
// User has already selected another workspace - don't override
return current;
}
return toWorkspaceSelection(metadata);
});

// Track telemetry
telemetry.workspaceCreated(
metadata.id,
getRuntimeTypeForTelemetry(metadata.runtimeConfig)
);

// Note: No need to call clearPendingWorkspaceCreation() here.
// Navigating to the workspace URL automatically clears the pending
// state since pendingNewWorkspaceProject is derived from the URL.
}}
/>
</React.Suspense>
);
})()
) : (
Expand Down Expand Up @@ -856,22 +904,34 @@ function AppInner() {
)}
</div>
</div>
<CommandPalette
getSlashContext={() => ({
providerNames: [],
workspaceId: selectedWorkspace?.workspaceId,
})}
/>
<ProjectCreateModal
isOpen={isProjectCreateModalOpen}
onClose={closeProjectCreateModal}
onSuccess={(normalizedPath, projectConfig) => {
addProject(normalizedPath, projectConfig);
updatePersistedState(getAgentsInitNudgeKey(normalizedPath), true);
beginWorkspaceCreation(normalizedPath);
}}
/>
<SettingsModal />
{isCommandPaletteOpen && (
<React.Suspense fallback={null}>
<CommandPalette
getSlashContext={() => ({
providerNames: [],
workspaceId: selectedWorkspace?.workspaceId,
})}
/>
</React.Suspense>
)}
{isProjectCreateModalOpen && (
<React.Suspense fallback={null}>
<ProjectCreateModal
isOpen={isProjectCreateModalOpen}
onClose={closeProjectCreateModal}
onSuccess={(normalizedPath, projectConfig) => {
addProject(normalizedPath, projectConfig);
updatePersistedState(getAgentsInitNudgeKey(normalizedPath), true);
beginWorkspaceCreation(normalizedPath);
}}
/>
</React.Suspense>
)}
{isSettingsOpen && (
<React.Suspense fallback={null}>
<SettingsModal />
</React.Suspense>
)}
</div>
</>
);
Expand Down
10 changes: 7 additions & 3 deletions src/browser/components/AppLoader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useState, useEffect } from "react";
import App from "../App";
import React, { useEffect, useState } from "react";
import { AuthTokenModal } from "./AuthTokenModal";
import { ThemeProvider } from "../contexts/ThemeContext";
import { LoadingScreen } from "./LoadingScreen";
Expand All @@ -14,6 +13,9 @@ import { RouterProvider } from "../contexts/RouterContext";
import { TelemetryEnabledProvider } from "../contexts/TelemetryEnabledContext";
import { TerminalRouterProvider } from "../terminal/TerminalRouterContext";

const appImport = import("../App");
const App = React.lazy(() => appImport);

interface AppLoaderProps {
/** Optional pre-created ORPC api?. If provided, skips internal connection setup. */
client?: APIClient;
Expand Down Expand Up @@ -110,7 +112,9 @@ function AppLoaderInner() {
return (
<TelemetryEnabledProvider>
<TerminalRouterProvider>
<App />
<React.Suspense fallback={<LoadingScreen />}>
<App />
</React.Suspense>
</TerminalRouterProvider>
</TelemetryEnabledProvider>
);
Expand Down
Loading
Loading