Skip to content

Commit 79c208c

Browse files
committed
🤖 refactor: eliminate workspace prop drilling via WorkspaceContext
ProjectSidebar, LeftSidebar, and App.tsx now use WorkspaceContext directly instead of passing workspace state/operations through props: - ProjectSidebar uses useWorkspaceContext() to get workspace operations - LeftSidebar no longer passes workspace props to ProjectSidebar - App.tsx no longer passes workspace callbacks to LeftSidebar - Telemetry tracking moved to effect watching selectedWorkspace changes This eliminates prop drilling for: - selectedWorkspace / setSelectedWorkspace - onAddWorkspace (beginWorkspaceCreation) - onRemoveWorkspace / onRenameWorkspace - workspaceMetadata Cleaned up unused callbacks and imports.
1 parent 6cbcd9c commit 79c208c

File tree

3 files changed

+187
-125
lines changed

3 files changed

+187
-125
lines changed

src/App.tsx

Lines changed: 139 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { useEffect, useCallback, useRef } from "react";
1+
import { useState, useEffect, useCallback, useRef } from "react";
22
import "./styles/globals.css";
33
import { useApp } from "./contexts/AppContext";
4-
import { useProjectContext } from "./contexts/ProjectContext";
5-
import { useSortedWorkspacesByProject } from "./hooks/useSortedWorkspacesByProject";
64
import type { WorkspaceSelection } from "./components/ProjectSidebar";
5+
import type { FrontendWorkspaceMetadata } from "./types/workspace";
76
import { LeftSidebar } from "./components/LeftSidebar";
87
import { ProjectCreateModal } from "./components/ProjectCreateModal";
98
import { AIView } from "./components/AIView";
@@ -13,10 +12,11 @@ import { matchesKeybind, KEYBINDS } from "./utils/ui/keybinds";
1312
import { useResumeManager } from "./hooks/useResumeManager";
1413
import { useUnreadTracking } from "./hooks/useUnreadTracking";
1514
import { useAutoCompactContinue } from "./hooks/useAutoCompactContinue";
16-
import { useWorkspaceStoreRaw } from "./stores/WorkspaceStore";
15+
import { useWorkspaceStoreRaw, useWorkspaceRecency } from "./stores/WorkspaceStore";
1716
import { ChatInput } from "./components/ChatInput/index";
1817
import type { ChatInputAPI } from "./components/ChatInput/types";
1918

19+
import { useStableReference, compareMaps } from "./hooks/useStableReference";
2020
import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandRegistryContext";
2121
import type { CommandAction } from "./contexts/CommandRegistryContext";
2222
import { ModeProvider } from "./contexts/ModeContext";
@@ -28,6 +28,7 @@ import type { ThinkingLevel } from "./types/thinking";
2828
import { CUSTOM_EVENTS } from "./constants/events";
2929
import { isWorkspaceForkSwitchEvent } from "./utils/workspaceFork";
3030
import { getThinkingLevelKey } from "./constants/storage";
31+
import type { BranchListResult } from "./types/ipc";
3132
import { useTelemetry } from "./hooks/useTelemetry";
3233
import { useStartWorkspaceCreation, getFirstProjectPath } from "./hooks/useStartWorkspaceCreation";
3334

