@@ -129,6 +129,13 @@ impl PaneMap {
129129 self . panes . get ( next_idx)
130130 }
131131
132+ /// Remove panes whose project_id is not in the given set.
133+ /// Called during render to evict stale entries from hidden projects
134+ /// (e.g. worktree columns that are retained but not currently visible).
135+ pub fn retain_projects ( & mut self , visible_ids : & std:: collections:: HashSet < & str > ) {
136+ self . panes . retain ( |p| visible_ids. contains ( p. project_id . as_str ( ) ) ) ;
137+ }
138+
132139 /// Get all registered panes
133140 pub fn panes ( & self ) -> & [ PaneBounds ] {
134141 & self . panes
@@ -216,6 +223,13 @@ pub fn deregister_pane_bounds(project_id: &str, layout_path: &[usize]) {
216223 pane_map_lock ( ) . lock ( ) . deregister ( project_id, layout_path) ;
217224}
218225
226+ /// Remove pane entries for projects not in the visible set.
227+ /// Prevents stale entries from hidden columns (e.g. worktree projects with
228+ /// show_in_overview=false) from blocking spatial navigation.
229+ pub fn prune_pane_map ( visible_project_ids : & std:: collections:: HashSet < & str > ) {
230+ pane_map_lock ( ) . lock ( ) . retain_projects ( visible_project_ids) ;
231+ }
232+
219233#[ cfg( test) ]
220234mod tests {
221235 use super :: { PaneMap , NavigationDirection } ;
@@ -309,6 +323,41 @@ mod tests {
309323 assert_eq ! ( map. panes( ) . len( ) , 1 ) ;
310324 }
311325
326+ #[ test]
327+ fn retain_projects_removes_hidden ( ) {
328+ let mut map = PaneMap :: new ( ) ;
329+ map. register ( "parent" . into ( ) , vec ! [ 0 ] , make_bounds ( 0.0 , 0.0 , 400.0 , 600.0 ) , None ) ;
330+ map. register ( "worktree" . into ( ) , vec ! [ 0 ] , make_bounds ( 400.0 , 0.0 , 400.0 , 600.0 ) , None ) ;
331+ map. register ( "other" . into ( ) , vec ! [ 0 ] , make_bounds ( 800.0 , 0.0 , 400.0 , 600.0 ) , None ) ;
332+ assert_eq ! ( map. panes( ) . len( ) , 3 ) ;
333+
334+ // Only parent and other are visible (worktree hidden in overview)
335+ let visible: std:: collections:: HashSet < & str > = [ "parent" , "other" ] . into_iter ( ) . collect ( ) ;
336+ map. retain_projects ( & visible) ;
337+ assert_eq ! ( map. panes( ) . len( ) , 2 ) ;
338+ assert ! ( map. find_pane( "parent" , & [ 0 ] ) . is_some( ) ) ;
339+ assert ! ( map. find_pane( "worktree" , & [ 0 ] ) . is_none( ) ) ;
340+ assert ! ( map. find_pane( "other" , & [ 0 ] ) . is_some( ) ) ;
341+ }
342+
343+ #[ test]
344+ fn retain_projects_allows_navigation_past_hidden ( ) {
345+ let mut map = PaneMap :: new ( ) ;
346+ map. register ( "a" . into ( ) , vec ! [ 0 ] , make_bounds ( 0.0 , 0.0 , 400.0 , 600.0 ) , None ) ;
347+ map. register ( "hidden_wt" . into ( ) , vec ! [ 0 ] , make_bounds ( 400.0 , 0.0 , 400.0 , 600.0 ) , None ) ;
348+ map. register ( "b" . into ( ) , vec ! [ 0 ] , make_bounds ( 800.0 , 0.0 , 400.0 , 600.0 ) , None ) ;
349+
350+ // Prune hidden worktree
351+ let visible: std:: collections:: HashSet < & str > = [ "a" , "b" ] . into_iter ( ) . collect ( ) ;
352+ map. retain_projects ( & visible) ;
353+
354+ // Navigation from a should reach b (not get stuck on hidden_wt)
355+ let source = map. find_pane ( "a" , & [ 0 ] ) . unwrap ( ) ;
356+ let target = map. find_nearest_in_direction ( source, NavigationDirection :: Right ) ;
357+ assert ! ( target. is_some( ) ) ;
358+ assert_eq ! ( target. unwrap( ) . project_id, "b" ) ;
359+ }
360+
312361 #[ test]
313362 fn navigation_works_after_upsert ( ) {
314363 let mut map = PaneMap :: new ( ) ;
0 commit comments