1- import { useEffect , useCallback , useRef } from "react" ;
1+ import { useState , 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" ;
64import type { WorkspaceSelection } from "./components/ProjectSidebar" ;
5+ import type { FrontendWorkspaceMetadata } from "./types/workspace" ;
76import { LeftSidebar } from "./components/LeftSidebar" ;
87import { ProjectCreateModal } from "./components/ProjectCreateModal" ;
98import { AIView } from "./components/AIView" ;
@@ -13,10 +12,11 @@ import { matchesKeybind, KEYBINDS } from "./utils/ui/keybinds";
1312import { useResumeManager } from "./hooks/useResumeManager" ;
1413import { useUnreadTracking } from "./hooks/useUnreadTracking" ;
1514import { useAutoCompactContinue } from "./hooks/useAutoCompactContinue" ;
16- import { useWorkspaceStoreRaw } from "./stores/WorkspaceStore" ;
15+ import { useWorkspaceStoreRaw , useWorkspaceRecency } from "./stores/WorkspaceStore" ;
1716import { ChatInput } from "./components/ChatInput/index" ;
1817import type { ChatInputAPI } from "./components/ChatInput/types" ;
1918
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,6 +28,7 @@ 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" ;
3132import { useTelemetry } from "./hooks/useTelemetry" ;
3233import { useStartWorkspaceCreation , getFirstProjectPath } from "./hooks/useStartWorkspaceCreation" ;
3334
@@ -36,25 +37,20 @@ const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high"];
3637function 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 >
0 commit comments