Skip to content

Commit 5bc5ae3

Browse files
authored
🤖 fix: prevent race condition when reloading with stale workspace (#943)
## Problem On page reload in Electron, the app fails to restore the previously selected workspace and instead shows the home page with this error: ``` Workspace 6164ccb357 no longer exists, clearing selection ``` ## Root Cause Two race conditions were causing this: ### 1. API connection timing `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. ### 2. Missing metadata guard Even after fixing the API timing, if `selectedWorkspace` from localStorage referred to a deleted workspace, the `AIView` would try to render before the validation effect could clear the stale selection. ## Solution 1. **Wait for API**: `loadWorkspaceMetadata` now returns a boolean indicating success. The effect only calls `setLoading(false)` after actual load. When `api` becomes available, the effect re-runs. 2. **Guard AIView render**: If `selectedWorkspace` exists but `currentMetadata` is undefined, return `null` instead of rendering `AIView`. The validation effect will clear the stale selection on the next tick. 3. **Remove redundant effect**: Removed the simple validation effect (lines 126-130) since the comprehensive one (lines 165-189) handles all cases including missing fields update. --- _Generated with `mux`_
1 parent b18381d commit 5bc5ae3

File tree

2 files changed

+21
-16
lines changed

2 files changed

+21
-16
lines changed

src/browser/App.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,6 @@ function AppInner() {
121121
prevWorkspaceRef.current = selectedWorkspace;
122122
}, [selectedWorkspace, telemetry]);
123123

124-
// Validate selectedWorkspace when metadata changes
125-
// Clear selection if workspace was deleted
126-
useEffect(() => {
127-
if (selectedWorkspace && !workspaceMetadata.has(selectedWorkspace.workspaceId)) {
128-
setSelectedWorkspace(null);
129-
}
130-
}, [selectedWorkspace, workspaceMetadata, setSelectedWorkspace]);
131-
132124
// Track last-read timestamps for unread indicators
133125
const { lastReadTimestamps, onToggleUnread } = useUnreadTracking(selectedWorkspace);
134126

@@ -571,15 +563,22 @@ function AppInner() {
571563
{selectedWorkspace ? (
572564
(() => {
573565
const currentMetadata = workspaceMetadata.get(selectedWorkspace.workspaceId);
566+
// Guard: Don't render AIView if workspace metadata not found.
567+
// This can happen when selectedWorkspace (from localStorage) refers to a
568+
// deleted workspace, or during a race condition on reload before the
569+
// validation effect clears the stale selection.
570+
if (!currentMetadata) {
571+
return null;
572+
}
574573
// Use metadata.name for workspace name (works for both worktree and local runtimes)
575574
// Fallback to path-based derivation for legacy compatibility
576575
const workspaceName =
577-
currentMetadata?.name ??
576+
currentMetadata.name ??
578577
selectedWorkspace.namedWorkspacePath?.split("/").pop() ??
579578
selectedWorkspace.workspaceId;
580579
// Use live metadata path (updates on rename) with fallback to initial path
581580
const workspacePath =
582-
currentMetadata?.namedWorkspacePath ?? selectedWorkspace.namedWorkspacePath ?? "";
581+
currentMetadata.namedWorkspacePath ?? selectedWorkspace.namedWorkspacePath ?? "";
583582
return (
584583
<ErrorBoundary
585584
workspaceInfo={`${selectedWorkspace.projectName}/${workspaceName}`}
@@ -591,9 +590,9 @@ function AppInner() {
591590
projectName={selectedWorkspace.projectName}
592591
branch={workspaceName}
593592
namedWorkspacePath={workspacePath}
594-
runtimeConfig={currentMetadata?.runtimeConfig}
595-
incompatibleRuntime={currentMetadata?.incompatibleRuntime}
596-
status={currentMetadata?.status}
593+
runtimeConfig={currentMetadata.runtimeConfig}
594+
incompatibleRuntime={currentMetadata.incompatibleRuntime}
595+
status={currentMetadata.status}
597596
/>
598597
</ErrorBoundary>
599598
);

src/browser/contexts/WorkspaceContext.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
118118
);
119119

120120
const loadWorkspaceMetadata = useCallback(async () => {
121-
if (!api) return;
121+
if (!api) return false; // Return false to indicate metadata wasn't loaded
122122
try {
123123
const metadataList = await api.workspace.list(undefined);
124124
const metadataMap = new Map<string, FrontendWorkspaceMetadata>();
@@ -128,16 +128,22 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
128128
metadataMap.set(metadata.id, metadata);
129129
}
130130
setWorkspaceMetadata(metadataMap);
131+
return true; // Return true to indicate metadata was loaded
131132
} catch (error) {
132133
console.error("Failed to load workspace metadata:", error);
133134
setWorkspaceMetadata(new Map());
135+
return true; // Still return true - we tried to load, just got empty result
134136
}
135137
}, [setWorkspaceMetadata, api]);
136138

137-
// Load metadata once on mount
139+
// Load metadata once on mount (and again when api becomes available)
138140
useEffect(() => {
139141
void (async () => {
140-
await loadWorkspaceMetadata();
142+
const loaded = await loadWorkspaceMetadata();
143+
if (!loaded) {
144+
// api not available yet - effect will run again when api connects
145+
return;
146+
}
141147
// After loading metadata (which may trigger migration), reload projects
142148
// to ensure frontend has the updated config with workspace IDs
143149
await refreshProjects();

0 commit comments

Comments
 (0)