From 19145ce617739f8c3dfb7aa97076c7dcf8742049 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 6 Dec 2025 13:08:06 -0600 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20fix:=20prevent=20race=20cond?= =?UTF-8?q?ition=20when=20reloading=20with=20stale=20workspace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On page reload, selectedWorkspace is restored from localStorage synchronously during render, but workspace metadata is loaded asynchronously. If the selected workspace was deleted or the metadata hasn't loaded yet, rendering AIView with a non-existent workspace ID causes WorkspaceStore.assertGet() to throw. Fix: Guard against missing metadata before rendering AIView. If selectedWorkspace exists but currentMetadata is undefined, return null instead of rendering. The existing validation effect will clear the stale selection on the next tick. This also simplifies the code by removing unnecessary optional chaining since we now guard at the top of the conditional. _Generated with mux_ --- src/browser/App.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/browser/App.tsx b/src/browser/App.tsx index f7fb5498bb..bdf19f5620 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -571,15 +571,22 @@ function AppInner() { {selectedWorkspace ? ( (() => { const currentMetadata = workspaceMetadata.get(selectedWorkspace.workspaceId); + // Guard: Don't render AIView if workspace metadata not found. + // This can happen when selectedWorkspace (from localStorage) refers to a + // deleted workspace, or during a race condition on reload before the + // validation effect clears the stale selection. + if (!currentMetadata) { + return null; + } // Use metadata.name for workspace name (works for both worktree and local runtimes) // Fallback to path-based derivation for legacy compatibility const workspaceName = - currentMetadata?.name ?? + currentMetadata.name ?? selectedWorkspace.namedWorkspacePath?.split("/").pop() ?? selectedWorkspace.workspaceId; // Use live metadata path (updates on rename) with fallback to initial path const workspacePath = - currentMetadata?.namedWorkspacePath ?? selectedWorkspace.namedWorkspacePath ?? ""; + currentMetadata.namedWorkspacePath ?? selectedWorkspace.namedWorkspacePath ?? ""; return ( ); From 940596bc7820c175c89a040a556ae7ac2c54a140 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 6 Dec 2025 15:05:17 -0600 Subject: [PATCH 2/2] fix: wait for API connection before setting loading=false Root cause: WorkspaceContext's loadWorkspaceMetadata() returned early when api was null (during initial 'connecting' state), but setLoading(false) was still called. This caused App to render with empty metadata while selectedWorkspace was restored from localStorage, triggering the validation effect to clear the selection. Fix: - loadWorkspaceMetadata now returns boolean indicating if it actually loaded - Effect only calls setLoading(false) after successful load - When api becomes available, effect re-runs due to dependency change Also: - Removed redundant validation effect (lines 126-130) - the comprehensive one at lines 165-189 handles all cases including missing fields update --- src/browser/App.tsx | 8 -------- src/browser/contexts/WorkspaceContext.tsx | 12 +++++++++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/browser/App.tsx b/src/browser/App.tsx index bdf19f5620..20874b4c3b 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -121,14 +121,6 @@ function AppInner() { prevWorkspaceRef.current = selectedWorkspace; }, [selectedWorkspace, telemetry]); - // Validate selectedWorkspace when metadata changes - // Clear selection if workspace was deleted - useEffect(() => { - if (selectedWorkspace && !workspaceMetadata.has(selectedWorkspace.workspaceId)) { - setSelectedWorkspace(null); - } - }, [selectedWorkspace, workspaceMetadata, setSelectedWorkspace]); - // Track last-read timestamps for unread indicators const { lastReadTimestamps, onToggleUnread } = useUnreadTracking(selectedWorkspace); diff --git a/src/browser/contexts/WorkspaceContext.tsx b/src/browser/contexts/WorkspaceContext.tsx index 293308db24..b62990427d 100644 --- a/src/browser/contexts/WorkspaceContext.tsx +++ b/src/browser/contexts/WorkspaceContext.tsx @@ -118,7 +118,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) { ); const loadWorkspaceMetadata = useCallback(async () => { - if (!api) return; + if (!api) return false; // Return false to indicate metadata wasn't loaded try { const metadataList = await api.workspace.list(undefined); const metadataMap = new Map(); @@ -128,16 +128,22 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) { metadataMap.set(metadata.id, metadata); } setWorkspaceMetadata(metadataMap); + return true; // Return true to indicate metadata was loaded } catch (error) { console.error("Failed to load workspace metadata:", error); setWorkspaceMetadata(new Map()); + return true; // Still return true - we tried to load, just got empty result } }, [setWorkspaceMetadata, api]); - // Load metadata once on mount + // Load metadata once on mount (and again when api becomes available) useEffect(() => { void (async () => { - await loadWorkspaceMetadata(); + const loaded = await loadWorkspaceMetadata(); + if (!loaded) { + // api not available yet - effect will run again when api connects + return; + } // After loading metadata (which may trigger migration), reload projects // to ensure frontend has the updated config with workspace IDs await refreshProjects();