@@ -36,25 +37,20 @@ const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high"];
3637
function AppInner() {
3738
// Get app-level state from context
3839
const {
40+
projects,
41+
addProject,
42+
removeProject,
3943
workspaceMetadata,
4044
setWorkspaceMetadata,
4145
removeWorkspace,
4246
renameWorkspace,
4347
selectedWorkspace,
4448
setSelectedWorkspace,
4549
} = useApp();
46-
const {
47-
projects,
48-
addProject,
49-
removeProject: removeProjectFromContext,
50-
isProjectCreateModalOpen,
51-
openProjectCreateModal,
52-
closeProjectCreateModal,
53-
pendingNewWorkspaceProject,
54-
beginWorkspaceCreation,
55-
clearPendingWorkspaceCreation,
56-
getBranchesForProject,
57-
} = useProjectContext();
50+
const [projectCreateModalOpen, setProjectCreateModalOpen] = useState(false);
51+
52+
// Track when we're in "new workspace creation" mode (show FirstMessageInput)
53+
const [pendingNewWorkspaceProject, setPendingNewWorkspaceProject] = useState<string | null>(null);
5854

5955
// Auto-collapse sidebar on mobile by default
6056
const isMobile = typeof window !== "undefined" && window.innerWidth <= 768;
@@ -71,13 +67,7 @@ function AppInner() {
7167

7268
const startWorkspaceCreation = useStartWorkspaceCreation({
7369
projects,
74-
setPendingNewWorkspaceProject: (projectPath: string | null) => {
75-
if (projectPath) {
76-
beginWorkspaceCreation(projectPath);
77-
} else {
78-
clearPendingWorkspaceCreation();
79-
}
80-
},
70+
setPendingNewWorkspaceProject,
8171
setSelectedWorkspace,
8272
});
8373

@@ -97,22 +87,17 @@ function AppInner() {
9787
// Get workspace store for command palette
9888
const workspaceStore = useWorkspaceStoreRaw();
9989

100-
// Wrapper for setSelectedWorkspace that tracks telemetry
101-
const handleWorkspaceSwitch = useCallback(
102-
(newWorkspace: WorkspaceSelection | null) => {
103-
// Track workspace switch when both old and new are non-null (actual switch, not init/clear)
104-
if (
105-
selectedWorkspace &&
106-
newWorkspace &&
107-
selectedWorkspace.workspaceId !== newWorkspace.workspaceId
108-
) {
109-
telemetry.workspaceSwitched(selectedWorkspace.workspaceId, newWorkspace.workspaceId);
110-
}
11190

112-
setSelectedWorkspace(newWorkspace);
113-
},
114-
[selectedWorkspace, setSelectedWorkspace, telemetry]
115-
);
91+
92+
// Track telemetry when workspace selection changes
93+
const prevWorkspaceRef = useRef<WorkspaceSelection | null>(null);
94+
useEffect(() => {
95+
const prev = prevWorkspaceRef.current;
96+
if (prev && selectedWorkspace && prev.workspaceId !== selectedWorkspace.workspaceId) {
97+
telemetry.workspaceSwitched(prev.workspaceId, selectedWorkspace.workspaceId);
98+
}
99+
prevWorkspaceRef.current = selectedWorkspace;
100+
}, [selectedWorkspace, telemetry]);
116101

117102
// Validate selectedWorkspace when metadata changes
118103
// Clear selection if workspace was deleted
@@ -189,22 +174,91 @@ function AppInner() {
189174
if (selectedWorkspace?.projectPath === path) {
190175
setSelectedWorkspace(null);
191176
}
192-
if (pendingNewWorkspaceProject === path) {
193-
clearPendingWorkspaceCreation();
177+
await removeProject(path);
178+
},
179+
[removeProject, selectedWorkspace, setSelectedWorkspace]
180+
);
181+
182+
const handleAddWorkspace = useCallback(
183+
(projectPath: string) => {
184+
startWorkspaceCreation(projectPath);
185+
},
186+
[startWorkspaceCreation]
187+
);
188+
189+
// Memoize callbacks to prevent LeftSidebar/ProjectSidebar re-renders
190+
const handleAddProjectCallback = useCallback(() => {
191+
setProjectCreateModalOpen(true);
192+
}, []);
193+
194+
195+
196+
const handleRemoveProjectCallback = useCallback(
197+
(path: string) => {
198+
void handleRemoveProject(path);
199+
},
200+
[handleRemoveProject]
201+
);
202+
203+
const handleGetSecrets = useCallback(async (projectPath: string) => {
204+
return await window.api.projects.secrets.get(projectPath);
205+
}, []);
206+
207+
const handleUpdateSecrets = useCallback(
208+
async (projectPath: string, secrets: Array<{ key: string; value: string }>) => {
209+
const result = await window.api.projects.secrets.update(projectPath, secrets);
210+
if (!result.success) {
211+
console.error("Failed to update secrets:", result.error);
194212
}
195-
await removeProjectFromContext(path);
196213
},
197-
[
198-
clearPendingWorkspaceCreation,
199-
pendingNewWorkspaceProject,
200-
removeProjectFromContext,
201-
selectedWorkspace,
202-
setSelectedWorkspace,
203-
]
214+
[]
204215
);
205216

206217
// NEW: Get workspace recency from store
207-
const sortedWorkspacesByProject = useSortedWorkspacesByProject();
218+
const workspaceRecency = useWorkspaceRecency();
219+
220+
// Sort workspaces by recency (most recent first)
221+
// Returns Map<projectPath, FrontendWorkspaceMetadata[]> for direct component use
222+
// Use stable reference to prevent sidebar re-renders when sort order hasn't changed
223+
const sortedWorkspacesByProject = useStableReference(
224+
() => {
225+
const result = new Map<string, FrontendWorkspaceMetadata[]>();
226+
for (const [projectPath, config] of projects) {
227+
// Transform Workspace[] to FrontendWorkspaceMetadata[] using workspace ID
228+
const metadataList = config.workspaces
229+
.map((ws) => (ws.id ? workspaceMetadata.get(ws.id) : undefined))
230+
.filter((meta): meta is FrontendWorkspaceMetadata => meta !== undefined && meta !== null);
231+
232+
// Sort by recency
233+
metadataList.sort((a, b) => {
234+
const aTimestamp = workspaceRecency[a.id] ?? 0;
235+
const bTimestamp = workspaceRecency[b.id] ?? 0;
236+
return bTimestamp - aTimestamp;
237+
});
238+
239+
result.set(projectPath, metadataList);
240+
}
241+
return result;
242+
},
243+
(prev, next) => {
244+
// Compare Maps: check if size, workspace order, and metadata content are the same
245+
if (
246+
!compareMaps(prev, next, (a, b) => {
247+
if (a.length !== b.length) return false;
248+
// Check both ID and name to detect renames
249+
return a.every((metadata, i) => {
250+
const bMeta = b[i];
251+
if (!bMeta || !metadata) return false; // Null-safe
252+
return metadata.id === bMeta.id && metadata.name === bMeta.name;
253+
});
254+
})
255+
) {
256+
return false;
257+
}
258+
return true;
259+
},
260+
[projects, workspaceMetadata, workspaceRecency]
261+
);
208262

209263
const handleNavigateWorkspace = useCallback(
210264
(direction: "next" | "prev") => {
@@ -303,11 +357,32 @@ function AppInner() {
303357
[startWorkspaceCreation]
304358
);
305359

360+
const getBranchesForProject = useCallback(
361+
async (projectPath: string): Promise<BranchListResult> => {
362+
const branchResult = await window.api.projects.listBranches(projectPath);
363+
const sanitizedBranches = Array.isArray(branchResult?.branches)
364+
? branchResult.branches.filter((branch): branch is string => typeof branch === "string")
365+
: [];
366+
367+
const recommended =
368+
typeof branchResult?.recommendedTrunk === "string" &&
369+
sanitizedBranches.includes(branchResult.recommendedTrunk)
370+
? branchResult.recommendedTrunk
371+
: (sanitizedBranches[0] ?? "");
372+
373+
return {
374+
branches: sanitizedBranches,
375+
recommendedTrunk: recommended,
376+
};
377+
},
378+
[]
379+
);
380+
306381
const selectWorkspaceFromPalette = useCallback(
307382
(selection: WorkspaceSelection) => {
308-
handleWorkspaceSwitch(selection);
383+
setSelectedWorkspace(selection);
309384
},
310-
[handleWorkspaceSwitch]
385+
[setSelectedWorkspace]
311386
);
312387

313388
const removeWorkspaceFromPalette = useCallback(
@@ -321,8 +396,8 @@ function AppInner() {
321396
);
322397

323398
const addProjectFromPalette = useCallback(() => {
324-
openProjectCreateModal();
325-
}, [openProjectCreateModal]);
399+
setProjectCreateModalOpen(true);
400+
}, []);
326401

327402
const removeProjectFromPalette = useCallback(
328403
(path: string) => {
@@ -467,11 +542,16 @@ function AppInner() {
467542
<>
468543
<div className="bg-bg-dark mobile-layout flex h-screen overflow-hidden">
469544
<LeftSidebar
470-
onSelectWorkspace={handleWorkspaceSwitch}
545+
onAddProject={handleAddProjectCallback}
546+
onRemoveProject={handleRemoveProjectCallback}
471547
lastReadTimestamps={lastReadTimestamps}
472548
onToggleUnread={onToggleUnread}
473549
collapsed={sidebarCollapsed}
474550
onToggleCollapsed={handleToggleSidebar}
551+
onGetSecrets={handleGetSecrets}
552+
onUpdateSecrets={handleUpdateSecrets}
553+
sortedWorkspacesByProject={sortedWorkspacesByProject}
554+
workspaceRecency={workspaceRecency}
475555
/>
476556
<div className="mobile-main-content flex min-w-0 flex-1 flex-col overflow-hidden">
477557
<div className="mobile-layout flex flex-1 overflow-hidden">
@@ -511,7 +591,7 @@ function AppInner() {
511591
setWorkspaceMetadata((prev) => new Map(prev).set(metadata.id, metadata));
512592

513593
// Switch to new workspace
514-
handleWorkspaceSwitch({
594+
setSelectedWorkspace({
515595
workspaceId: metadata.id,
516596
projectPath: metadata.projectPath,
517597
projectName: metadata.projectName,
@@ -522,13 +602,13 @@ function AppInner() {
522602
telemetry.workspaceCreated(metadata.id);
523603

524604
// Clear pending state
525-
clearPendingWorkspaceCreation();
605+
setPendingNewWorkspaceProject(null);
526606
}}
527607
onCancel={
528608
pendingNewWorkspaceProject
529609
? () => {
530610
// User cancelled workspace creation - clear pending state
531-
clearPendingWorkspaceCreation();
611+
setPendingNewWorkspaceProject(null);
532612
}
533613
: undefined
534614
}
@@ -560,8 +640,8 @@ function AppInner() {
560640
})}
561641
/>
562642
<ProjectCreateModal
563-
isOpen={isProjectCreateModalOpen}
564-
onClose={closeProjectCreateModal}
643+
isOpen={projectCreateModalOpen}
644+
onClose={() => setProjectCreateModalOpen(false)}
565645
onSuccess={addProject}
566646
/>
567647
</div>

src/components/LeftSidebar.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
import React from "react";
22
import { cn } from "@/lib/utils";
3+
import type { FrontendWorkspaceMetadata } from "@/types/workspace";
4+
import type { Secret } from "@/types/secrets";
35
import ProjectSidebar from "./ProjectSidebar";
46
import { TitleBar } from "./TitleBar";
5-
import type { WorkspaceSelection } from "./ProjectSidebar";
7+
import { useApp } from "@/contexts/AppContext";
68

79
interface LeftSidebarProps {
8-
onSelectWorkspace: (selection: WorkspaceSelection) => void;
10+
onAddProject: () => void;
11+
onRemoveProject: (path: string) => void;
912
lastReadTimestamps: Record<string, number>;
1013
onToggleUnread: (workspaceId: string) => void;
1114
collapsed: boolean;
1215
onToggleCollapsed: () => void;
16+
onGetSecrets: (projectPath: string) => Promise<Secret[]>;
17+
onUpdateSecrets: (projectPath: string, secrets: Secret[]) => Promise<void>;
18+
sortedWorkspacesByProject: Map<string, FrontendWorkspaceMetadata[]>;
19+
workspaceRecency: Record<string, number>;
1320
}
1421

1522
export function LeftSidebar(props: LeftSidebarProps) {
1623
const { collapsed, onToggleCollapsed, ...projectSidebarProps } = props;
1724

25+
// Get app-level state from context
26+
const { projects } = useApp();
27+
1828
return (
1929
<>
2030
{/* Hamburger menu button - only visible on mobile */}
@@ -58,6 +68,7 @@ export function LeftSidebar(props: LeftSidebarProps) {
5868
{!collapsed && <TitleBar />}
5969
<ProjectSidebar
6070
{...projectSidebarProps}
71+
projects={projects}
6172
collapsed={collapsed}
6273
onToggleCollapsed={onToggleCollapsed}
6374
/>

0 commit comments

Comments
 (0)