1- import { useState , useEffect , useCallback , useRef } from "react" ;
1+ import { useEffect , useCallback , useRef } from "react" ;
22import "./styles/globals.css" ;
33import { useApp } from "./contexts/AppContext" ;
4+ import { useProjectContext } from "./contexts/ProjectContext" ;
5+ import { useSortedWorkspacesByProject } from "./hooks/useSortedWorkspacesByProject" ;
46import type { WorkspaceSelection } from "./components/ProjectSidebar" ;
5- import type { FrontendWorkspaceMetadata } from "./types/workspace" ;
67import { LeftSidebar } from "./components/LeftSidebar" ;
78import { ProjectCreateModal } from "./components/ProjectCreateModal" ;
89import { AIView } from "./components/AIView" ;
@@ -12,11 +13,10 @@ import { matchesKeybind, KEYBINDS } from "./utils/ui/keybinds";
1213import { useResumeManager } from "./hooks/useResumeManager" ;
1314import { useUnreadTracking } from "./hooks/useUnreadTracking" ;
1415import { useAutoCompactContinue } from "./hooks/useAutoCompactContinue" ;
15- import { useWorkspaceStoreRaw , useWorkspaceRecency } from "./stores/WorkspaceStore" ;
16+ import { useWorkspaceStoreRaw } from "./stores/WorkspaceStore" ;
1617import { ChatInput } from "./components/ChatInput/index" ;
1718import type { ChatInputAPI } from "./components/ChatInput/types" ;
1819
19- import { useStableReference , compareMaps } from "./hooks/useStableReference" ;
2020import { CommandRegistryProvider , useCommandRegistry } from "./contexts/CommandRegistryContext" ;
2121import type { CommandAction } from "./contexts/CommandRegistryContext" ;
2222import { ModeProvider } from "./contexts/ModeContext" ;
@@ -28,7 +28,6 @@ import type { ThinkingLevel } from "./types/thinking";
2828import { CUSTOM_EVENTS } from "./constants/events" ;
2929import { isWorkspaceForkSwitchEvent } from "./utils/workspaceFork" ;
3030import { getThinkingLevelKey } from "./constants/storage" ;
31- import type { BranchListResult } from "./types/ipc" ;
3231import { useTelemetry } from "./hooks/useTelemetry" ;
3332import { useStartWorkspaceCreation , getFirstProjectPath } from "./hooks/useStartWorkspaceCreation" ;
3433
@@ -37,20 +36,25 @@ const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high"];
3736function AppInner ( ) {
3837 // Get app-level state from context
3938 const {
40- projects,
41- addProject,
42- removeProject,
4339 workspaceMetadata,
4440 setWorkspaceMetadata,
4541 removeWorkspace,
4642 renameWorkspace,
4743 selectedWorkspace,
4844 setSelectedWorkspace,
4945 } = useApp ( ) ;
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 ) ;
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 ( ) ;
5458
5559 // Auto-collapse sidebar on mobile by default
5660 const isMobile = typeof window !== "undefined" && window . innerWidth <= 768 ;
@@ -67,7 +71,13 @@ function AppInner() {
6771
6872 const startWorkspaceCreation = useStartWorkspaceCreation ( {
6973 projects,
70- setPendingNewWorkspaceProject,
74+ setPendingNewWorkspaceProject : ( projectPath : string | null ) => {
75+ if ( projectPath ) {
76+ beginWorkspaceCreation ( projectPath ) ;
77+ } else {
78+ clearPendingWorkspaceCreation ( ) ;
79+ }
80+ } ,
7181 setSelectedWorkspace,
7282 } ) ;
7383
@@ -179,96 +189,22 @@ function AppInner() {
179189 if ( selectedWorkspace ?. projectPath === path ) {
180190 setSelectedWorkspace ( null ) ;
181191 }
182- await removeProject ( path ) ;
183- } ,
184- [ removeProject , selectedWorkspace , setSelectedWorkspace ]
185- ) ;
186-
187- const handleAddWorkspace = useCallback (
188- ( projectPath : string ) => {
189- startWorkspaceCreation ( projectPath ) ;
190- } ,
191- [ startWorkspaceCreation ]
192- ) ;
193-
194- // Memoize callbacks to prevent LeftSidebar/ProjectSidebar re-renders
195- const handleAddProjectCallback = useCallback ( ( ) => {
196- setProjectCreateModalOpen ( true ) ;
197- } , [ ] ) ;
198-
199- const handleAddWorkspaceCallback = useCallback (
200- ( projectPath : string ) => {
201- void handleAddWorkspace ( projectPath ) ;
202- } ,
203- [ handleAddWorkspace ]
204- ) ;
205-
206- const handleRemoveProjectCallback = useCallback (
207- ( path : string ) => {
208- void handleRemoveProject ( path ) ;
209- } ,
210- [ handleRemoveProject ]
211- ) ;
212-
213- const handleGetSecrets = useCallback ( async ( projectPath : string ) => {
214- return await window . api . projects . secrets . get ( projectPath ) ;
215- } , [ ] ) ;
216-
217- const handleUpdateSecrets = useCallback (
218- async ( projectPath : string , secrets : Array < { key : string ; value : string } > ) => {
219- const result = await window . api . projects . secrets . update ( projectPath , secrets ) ;
220- if ( ! result . success ) {
221- console . error ( "Failed to update secrets:" , result . error ) ;
192+ if ( pendingNewWorkspaceProject === path ) {
193+ clearPendingWorkspaceCreation ( ) ;
222194 }
195+ await removeProjectFromContext ( path ) ;
223196 } ,
224- [ ]
197+ [
198+ clearPendingWorkspaceCreation ,
199+ pendingNewWorkspaceProject ,
200+ removeProjectFromContext ,
201+ selectedWorkspace ,
202+ setSelectedWorkspace ,
203+ ]
225204 ) ;
226205
227206 // NEW: Get workspace recency from store
228- const workspaceRecency = useWorkspaceRecency ( ) ;
229-
230- // Sort workspaces by recency (most recent first)
231- // Returns Map<projectPath, FrontendWorkspaceMetadata[]> for direct component use
232- // Use stable reference to prevent sidebar re-renders when sort order hasn't changed
233- const sortedWorkspacesByProject = useStableReference (
234- ( ) => {
235- const result = new Map < string , FrontendWorkspaceMetadata [ ] > ( ) ;
236- for ( const [ projectPath , config ] of projects ) {
237- // Transform Workspace[] to FrontendWorkspaceMetadata[] using workspace ID
238- const metadataList = config . workspaces
239- . map ( ( ws ) => ( ws . id ? workspaceMetadata . get ( ws . id ) : undefined ) )
240- . filter ( ( meta ) : meta is FrontendWorkspaceMetadata => meta !== undefined && meta !== null ) ;
241-
242- // Sort by recency
243- metadataList . sort ( ( a , b ) => {
244- const aTimestamp = workspaceRecency [ a . id ] ?? 0 ;
245- const bTimestamp = workspaceRecency [ b . id ] ?? 0 ;
246- return bTimestamp - aTimestamp ;
247- } ) ;
248-
249- result . set ( projectPath , metadataList ) ;
250- }
251- return result ;
252- } ,
253- ( prev , next ) => {
254- // Compare Maps: check if size, workspace order, and metadata content are the same
255- if (
256- ! compareMaps ( prev , next , ( a , b ) => {
257- if ( a . length !== b . length ) return false ;
258- // Check both ID and name to detect renames
259- return a . every ( ( metadata , i ) => {
260- const bMeta = b [ i ] ;
261- if ( ! bMeta || ! metadata ) return false ; // Null-safe
262- return metadata . id === bMeta . id && metadata . name === bMeta . name ;
263- } ) ;
264- } )
265- ) {
266- return false ;
267- }
268- return true ;
269- } ,
270- [ projects , workspaceMetadata , workspaceRecency ]
271- ) ;
207+ const sortedWorkspacesByProject = useSortedWorkspacesByProject ( ) ;
272208
273209 const handleNavigateWorkspace = useCallback (
274210 ( direction : "next" | "prev" ) => {
@@ -367,27 +303,6 @@ function AppInner() {
367303 [ startWorkspaceCreation ]
368304 ) ;
369305
370- const getBranchesForProject = useCallback (
371- async ( projectPath : string ) : Promise < BranchListResult > => {
372- const branchResult = await window . api . projects . listBranches ( projectPath ) ;
373- const sanitizedBranches = Array . isArray ( branchResult ?. branches )
374- ? branchResult . branches . filter ( ( branch ) : branch is string => typeof branch === "string" )
375- : [ ] ;
376-
377- const recommended =
378- typeof branchResult ?. recommendedTrunk === "string" &&
379- sanitizedBranches . includes ( branchResult . recommendedTrunk )
380- ? branchResult . recommendedTrunk
381- : ( sanitizedBranches [ 0 ] ?? "" ) ;
382-
383- return {
384- branches : sanitizedBranches ,
385- recommendedTrunk : recommended ,
386- } ;
387- } ,
388- [ ]
389- ) ;
390-
391306 const selectWorkspaceFromPalette = useCallback (
392307 ( selection : WorkspaceSelection ) => {
393308 handleWorkspaceSwitch ( selection ) ;
@@ -406,8 +321,8 @@ function AppInner() {
406321 ) ;
407322
408323 const addProjectFromPalette = useCallback ( ( ) => {
409- setProjectCreateModalOpen ( true ) ;
410- } , [ ] ) ;
324+ openProjectCreateModal ( ) ;
325+ } , [ openProjectCreateModal ] ) ;
411326
412327 const removeProjectFromPalette = useCallback (
413328 ( path : string ) => {
@@ -553,17 +468,10 @@ function AppInner() {
553468 < div className = "bg-bg-dark mobile-layout flex h-screen overflow-hidden" >
554469 < LeftSidebar
555470 onSelectWorkspace = { handleWorkspaceSwitch }
556- onAddProject = { handleAddProjectCallback }
557- onAddWorkspace = { handleAddWorkspaceCallback }
558- onRemoveProject = { handleRemoveProjectCallback }
559471 lastReadTimestamps = { lastReadTimestamps }
560472 onToggleUnread = { onToggleUnread }
561473 collapsed = { sidebarCollapsed }
562474 onToggleCollapsed = { handleToggleSidebar }
563- onGetSecrets = { handleGetSecrets }
564- onUpdateSecrets = { handleUpdateSecrets }
565- sortedWorkspacesByProject = { sortedWorkspacesByProject }
566- workspaceRecency = { workspaceRecency }
567475 />
568476 < div className = "mobile-main-content flex min-w-0 flex-1 flex-col overflow-hidden" >
569477 < div className = "mobile-layout flex flex-1 overflow-hidden" >
@@ -614,13 +522,13 @@ function AppInner() {
614522 telemetry . workspaceCreated ( metadata . id ) ;
615523
616524 // Clear pending state
617- setPendingNewWorkspaceProject ( null ) ;
525+ clearPendingWorkspaceCreation ( ) ;
618526 } }
619527 onCancel = {
620528 pendingNewWorkspaceProject
621529 ? ( ) => {
622530 // User cancelled workspace creation - clear pending state
623- setPendingNewWorkspaceProject ( null ) ;
531+ clearPendingWorkspaceCreation ( ) ;
624532 }
625533 : undefined
626534 }
@@ -652,8 +560,8 @@ function AppInner() {
652560 } ) }
653561 />
654562 < ProjectCreateModal
655- isOpen = { projectCreateModalOpen }
656- onClose = { ( ) => setProjectCreateModalOpen ( false ) }
563+ isOpen = { isProjectCreateModalOpen }
564+ onClose = { closeProjectCreateModal }
657565 onSuccess = { addProject }
658566 />
659567 </ div >
0 commit comments