From 09fe70cdb49a059c182a42338919e50f2e979b32 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 7 Dec 2025 12:19:58 -0600 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20add=20loading=20state=20t?= =?UTF-8?q?o=20ProjectContext=20to=20fix=20Storybook=20race?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SingleProject story sometimes failed to show workspace creation controls because of a race between two async operations: 1. ProjectContext loads projects via api.projects.list() 2. WorkspaceContext loads workspaces via api.workspace.list() When WorkspaceContext.loading became false, App would render but projects might still be empty (size 0), causing the condition `projects.size === 1` to be false, so creationProjectPath was null. Fix: Add a loading state to ProjectContext and gate on it in AppLoaderInner alongside workspaceContext.loading. _Generated with `mux`_ --- src/browser/components/AppLoader.tsx | 7 ++++--- src/browser/contexts/ProjectContext.tsx | 10 +++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/browser/components/AppLoader.tsx b/src/browser/components/AppLoader.tsx index 74b48d8103..89e20306b2 100644 --- a/src/browser/components/AppLoader.tsx +++ b/src/browser/components/AppLoader.tsx @@ -3,7 +3,7 @@ import App from "../App"; import { LoadingScreen } from "./LoadingScreen"; import { useWorkspaceStoreRaw } from "../stores/WorkspaceStore"; import { useGitStatusStoreRaw } from "../stores/GitStatusStore"; -import { ProjectProvider } from "../contexts/ProjectContext"; +import { ProjectProvider, useProjectContext } from "../contexts/ProjectContext"; import { APIProvider, useAPI, type APIClient } from "@/browser/contexts/API"; import { WorkspaceProvider, useWorkspaceContext } from "../contexts/WorkspaceContext"; @@ -41,6 +41,7 @@ export function AppLoader(props: AppLoaderProps) { */ function AppLoaderInner() { const workspaceContext = useWorkspaceContext(); + const projectContext = useProjectContext(); const { api } = useAPI(); // Get store instances @@ -72,8 +73,8 @@ function AppLoaderInner() { api, ]); - // Show loading screen until stores are synced - if (workspaceContext.loading || !storesSynced) { + // Show loading screen until both projects and workspaces are loaded and stores synced + if (projectContext.loading || workspaceContext.loading || !storesSynced) { return ; } diff --git a/src/browser/contexts/ProjectContext.tsx b/src/browser/contexts/ProjectContext.tsx index 39c36f85fb..1bfa2a2cfd 100644 --- a/src/browser/contexts/ProjectContext.tsx +++ b/src/browser/contexts/ProjectContext.tsx @@ -25,6 +25,8 @@ interface WorkspaceModalState { export interface ProjectContext { projects: Map; + /** True while initial project list is loading */ + loading: boolean; refreshProjects: () => Promise; addProject: (normalizedPath: string, projectConfig: ProjectConfig) => void; removeProject: (path: string) => Promise<{ success: boolean; error?: string }>; @@ -58,6 +60,7 @@ function deriveProjectName(projectPath: string): string { export function ProjectProvider(props: { children: ReactNode }) { const { api } = useAPI(); const [projects, setProjects] = useState>(new Map()); + const [loading, setLoading] = useState(true); const [isProjectCreateModalOpen, setProjectCreateModalOpen] = useState(false); const [workspaceModalState, setWorkspaceModalState] = useState({ isOpen: false, @@ -82,7 +85,10 @@ export function ProjectProvider(props: { children: ReactNode }) { }, [api]); useEffect(() => { - void refreshProjects(); + void (async () => { + await refreshProjects(); + setLoading(false); + })(); }, [refreshProjects]); const addProject = useCallback((normalizedPath: string, projectConfig: ProjectConfig) => { @@ -224,6 +230,7 @@ export function ProjectProvider(props: { children: ReactNode }) { const value = useMemo( () => ({ projects, + loading, refreshProjects, addProject, removeProject, @@ -239,6 +246,7 @@ export function ProjectProvider(props: { children: ReactNode }) { }), [ projects, + loading, refreshProjects, addProject, removeProject,