1- import { useState , useEffect , useCallback , useRef } from "react" ;
1+ import { useState , useEffect , useCallback , useRef , type Dispatch , type SetStateAction } from "react" ;
22import "./styles/globals.css" ;
33import type { ProjectConfig } from "./config" ;
44import type { WorkspaceSelection } from "./components/ProjectSidebar" ;
55import type { FrontendWorkspaceMetadata } from "./types/workspace" ;
66import { LeftSidebar } from "./components/LeftSidebar" ;
7- import { LoadingScreen } from "./components/LoadingScreen" ;
87import NewWorkspaceModal from "./components/NewWorkspaceModal" ;
98import { DirectorySelectModal } from "./components/DirectorySelectModal" ;
109import { AIView } from "./components/AIView" ;
1110import { ErrorBoundary } from "./components/ErrorBoundary" ;
1211import { usePersistedState , updatePersistedState } from "./hooks/usePersistedState" ;
1312import { matchesKeybind , KEYBINDS } from "./utils/ui/keybinds" ;
14- import { useProjectManagement } from "./hooks/useProjectManagement" ;
15- import { useWorkspaceManagement } from "./hooks/useWorkspaceManagement" ;
1613import { useResumeManager } from "./hooks/useResumeManager" ;
1714import { useUnreadTracking } from "./hooks/useUnreadTracking" ;
1815import { useAutoCompactContinue } from "./hooks/useAutoCompactContinue" ;
1916import { useWorkspaceStoreRaw , useWorkspaceRecency } from "./stores/WorkspaceStore" ;
20- import { useGitStatusStoreRaw } from "./stores/GitStatusStore" ;
2117
2218import { useStableReference , compareMaps } from "./hooks/useStableReference" ;
2319import { CommandRegistryProvider , useCommandRegistry } from "./contexts/CommandRegistryContext" ;
@@ -34,13 +30,48 @@ import { useTelemetry } from "./hooks/useTelemetry";
3430
3531const THINKING_LEVELS : ThinkingLevel [ ] = [ "off" , "low" , "medium" , "high" ] ;
3632
37- function AppInner ( ) {
38- // Workspace selection - restored from localStorage immediately,
39- // but entire UI is gated behind metadata loading (see early return below)
40- const [ selectedWorkspace , setSelectedWorkspace ] = usePersistedState < WorkspaceSelection | null > (
41- "selectedWorkspace" ,
42- null
43- ) ;
33+ interface AppInnerProps {
34+ projects : Map < string , ProjectConfig > ;
35+ setProjects : Dispatch < SetStateAction < Map < string , ProjectConfig > > > ;
36+ addProject : ( ) => Promise < void > ;
37+ removeProject : ( path : string ) => Promise < void > ;
38+ workspaceMetadata : Map < string , FrontendWorkspaceMetadata > ;
39+ setWorkspaceMetadata : Dispatch < SetStateAction < Map < string , FrontendWorkspaceMetadata > > > ;
40+ createWorkspace : (
41+ projectPath : string ,
42+ branchName : string ,
43+ trunkBranch : string
44+ ) => Promise < {
45+ projectPath : string ;
46+ projectName : string ;
47+ namedWorkspacePath : string ;
48+ workspaceId : string ;
49+ } > ;
50+ removeWorkspace : (
51+ workspaceId : string ,
52+ options ?: { force ?: boolean }
53+ ) => Promise < { success : boolean ; error ?: string } > ;
54+ renameWorkspace : (
55+ workspaceId : string ,
56+ newName : string
57+ ) => Promise < { success : boolean ; error ?: string } > ;
58+ selectedWorkspace : WorkspaceSelection | null ;
59+ setSelectedWorkspace : ( workspace : WorkspaceSelection | null ) => void ;
60+ }
61+
62+ function AppInner ( {
63+ projects,
64+ setProjects,
65+ addProject,
66+ removeProject,
67+ workspaceMetadata,
68+ setWorkspaceMetadata,
69+ createWorkspace,
70+ removeWorkspace,
71+ renameWorkspace,
72+ selectedWorkspace,
73+ setSelectedWorkspace,
74+ } : AppInnerProps ) {
4475
4576 const [ workspaceModalOpen , setWorkspaceModalOpen ] = useState ( false ) ;
4677 const [ workspaceModalProject , setWorkspaceModalProject ] = useState < string | null > ( null ) ;
@@ -63,21 +94,18 @@ function AppInner() {
6394 // Telemetry tracking
6495 const telemetry = useTelemetry ( ) ;
6596
97+ // Get workspace store for command palette
98+ const workspaceStore = useWorkspaceStoreRaw ( ) ;
99+
66100 // Wrapper for setSelectedWorkspace that tracks telemetry
67101 const handleWorkspaceSwitch = useCallback (
68102 ( newWorkspace : WorkspaceSelection | null ) => {
69- console . debug ( "[App] handleWorkspaceSwitch called" , {
70- from : selectedWorkspace ?. workspaceId ,
71- to : newWorkspace ?. workspaceId ,
72- } ) ;
73-
74103 // Track workspace switch when both old and new are non-null (actual switch, not init/clear)
75104 if (
76105 selectedWorkspace &&
77106 newWorkspace &&
78107 selectedWorkspace . workspaceId !== newWorkspace . workspaceId
79108 ) {
80- console . debug ( "[App] Calling telemetry.workspaceSwitched" ) ;
81109 telemetry . workspaceSwitched ( selectedWorkspace . workspaceId , newWorkspace . workspaceId ) ;
82110 }
83111
@@ -86,54 +114,13 @@ function AppInner() {
86114 [ selectedWorkspace , setSelectedWorkspace , telemetry ]
87115 ) ;
88116
89- // Use custom hooks for project and workspace management
90- const { projects, setProjects, addProject, removeProject } = useProjectManagement ( ) ;
91-
92- // Workspace management needs to update projects state when workspace operations complete
93- const handleProjectsUpdate = useCallback (
94- ( newProjects : Map < string , ProjectConfig > ) => {
95- setProjects ( newProjects ) ;
96- } ,
97- [ setProjects ]
98- ) ;
99-
100- const {
101- workspaceMetadata,
102- setWorkspaceMetadata,
103- loading : metadataLoading ,
104- createWorkspace,
105- removeWorkspace,
106- renameWorkspace,
107- } = useWorkspaceManagement ( {
108- selectedWorkspace,
109- onProjectsUpdate : handleProjectsUpdate ,
110- onSelectedWorkspaceUpdate : setSelectedWorkspace ,
111- } ) ;
112-
113- // Sync workspace metadata with the stores BEFORE rendering workspace UI
114- const workspaceStore = useWorkspaceStoreRaw ( ) ;
115- const gitStatusStore = useGitStatusStoreRaw ( ) ;
116-
117- // Track whether stores have been synced (separate from metadata loading)
118- const [ storesSynced , setStoresSynced ] = useState ( false ) ;
119-
120- useEffect ( ( ) => {
121- if ( ! metadataLoading ) {
122- workspaceStore . syncWorkspaces ( workspaceMetadata ) ;
123- gitStatusStore . syncWorkspaces ( workspaceMetadata ) ;
124- setStoresSynced ( true ) ;
125- } else {
126- setStoresSynced ( false ) ;
127- }
128- } , [ metadataLoading , workspaceMetadata , workspaceStore , gitStatusStore ] ) ;
129-
130117 // Validate selectedWorkspace when metadata changes
131118 // Clear selection if workspace was deleted
132119 useEffect ( ( ) => {
133- if ( storesSynced && selectedWorkspace && ! workspaceMetadata . has ( selectedWorkspace . workspaceId ) ) {
120+ if ( selectedWorkspace && ! workspaceMetadata . has ( selectedWorkspace . workspaceId ) ) {
134121 setSelectedWorkspace ( null ) ;
135122 }
136- } , [ storesSynced , selectedWorkspace , workspaceMetadata , setSelectedWorkspace ] ) ;
123+ } , [ selectedWorkspace , workspaceMetadata , setSelectedWorkspace ] ) ;
137124
138125 // Track last-read timestamps for unread indicators
139126 const { lastReadTimestamps, onToggleUnread } = useUnreadTracking ( selectedWorkspace ) ;
@@ -167,35 +154,6 @@ function AppInner() {
167154 }
168155 } , [ selectedWorkspace , workspaceMetadata ] ) ;
169156
170- // Restore workspace from URL on mount (if valid)
171- // This effect runs once on mount to restore from hash, which takes priority over localStorage
172- const [ hasRestoredFromHash , setHasRestoredFromHash ] = useState ( false ) ;
173-
174- useEffect ( ( ) => {
175- // Only run once
176- if ( hasRestoredFromHash ) return ;
177-
178- const hash = window . location . hash ;
179- if ( hash . startsWith ( "#workspace=" ) ) {
180- const workspaceId = decodeURIComponent ( hash . substring ( "#workspace=" . length ) ) ;
181-
182- // Find workspace in metadata
183- const metadata = workspaceMetadata . get ( workspaceId ) ;
184-
185- if ( metadata ) {
186- // Restore from hash (overrides localStorage)
187- setSelectedWorkspace ( {
188- workspaceId : metadata . id ,
189- projectPath : metadata . projectPath ,
190- projectName : metadata . projectName ,
191- namedWorkspacePath : metadata . namedWorkspacePath ,
192- } ) ;
193- }
194- }
195-
196- setHasRestoredFromHash ( true ) ;
197- } , [ workspaceMetadata , hasRestoredFromHash , setSelectedWorkspace ] ) ;
198-
199157 // Validate selected workspace exists and has all required fields
200158 useEffect ( ( ) => {
201159 if ( selectedWorkspace ) {
@@ -674,12 +632,6 @@ function AppInner() {
674632 ) ;
675633 } , [ projects , setSelectedWorkspace , setWorkspaceMetadata ] ) ;
676634
677- // CRITICAL: Don't render workspace UI until metadata loads AND stores are synced
678- // This ensures WorkspaceStore.addWorkspace() is called before any component accesses workspaces
679- if ( metadataLoading || ! storesSynced ) {
680- return < LoadingScreen /> ;
681- }
682-
683635 return (
684636 < >
685637 < div className = "bg-bg-dark flex h-screen overflow-hidden [@media(max-width:768px)]:flex-col" >
@@ -766,10 +718,10 @@ function AppInner() {
766718 ) ;
767719}
768720
769- function App ( ) {
721+ function App ( props : AppInnerProps ) {
770722 return (
771723 < CommandRegistryProvider >
772- < AppInner />
724+ < AppInner { ... props } />
773725 </ CommandRegistryProvider >
774726 ) ;
775727}
0 commit comments