Skip to content

Commit 69bf899

Browse files
committed
🤖 refactor: move selectedWorkspace into WorkspaceContext
WorkspaceContext now owns all workspace state including selectedWorkspace: - Uses usePersistedState internally for localStorage persistence - Handles URL hash restoration (#workspace=<id>) - Handles launch project detection (--add-project flag) Benefits: - Single source of truth for all workspace state - Simplified AppLoader (eliminated AppLoaderMiddle layer) - Cleaner separation of concerns - ~60 net lines removed Tests updated to set selectedWorkspace via context API instead of props.
1 parent d7ffd74 commit 69bf899

File tree

3 files changed

+129
-189
lines changed

3 files changed

+129
-189
lines changed

src/components/AppLoader.tsx

Lines changed: 17 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,41 @@ import App from "../App";
33
import { LoadingScreen } from "./LoadingScreen";
44
import { useWorkspaceStoreRaw } from "../stores/WorkspaceStore";
55
import { useGitStatusStoreRaw } from "../stores/GitStatusStore";
6-
import { usePersistedState } from "../hooks/usePersistedState";
7-
import type { WorkspaceSelection } from "./ProjectSidebar";
86
import { AppProvider } from "../contexts/AppContext";
97
import { ProjectProvider, useProjectContext } from "../contexts/ProjectContext";
108
import { WorkspaceProvider, useWorkspaceContext } from "../contexts/WorkspaceContext";
119

1210
/**
1311
* AppLoader handles all initialization before rendering the main App:
14-
* 1. Load workspace metadata and projects
12+
* 1. Load workspace metadata and projects (via contexts)
1513
* 2. Sync stores with loaded data
16-
* 3. Restore workspace selection from URL hash (if present)
17-
* 4. Only render App when everything is ready
14+
* 3. Only render App when everything is ready
1815
*
16+
* WorkspaceContext handles workspace selection restoration (localStorage, URL hash, launch project).
1917
* This ensures App.tsx can assume stores are always synced and removes
2018
* the need for conditional guards in effects.
2119
*/
2220
export function AppLoader() {
23-
return (
24-
<ProjectProvider>
25-
<AppLoaderMiddle />
26-
</ProjectProvider>
27-
);
28-
}
29-
30-
function AppLoaderMiddle() {
31-
// Workspace selection - restored from localStorage immediately
32-
const [selectedWorkspace, setSelectedWorkspace] = usePersistedState<WorkspaceSelection | null>(
33-
"selectedWorkspace",
34-
null
35-
);
36-
3721
const { refreshProjects } = useProjectContext();
3822

39-
// Render App with WorkspaceProvider wrapping it
4023
return (
41-
<WorkspaceProvider
42-
selectedWorkspace={selectedWorkspace}
43-
onSelectedWorkspaceUpdate={setSelectedWorkspace}
44-
onProjectsUpdate={() => {
45-
void refreshProjects();
46-
}}
47-
>
48-
<AppLoaderInner
49-
selectedWorkspace={selectedWorkspace}
50-
setSelectedWorkspace={setSelectedWorkspace}
51-
/>
52-
</WorkspaceProvider>
24+
<ProjectProvider>
25+
<WorkspaceProvider
26+
onProjectsUpdate={() => {
27+
void refreshProjects();
28+
}}
29+
>
30+
<AppLoaderInner />
31+
</WorkspaceProvider>
32+
</ProjectProvider>
5333
);
5434
}
5535

5636
/**
57-
* Inner component that has access to WorkspaceContext
37+
* Inner component that has access to both ProjectContext and WorkspaceContext.
38+
* Syncs stores and shows loading screen until ready.
5839
*/
59-
function AppLoaderInner(props: {
60-
selectedWorkspace: WorkspaceSelection | null;
61-
setSelectedWorkspace: (workspace: WorkspaceSelection | null) => void;
62-
}) {
40+
function AppLoaderInner() {
6341
const workspaceContext = useWorkspaceContext();
6442

6543
// Get store instances
@@ -85,78 +63,6 @@ function AppLoaderInner(props: {
8563
gitStatusStore,
8664
]);
8765

88-
// Restore workspace from URL hash (runs once when stores are synced)
89-
const [hasRestoredFromHash, setHasRestoredFromHash] = useState(false);
90-
91-
useEffect(() => {
92-
const { setSelectedWorkspace } = props;
93-
// Wait until stores are synced before attempting restoration
94-
if (!storesSynced) return;
95-
96-
// Only run once
97-
if (hasRestoredFromHash) return;
98-
99-
const hash = window.location.hash;
100-
if (hash.startsWith("#workspace=")) {
101-
const workspaceId = decodeURIComponent(hash.substring("#workspace=".length));
102-
103-
// Find workspace in metadata
104-
const metadata = workspaceContext.workspaceMetadata.get(workspaceId);
105-
106-
if (metadata) {
107-
// Restore from hash (overrides localStorage)
108-
setSelectedWorkspace({
109-
workspaceId: metadata.id,
110-
projectPath: metadata.projectPath,
111-
projectName: metadata.projectName,
112-
namedWorkspacePath: metadata.namedWorkspacePath,
113-
});
114-
}
115-
}
116-
117-
setHasRestoredFromHash(true);
118-
}, [storesSynced, workspaceContext.workspaceMetadata, hasRestoredFromHash, props]);
119-
120-
// Check for launch project from server (for --add-project flag)
121-
// This only applies in server mode
122-
useEffect(() => {
123-
const { selectedWorkspace, setSelectedWorkspace } = props;
124-
// Wait until stores are synced and hash restoration is complete
125-
if (!storesSynced || !hasRestoredFromHash) return;
126-
127-
// Skip if we already have a selected workspace (from localStorage or URL hash)
128-
if (selectedWorkspace) return;
129-
130-
// Only check once
131-
const checkLaunchProject = async () => {
132-
// Only available in server mode
133-
if (!window.api.server?.getLaunchProject) return;
134-
135-
const launchProjectPath = await window.api.server.getLaunchProject();
136-
if (!launchProjectPath) return;
137-
138-
// Find first workspace in this project
139-
const projectWorkspaces = Array.from(workspaceContext.workspaceMetadata.values()).filter(
140-
(meta) => meta.projectPath === launchProjectPath
141-
);
142-
143-
if (projectWorkspaces.length > 0) {
144-
// Select the first workspace in the project
145-
const metadata = projectWorkspaces[0];
146-
setSelectedWorkspace({
147-
workspaceId: metadata.id,
148-
projectPath: metadata.projectPath,
149-
projectName: metadata.projectName,
150-
namedWorkspacePath: metadata.namedWorkspacePath,
151-
});
152-
}
153-
// If no workspaces exist yet, just leave the project in the sidebar
154-
// The user will need to create a workspace
155-
};
156-
157-
void checkLaunchProject();
158-
}, [storesSynced, hasRestoredFromHash, workspaceContext.workspaceMetadata, props]);
159-
16066
// Show loading screen until stores are synced
16167
if (workspaceContext.loading || !storesSynced) {
16268
return <LoadingScreen />;
@@ -170,8 +76,8 @@ function AppLoaderInner(props: {
17076
createWorkspace={workspaceContext.createWorkspace}
17177
removeWorkspace={workspaceContext.removeWorkspace}
17278
renameWorkspace={workspaceContext.renameWorkspace}
173-
selectedWorkspace={props.selectedWorkspace}
174-
setSelectedWorkspace={props.setSelectedWorkspace}
79+
selectedWorkspace={workspaceContext.selectedWorkspace}
80+
setSelectedWorkspace={workspaceContext.setSelectedWorkspace}
17581
>
17682
<App />
17783
</AppProvider>

0 commit comments

Comments
 (0)