1- import { useEffect , useCallback , useRef } from "react" ;
1+ import { useState , useEffect , useCallback , useRef } from "react" ;
22import "./styles/globals.css" ;
3- import { useApp } from "./contexts/AppContext " ;
3+ import { useWorkspaceContext } from "./contexts/WorkspaceContext " ;
44import { useProjectContext } from "./contexts/ProjectContext" ;
5- import { useSortedWorkspacesByProject } from "./hooks/useSortedWorkspacesByProject" ;
65import type { WorkspaceSelection } from "./components/ProjectSidebar" ;
6+ import type { FrontendWorkspaceMetadata } from "./types/workspace" ;
77import { LeftSidebar } from "./components/LeftSidebar" ;
88import { ProjectCreateModal } from "./components/ProjectCreateModal" ;
99import { AIView } from "./components/AIView" ;
@@ -13,10 +13,11 @@ import { matchesKeybind, KEYBINDS } from "./utils/ui/keybinds";
1313import { useResumeManager } from "./hooks/useResumeManager" ;
1414import { useUnreadTracking } from "./hooks/useUnreadTracking" ;
1515import { useAutoCompactContinue } from "./hooks/useAutoCompactContinue" ;
16- import { useWorkspaceStoreRaw } from "./stores/WorkspaceStore" ;
16+ import { useWorkspaceStoreRaw , useWorkspaceRecency } from "./stores/WorkspaceStore" ;
1717import { ChatInput } from "./components/ChatInput/index" ;
1818import type { ChatInputAPI } from "./components/ChatInput/types" ;
1919
20+ import { useStableReference , compareMaps } from "./hooks/useStableReference" ;
2021import { CommandRegistryProvider , useCommandRegistry } from "./contexts/CommandRegistryContext" ;
2122import type { CommandAction } from "./contexts/CommandRegistryContext" ;
2223import { ModeProvider } from "./contexts/ModeContext" ;
@@ -28,34 +29,34 @@ import type { ThinkingLevel } from "./types/thinking";
2829import { CUSTOM_EVENTS } from "./constants/events" ;
2930import { isWorkspaceForkSwitchEvent } from "./utils/workspaceFork" ;
3031import { getThinkingLevelKey } from "./constants/storage" ;
32+ import type { BranchListResult } from "./types/ipc" ;
3133import { useTelemetry } from "./hooks/useTelemetry" ;
3234import { useStartWorkspaceCreation , getFirstProjectPath } from "./hooks/useStartWorkspaceCreation" ;
3335
3436const THINKING_LEVELS : ThinkingLevel [ ] = [ "off" , "low" , "medium" , "high" ] ;
3537
3638function AppInner ( ) {
37- // Get app-level state from context
39+ // Get workspace state from context
3840 const {
3941 workspaceMetadata,
4042 setWorkspaceMetadata,
4143 removeWorkspace,
4244 renameWorkspace,
4345 selectedWorkspace,
4446 setSelectedWorkspace,
45- } = useApp ( ) ;
47+ } = useWorkspaceContext ( ) ;
4648 const {
4749 projects,
48- addProject,
49- removeProject : removeProjectFromContext ,
50- isProjectCreateModalOpen,
50+ removeProject,
5151 openProjectCreateModal,
52+ isProjectCreateModalOpen,
5253 closeProjectCreateModal,
53- pendingNewWorkspaceProject,
54- beginWorkspaceCreation,
55- clearPendingWorkspaceCreation,
56- getBranchesForProject,
54+ addProject,
5755 } = useProjectContext ( ) ;
5856
57+ // Track when we're in "new workspace creation" mode (show FirstMessageInput)
58+ const [ pendingNewWorkspaceProject , setPendingNewWorkspaceProject ] = useState < string | null > ( null ) ;
59+
5960 // Auto-collapse sidebar on mobile by default
6061 const isMobile = typeof window !== "undefined" && window . innerWidth <= 768 ;
6162 const [ sidebarCollapsed , setSidebarCollapsed ] = usePersistedState ( "sidebarCollapsed" , isMobile ) ;
@@ -71,13 +72,7 @@ function AppInner() {
7172
7273 const startWorkspaceCreation = useStartWorkspaceCreation ( {
7374 projects,
74- setPendingNewWorkspaceProject : ( projectPath : string | null ) => {
75- if ( projectPath ) {
76- beginWorkspaceCreation ( projectPath ) ;
77- } else {
78- clearPendingWorkspaceCreation ( ) ;
79- }
80- } ,
75+ setPendingNewWorkspaceProject,
8176 setSelectedWorkspace,
8277 } ) ;
8378
@@ -97,22 +92,15 @@ function AppInner() {
9792 // Get workspace store for command palette
9893 const workspaceStore = useWorkspaceStoreRaw ( ) ;
9994
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- }
111-
112- setSelectedWorkspace ( newWorkspace ) ;
113- } ,
114- [ selectedWorkspace , setSelectedWorkspace , telemetry ]
115- ) ;
95+ // Track telemetry when workspace selection changes
96+ const prevWorkspaceRef = useRef < WorkspaceSelection | null > ( null ) ;
97+ useEffect ( ( ) => {
98+ const prev = prevWorkspaceRef . current ;
99+ if ( prev && selectedWorkspace && prev . workspaceId !== selectedWorkspace . workspaceId ) {
100+ telemetry . workspaceSwitched ( prev . workspaceId , selectedWorkspace . workspaceId ) ;
101+ }
102+ prevWorkspaceRef . current = selectedWorkspace ;
103+ } , [ selectedWorkspace , telemetry ] ) ;
116104
117105 // Validate selectedWorkspace when metadata changes
118106 // Clear selection if workspace was deleted
@@ -189,22 +177,59 @@ function AppInner() {
189177 if ( selectedWorkspace ?. projectPath === path ) {
190178 setSelectedWorkspace ( null ) ;
191179 }
192- if ( pendingNewWorkspaceProject === path ) {
193- clearPendingWorkspaceCreation ( ) ;
194- }
195- await removeProjectFromContext ( path ) ;
180+ await removeProject ( path ) ;
196181 } ,
197- [
198- clearPendingWorkspaceCreation ,
199- pendingNewWorkspaceProject ,
200- removeProjectFromContext ,
201- selectedWorkspace ,
202- setSelectedWorkspace ,
203- ]
182+ // eslint-disable-next-line react-hooks/exhaustive-deps
183+ [ selectedWorkspace , setSelectedWorkspace ]
204184 ) ;
205185
186+ // Memoize callbacks to prevent LeftSidebar/ProjectSidebar re-renders
187+
206188 // NEW: Get workspace recency from store
207- const sortedWorkspacesByProject = useSortedWorkspacesByProject ( ) ;
189+ const workspaceRecency = useWorkspaceRecency ( ) ;
190+
191+ // Sort workspaces by recency (most recent first)
192+ // Returns Map<projectPath, FrontendWorkspaceMetadata[]> for direct component use
193+ // Use stable reference to prevent sidebar re-renders when sort order hasn't changed
194+ const sortedWorkspacesByProject = useStableReference (
195+ ( ) => {
196+ const result = new Map < string , FrontendWorkspaceMetadata [ ] > ( ) ;
197+ for ( const [ projectPath , config ] of projects ) {
198+ // Transform Workspace[] to FrontendWorkspaceMetadata[] using workspace ID
199+ const metadataList = config . workspaces
200+ . map ( ( ws ) => ( ws . id ? workspaceMetadata . get ( ws . id ) : undefined ) )
201+ . filter ( ( meta ) : meta is FrontendWorkspaceMetadata => meta !== undefined && meta !== null ) ;
202+
203+ // Sort by recency
204+ metadataList . sort ( ( a , b ) => {
205+ const aTimestamp = workspaceRecency [ a . id ] ?? 0 ;
206+ const bTimestamp = workspaceRecency [ b . id ] ?? 0 ;
207+ return bTimestamp - aTimestamp ;
208+ } ) ;
209+
210+ result . set ( projectPath , metadataList ) ;
211+ }
212+ return result ;
213+ } ,
214+ ( prev , next ) => {
215+ // Compare Maps: check if size, workspace order, and metadata content are the same
216+ if (
217+ ! compareMaps ( prev , next , ( a , b ) => {
218+ if ( a . length !== b . length ) return false ;
219+ // Check both ID and name to detect renames
220+ return a . every ( ( metadata , i ) => {
221+ const bMeta = b [ i ] ;
222+ if ( ! bMeta || ! metadata ) return false ; // Null-safe
223+ return metadata . id === bMeta . id && metadata . name === bMeta . name ;
224+ } ) ;
225+ } )
226+ ) {
227+ return false ;
228+ }
229+ return true ;
230+ } ,
231+ [ projects , workspaceMetadata , workspaceRecency ]
232+ ) ;
208233
209234 const handleNavigateWorkspace = useCallback (
210235 ( direction : "next" | "prev" ) => {
@@ -303,11 +328,32 @@ function AppInner() {
303328 [ startWorkspaceCreation ]
304329 ) ;
305330
331+ const getBranchesForProject = useCallback (
332+ async ( projectPath : string ) : Promise < BranchListResult > => {
333+ const branchResult = await window . api . projects . listBranches ( projectPath ) ;
334+ const sanitizedBranches = Array . isArray ( branchResult ?. branches )
335+ ? branchResult . branches . filter ( ( branch ) : branch is string => typeof branch === "string" )
336+ : [ ] ;
337+
338+ const recommended =
339+ typeof branchResult ?. recommendedTrunk === "string" &&
340+ sanitizedBranches . includes ( branchResult . recommendedTrunk )
341+ ? branchResult . recommendedTrunk
342+ : ( sanitizedBranches [ 0 ] ?? "" ) ;
343+
344+ return {
345+ branches : sanitizedBranches ,
346+ recommendedTrunk : recommended ,
347+ } ;
348+ } ,
349+ [ ]
350+ ) ;
351+
306352 const selectWorkspaceFromPalette = useCallback (
307353 ( selection : WorkspaceSelection ) => {
308- handleWorkspaceSwitch ( selection ) ;
354+ setSelectedWorkspace ( selection ) ;
309355 } ,
310- [ handleWorkspaceSwitch ]
356+ [ setSelectedWorkspace ]
311357 ) ;
312358
313359 const removeWorkspaceFromPalette = useCallback (
@@ -467,11 +513,12 @@ function AppInner() {
467513 < >
468514 < div className = "bg-bg-dark mobile-layout flex h-screen overflow-hidden" >
469515 < LeftSidebar
470- onSelectWorkspace = { handleWorkspaceSwitch }
471516 lastReadTimestamps = { lastReadTimestamps }
472517 onToggleUnread = { onToggleUnread }
473518 collapsed = { sidebarCollapsed }
474519 onToggleCollapsed = { handleToggleSidebar }
520+ sortedWorkspacesByProject = { sortedWorkspacesByProject }
521+ workspaceRecency = { workspaceRecency }
475522 />
476523 < div className = "mobile-main-content flex min-w-0 flex-1 flex-col overflow-hidden" >
477524 < div className = "mobile-layout flex flex-1 overflow-hidden" >
@@ -511,7 +558,7 @@ function AppInner() {
511558 setWorkspaceMetadata ( ( prev ) => new Map ( prev ) . set ( metadata . id , metadata ) ) ;
512559
513560 // Switch to new workspace
514- handleWorkspaceSwitch ( {
561+ setSelectedWorkspace ( {
515562 workspaceId : metadata . id ,
516563 projectPath : metadata . projectPath ,
517564 projectName : metadata . projectName ,
@@ -522,13 +569,13 @@ function AppInner() {
522569 telemetry . workspaceCreated ( metadata . id ) ;
523570
524571 // Clear pending state
525- clearPendingWorkspaceCreation ( ) ;
572+ setPendingNewWorkspaceProject ( null ) ;
526573 } }
527574 onCancel = {
528575 pendingNewWorkspaceProject
529576 ? ( ) => {
530577 // User cancelled workspace creation - clear pending state
531- clearPendingWorkspaceCreation ( ) ;
578+ setPendingNewWorkspaceProject ( null ) ;
532579 }
533580 : undefined
534581 }
0 commit comments