@@ -41,14 +41,7 @@ const ptyManager = new PtyManager()
4141let config = loadConfig ( )
4242let stopWatchingStatus : ( ( ) => void ) | null = null
4343
44- // Track repo root per window
45- const windowRepoRoots = new Map < number , string > ( )
46-
47- function getWindowFromEvent ( event : Electron . IpcMainEvent | Electron . IpcMainInvokeEvent ) : BrowserWindow | null {
48- return BrowserWindow . fromWebContents ( event . sender )
49- }
50-
51- function createWindow ( repoRoot ?: string ) : BrowserWindow {
44+ function createWindow ( ) : BrowserWindow {
5245 const bounds = config . windowBounds || { width : 1400 , height : 900 , x : undefined ! , y : undefined ! }
5346
5447 const win = new BrowserWindow ( {
@@ -68,10 +61,6 @@ function createWindow(repoRoot?: string): BrowserWindow {
6861 }
6962 } )
7063
71- if ( repoRoot ) {
72- windowRepoRoots . set ( win . id , repoRoot )
73- }
74-
7564 // Forward renderer console logs to debug log
7665 win . webContents . on ( 'console-message' , ( _event , level , message ) => {
7766 const levelName = [ 'verbose' , 'info' , 'warn' , 'error' ] [ level ] || 'log'
@@ -87,10 +76,6 @@ function createWindow(repoRoot?: string): BrowserWindow {
8776 win . on ( 'resize' , saveBounds )
8877 win . on ( 'move' , saveBounds )
8978
90- win . on ( 'closed' , ( ) => {
91- windowRepoRoots . delete ( win . id )
92- } )
93-
9479 // Load renderer
9580 if ( process . env [ 'ELECTRON_RENDERER_URL' ] ) {
9681 win . loadURL ( process . env [ 'ELECTRON_RENDERER_URL' ] )
@@ -102,31 +87,24 @@ function createWindow(repoRoot?: string): BrowserWindow {
10287}
10388
10489function registerIpcHandlers ( ) : void {
105- // Worktree handlers — scoped to the calling window's repo root
106- ipcMain . handle ( 'worktree:list' , async ( event ) => {
107- const win = getWindowFromEvent ( event )
108- const repoRoot = win ? windowRepoRoots . get ( win . id ) : null
90+ // Worktree handlers — every call takes an explicit repoRoot, since a single
91+ // window now shows worktrees from multiple repos at once.
92+ ipcMain . handle ( 'worktree:list' , async ( _ , repoRoot : string ) => {
10993 if ( ! repoRoot ) return [ ]
11094 const trees = await listWorktrees ( repoRoot )
111- // Keep activity records' branch/repoRoot in sync with current git state so
112- // historical reporting still identifies the worktree after it's removed.
11395 for ( const wt of trees ) {
11496 touchActivityMeta ( wt . path , { branch : wt . branch , repoRoot } )
11597 }
11698 return trees
11799 } )
118100
119- ipcMain . handle ( 'worktree:branches' , async ( event ) => {
120- const win = getWindowFromEvent ( event )
121- const repoRoot = win ? windowRepoRoots . get ( win . id ) : null
101+ ipcMain . handle ( 'worktree:branches' , async ( _ , repoRoot : string ) => {
122102 if ( ! repoRoot ) return [ ]
123103 return listBranches ( repoRoot )
124104 } )
125105
126- ipcMain . handle ( 'worktree:add' , async ( event , branchName : string , baseBranch ?: string ) => {
127- const win = getWindowFromEvent ( event )
128- const repoRoot = win ? windowRepoRoots . get ( win . id ) : null
129- if ( ! repoRoot ) throw new Error ( 'No repo root configured' )
106+ ipcMain . handle ( 'worktree:add' , async ( _ , repoRoot : string , branchName : string , baseBranch ?: string ) => {
107+ if ( ! repoRoot ) throw new Error ( 'No repo root provided' )
130108 const wtDir = defaultWorktreeDir ( repoRoot )
131109 const mode = config . worktreeBase || DEFAULT_WORKTREE_BASE
132110 return addWorktree ( repoRoot , wtDir , branchName , {
@@ -137,10 +115,8 @@ function registerIpcHandlers(): void {
137115
138116 ipcMain . handle (
139117 'worktree:continue' ,
140- async ( event , worktreePath : string , newBranchName : string , baseBranch ?: string ) => {
141- const win = getWindowFromEvent ( event )
142- const repoRoot = win ? windowRepoRoots . get ( win . id ) : null
143- if ( ! repoRoot ) throw new Error ( 'No repo root configured' )
118+ async ( _ , repoRoot : string , worktreePath : string , newBranchName : string , baseBranch ?: string ) => {
119+ if ( ! repoRoot ) throw new Error ( 'No repo root provided' )
144120 const mode = config . worktreeBase || DEFAULT_WORKTREE_BASE
145121 return continueWorktree ( repoRoot , worktreePath , newBranchName , {
146122 baseBranch,
@@ -154,14 +130,13 @@ function registerIpcHandlers(): void {
154130 } )
155131
156132 ipcMain . handle ( 'worktree:remove' , async (
157- event ,
133+ _ ,
134+ repoRoot : string ,
158135 path : string ,
159136 force ?: boolean ,
160137 removeMeta ?: { prNumber ?: number ; prState ?: PRState }
161138 ) => {
162- const win = getWindowFromEvent ( event )
163- const repoRoot = win ? windowRepoRoots . get ( win . id ) : null
164- if ( ! repoRoot ) throw new Error ( 'No repo root configured' )
139+ if ( ! repoRoot ) throw new Error ( 'No repo root provided' )
165140 // Drop any locally-merged flag for the branch at this path
166141 const trees = await listWorktrees ( repoRoot )
167142 const wt = trees . find ( ( t ) => t . path === path )
@@ -180,34 +155,43 @@ function registerIpcHandlers(): void {
180155 return removeWorktree ( repoRoot , path , force )
181156 } )
182157
183- ipcMain . handle ( 'worktree:dir' , async ( event ) => {
184- const win = getWindowFromEvent ( event )
185- const repoRoot = win ? windowRepoRoots . get ( win . id ) : null
158+ ipcMain . handle ( 'worktree:dir' , async ( _ , repoRoot : string ) => {
186159 if ( ! repoRoot ) return ''
187160 return defaultWorktreeDir ( repoRoot )
188161 } )
189162
190- ipcMain . handle ( 'repo:select' , async ( event ) => {
191- const win = getWindowFromEvent ( event )
192- if ( ! win ) return null
193- const result = await dialog . showOpenDialog ( win , {
163+ ipcMain . handle ( 'repo:list' , ( ) => {
164+ return config . repoRoots
165+ } )
166+
167+ ipcMain . handle ( 'repo:add' , async ( event ) => {
168+ const win = BrowserWindow . fromWebContents ( event . sender )
169+ const result = await dialog . showOpenDialog ( win ! , {
194170 properties : [ 'openDirectory' ] ,
195- title : 'Select Git Repository Root '
171+ title : 'Open Git Repository'
196172 } )
197173 if ( result . canceled || result . filePaths . length === 0 ) return null
198174 const repoRoot = result . filePaths [ 0 ]
199- windowRepoRoots . set ( win . id , repoRoot )
200- // Track in config for reopening
201175 if ( ! config . repoRoots . includes ( repoRoot ) ) {
202176 config . repoRoots . push ( repoRoot )
203177 saveConfig ( config )
178+ broadcastToAllWindows ( 'repo:listChanged' , config . repoRoots )
204179 }
205180 return repoRoot
206181 } )
207182
208- ipcMain . handle ( 'repo:getRoot' , ( event ) => {
209- const win = getWindowFromEvent ( event )
210- return win ? windowRepoRoots . get ( win . id ) || null : null
183+ ipcMain . handle ( 'repo:remove' , ( _ , repoRoot : string ) => {
184+ const idx = config . repoRoots . indexOf ( repoRoot )
185+ if ( idx === - 1 ) return false
186+ config . repoRoots . splice ( idx , 1 )
187+ // Also drop any persisted panes for the removed repo so they don't
188+ // linger as orphans.
189+ if ( config . panes && config . panes [ repoRoot ] ) {
190+ delete config . panes [ repoRoot ]
191+ }
192+ saveConfig ( config )
193+ broadcastToAllWindows ( 'repo:listChanged' , config . repoRoots )
194+ return true
211195 } )
212196
213197 // Changed files
@@ -248,34 +232,26 @@ function registerIpcHandlers(): void {
248232 return getPRStatus ( worktreePath )
249233 } )
250234
251- ipcMain . handle ( 'worktree:mainStatus' , async ( event ) => {
252- const win = getWindowFromEvent ( event )
253- const repoRoot = win ? windowRepoRoots . get ( win . id ) : null
254- if ( ! repoRoot ) throw new Error ( 'No repo root configured' )
235+ ipcMain . handle ( 'worktree:mainStatus' , async ( _ , repoRoot : string ) => {
236+ if ( ! repoRoot ) throw new Error ( 'No repo root provided' )
255237 return getMainWorktreeStatus ( repoRoot )
256238 } )
257239
258- ipcMain . handle ( 'worktree:previewMerge' , async ( event , sourceBranch : string ) => {
259- const win = getWindowFromEvent ( event )
260- const repoRoot = win ? windowRepoRoots . get ( win . id ) : null
261- if ( ! repoRoot ) throw new Error ( 'No repo root configured' )
240+ ipcMain . handle ( 'worktree:previewMerge' , async ( _ , repoRoot : string , sourceBranch : string ) => {
241+ if ( ! repoRoot ) throw new Error ( 'No repo root provided' )
262242 const status = await getMainWorktreeStatus ( repoRoot )
263243 return previewMergeConflicts ( repoRoot , sourceBranch , status . baseBranch )
264244 } )
265245
266- ipcMain . handle ( 'worktree:prepareMain' , async ( event ) => {
267- const win = getWindowFromEvent ( event )
268- const repoRoot = win ? windowRepoRoots . get ( win . id ) : null
269- if ( ! repoRoot ) throw new Error ( 'No repo root configured' )
246+ ipcMain . handle ( 'worktree:prepareMain' , async ( _ , repoRoot : string ) => {
247+ if ( ! repoRoot ) throw new Error ( 'No repo root provided' )
270248 return prepareMainForMerge ( repoRoot )
271249 } )
272250
273251 ipcMain . handle (
274252 'worktree:mergeLocal' ,
275- async ( event , sourceBranch : string , strategy : MergeStrategy ) => {
276- const win = getWindowFromEvent ( event )
277- const repoRoot = win ? windowRepoRoots . get ( win . id ) : null
278- if ( ! repoRoot ) throw new Error ( 'No repo root configured' )
253+ async ( _ , repoRoot : string , sourceBranch : string , strategy : MergeStrategy ) => {
254+ if ( ! repoRoot ) throw new Error ( 'No repo root provided' )
279255 const result = await mergeWorktreeLocally ( repoRoot , sourceBranch , strategy )
280256 // Record the branch as locally merged at its current tip sha. If new
281257 // commits are pushed to the branch later, the flag becomes stale and
@@ -296,9 +272,7 @@ function registerIpcHandlers(): void {
296272 * check can't tell "fork point on trunk" from "trunk position after merge",
297273 * so we don't try to detect external merges automatically. The flag is
298274 * auto-cleared if the branch later gains new commits. */
299- ipcMain . handle ( 'worktree:mergedStatus' , async ( event ) => {
300- const win = getWindowFromEvent ( event )
301- const repoRoot = win ? windowRepoRoots . get ( win . id ) : null
275+ ipcMain . handle ( 'worktree:mergedStatus' , async ( _ , repoRoot : string ) => {
302276 if ( ! repoRoot ) return { }
303277 const trees = await listWorktrees ( repoRoot )
304278 const result : Record < string , boolean > = { }
@@ -515,14 +489,14 @@ function registerIpcHandlers(): void {
515489 return true
516490 } )
517491
518- // Persisted workspace panes (tabs per pane, per worktree)
492+ // Persisted workspace panes (tabs per pane, per worktree, per repo).
519493 ipcMain . handle ( 'config:getPanes' , ( ) => {
520494 return config . panes || { }
521495 } )
522496
523497 ipcMain . handle (
524498 'config:setPanes' ,
525- ( _ , panes : Record < string , PersistedPane [ ] > ) => {
499+ ( _ , panes : Record < string , Record < string , PersistedPane [ ] > > ) => {
526500 config . panes = panes
527501 saveConfig ( config )
528502 return true
@@ -683,7 +657,7 @@ function registerIpcHandlers(): void {
683657
684658 // PTY handlers — route to the calling window
685659 ipcMain . on ( 'pty:create' , ( event , id : string , cwd : string , cmd : string , args : string [ ] ) => {
686- const win = getWindowFromEvent ( event )
660+ const win = BrowserWindow . fromWebContents ( event . sender )
687661 if ( win ) ptyManager . create ( id , cwd , cmd , args , win )
688662 } )
689663
@@ -837,24 +811,21 @@ app.whenReady().then(() => {
837811
838812 // Prune terminal history files not referenced by any persisted tab
839813 const keepIds = new Set < string > ( )
840- for ( const panes of Object . values ( config . panes || { } ) ) {
841- for ( const pane of panes ) {
842- for ( const tab of pane . tabs ) keepIds . add ( tab . id )
814+ for ( const byRepo of Object . values ( config . panes || { } ) ) {
815+ for ( const panes of Object . values ( byRepo ) ) {
816+ for ( const pane of panes ) {
817+ for ( const tab of pane . tabs ) keepIds . add ( tab . id )
818+ }
843819 }
844820 }
845821 pruneTerminalHistory ( keepIds )
846822
847823 // Watch status dir globally — route to correct window via ptyManager
848824 stopWatchingStatus = watchStatusDir ( ( id ) => ptyManager . getWindowForTerminal ( id ) )
849825
850- // Open a window for each saved repo root, or one empty window
851- if ( config . repoRoots . length > 0 ) {
852- for ( const root of config . repoRoots ) {
853- createWindow ( root )
854- }
855- } else {
856- createWindow ( )
857- }
826+ // One window shows all repos. The renderer reads `config.repoRoots` via
827+ // `repo:list` and opens each one on mount.
828+ createWindow ( )
858829
859830 setupAutoUpdater ( )
860831
0 commit comments