Skip to content
Open
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
75 changes: 72 additions & 3 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useCallback, useRef } from "react";
import { useEffect, useCallback, useRef, useState } from "react";
import { useRouter } from "./contexts/RouterContext";
import { useLocation, useNavigate } from "react-router-dom";
import "./styles/globals.css";
Expand Down Expand Up @@ -59,7 +59,7 @@ import {
markPendingWorkspaceAiSettings,
} from "@/browser/utils/workspaceAiSettingsSync";
import { AuthTokenModal } from "@/browser/components/AuthTokenModal/AuthTokenModal";

import { CreationCenterContent } from "@/browser/features/ChatInput/CreationCenterContent";
import { ProjectPage } from "@/browser/components/ProjectPage/ProjectPage";

import { SettingsProvider, useSettings } from "./contexts/SettingsContext";
Expand All @@ -86,6 +86,13 @@ import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults";
import { LandingPage } from "@/browser/features/LandingPage/LandingPage";
import { LoadingScreen } from "@/browser/components/LoadingScreen/LoadingScreen";

interface WorkspaceCreationTransition {
workspaceId: string;
projectName: string;
workspaceName: string;
workspaceTitle?: string;
}

function AppInner() {
// Get workspace state from context
const {
Expand Down Expand Up @@ -175,6 +182,41 @@ function AppInner() {
const creationProjectPath =
!selectedWorkspace && !currentWorkspaceId ? pendingNewWorkspaceProject : null;

const [workspaceCreationTransition, setWorkspaceCreationTransition] =
useState<WorkspaceCreationTransition | null>(null);

const activeWorkspaceIdForTransition =
currentWorkspaceId ?? selectedWorkspace?.workspaceId ?? null;

// Keep a single creation overlay visible while routing from ProjectPage → WorkspaceShell.
// Without this bridge the mobile UI shows two back-to-back loading screens with a
// restarted animation, which feels like a redundant second loading step.
useEffect(() => {
if (!workspaceCreationTransition) {
return;
}

if (isAnalyticsOpen || currentSettingsSection) {
setWorkspaceCreationTransition(null);
return;
}

if (activeWorkspaceIdForTransition !== workspaceCreationTransition.workspaceId) {
setWorkspaceCreationTransition(null);
}
}, [
activeWorkspaceIdForTransition,
currentSettingsSection,
isAnalyticsOpen,
workspaceCreationTransition,
]);

const showWorkspaceCreationTransition =
workspaceCreationTransition !== null &&
!isAnalyticsOpen &&
!currentSettingsSection &&
activeWorkspaceIdForTransition === workspaceCreationTransition.workspaceId;

// History navigation (back/forward)
const navigate = useNavigate();
const location = useLocation();
Expand All @@ -192,6 +234,15 @@ function AppInner() {
setSidebarCollapsed((prev) => !prev);
}, [setSidebarCollapsed]);

const handleWorkspaceHydrated = useCallback((workspaceId: string) => {
setWorkspaceCreationTransition((prev) => {
if (prev?.workspaceId !== workspaceId) {
return prev;
}
return null;
});
}, []);

// Telemetry tracking
const telemetry = useTelemetry();

Expand Down Expand Up @@ -984,7 +1035,7 @@ function AppInner() {
<div className="mobile-main-content flex min-w-0 flex-1 flex-col overflow-hidden">
<WindowsToolchainBanner />
<RosettaBanner />
<div className="mobile-layout flex flex-1 overflow-hidden">
<div className="mobile-layout relative flex flex-1 overflow-hidden">
{/* Route-driven settings and analytics render in the main pane so project/workspace navigation stays visible. */}
{isAnalyticsOpen ? (
<AnalyticsDashboard
Expand Down Expand Up @@ -1029,6 +1080,7 @@ function AppInner() {
namedWorkspacePath={workspacePath}
runtimeConfig={currentMetadata.runtimeConfig}
incompatibleRuntime={currentMetadata.incompatibleRuntime}
onWorkspaceHydrated={handleWorkspaceHydrated}
isInitializing={currentMetadata.isInitializing === true}
/>
</ErrorBoundary>
Expand Down Expand Up @@ -1067,6 +1119,13 @@ function AppInner() {
setWorkspaceMetadata((prev) => new Map(prev).set(metadata.id, metadata));

if (options?.autoNavigate !== false) {
setWorkspaceCreationTransition({
workspaceId: metadata.id,
projectName,
workspaceName: metadata.name,
workspaceTitle: metadata.title,
});

// Only switch to new workspace if user hasn't selected another one
// during the creation process (selectedWorkspace was null when creation started)
setSelectedWorkspace((current) => {
Expand Down Expand Up @@ -1097,6 +1156,16 @@ function AppInner() {
onToggleLeftSidebarCollapsed={handleToggleSidebar}
/>
)}
{showWorkspaceCreationTransition && workspaceCreationTransition && (
<div className="absolute inset-0 z-20">
<CreationCenterContent
projectName={workspaceCreationTransition.projectName}
isSending
workspaceName={workspaceCreationTransition.workspaceName}
workspaceTitle={workspaceCreationTransition.workspaceTitle}
/>
</div>
)}
</div>
</div>
<CommandPalette getSlashContext={() => ({ workspaceId: selectedWorkspace?.workspaceId })} />
Expand Down
2 changes: 2 additions & 0 deletions src/browser/components/AIView/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ interface AIViewProps {
className?: string;
/** If set, workspace is incompatible (from newer mux version) and this error should be displayed */
incompatibleRuntime?: string;
/** Called once the workspace shell has hydrated initial state after navigation. */
onWorkspaceHydrated?: (workspaceId: string) => void;
/** True if workspace is still being initialized (postCreateSetup or initWorkspace running) */
isInitializing?: boolean;
}
Expand Down
23 changes: 17 additions & 6 deletions src/browser/components/WorkspaceShell/WorkspaceShell.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { TerminalSessionCreateOptions } from "@/browser/utils/terminal";
import React, { useCallback, useRef } from "react";
import React, { useCallback, useEffect, useRef } from "react";
import { cn } from "@/common/lib/utils";
import { LoadingAnimation } from "../LoadingAnimation/LoadingAnimation";
import { RIGHT_SIDEBAR_WIDTH_KEY, getReviewImmersiveKey } from "@/common/constants/storage";
Expand Down Expand Up @@ -38,6 +38,8 @@ interface WorkspaceShellProps {
onToggleLeftSidebarCollapsed: () => void;
runtimeConfig?: RuntimeConfig;
className?: string;
/** Called when initial workspace hydration finishes after navigation. */
onWorkspaceHydrated?: (workspaceId: string) => void;
/** True if workspace is still being initialized (postCreateSetup or initWorkspace running) */
isInitializing?: boolean;
}
Expand Down Expand Up @@ -68,6 +70,9 @@ const WorkspacePlaceholder: React.FC<{
);

export const WorkspaceShell: React.FC<WorkspaceShellProps> = (props) => {
const workspaceId = props.workspaceId;
const onWorkspaceHydrated = props.onWorkspaceHydrated;

const shellRef = useRef<HTMLDivElement>(null);
const shellSize = useResizeObserver(shellRef);

Expand Down Expand Up @@ -113,15 +118,15 @@ export const WorkspaceShell: React.FC<WorkspaceShellProps> = (props) => {
// On mobile touch devices, always use popout since the right sidebar is hidden
const isMobileTouch = window.matchMedia("(max-width: 768px) and (pointer: coarse)").matches;
if (isMobileTouch) {
void openTerminalPopout(props.workspaceId, props.runtimeConfig, options);
void openTerminalPopout(workspaceId, props.runtimeConfig, options);
} else {
addTerminalRef.current?.(options);
}
},
[openTerminalPopout, props.workspaceId, props.runtimeConfig]
[openTerminalPopout, props.runtimeConfig, workspaceId]
);

const reviews = useReviews(props.workspaceId);
const reviews = useReviews(workspaceId);
const { addReview } = reviews;
const handleReviewNote = useCallback(
(data: ReviewNoteData) => {
Expand All @@ -130,12 +135,18 @@ export const WorkspaceShell: React.FC<WorkspaceShellProps> = (props) => {
[addReview]
);

const workspaceState = useWorkspaceState(props.workspaceId);
const [isReviewImmersive] = usePersistedState(getReviewImmersiveKey(props.workspaceId), false, {
const workspaceState = useWorkspaceState(workspaceId);
const [isReviewImmersive] = usePersistedState(getReviewImmersiveKey(workspaceId), false, {
listener: true,
});
const backgroundBashError = useBackgroundBashError();

useEffect(() => {
if (!workspaceState.loading) {
onWorkspaceHydrated?.(workspaceId);
}
}, [onWorkspaceHydrated, workspaceId, workspaceState.loading]);

if (!workspaceState || workspaceState.loading) {
return (
<WorkspacePlaceholder
Expand Down
Loading