Skip to content

Commit 1a4d591

Browse files
jonasnobileclaude
andcommitted
feat: prune stale pane map entries for hidden worktree columns
Evicts pane map entries for projects no longer rendered (e.g. worktree columns hidden in overview mode) so spatial navigation skips them instead of getting stuck on invisible panes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8e69924 commit 1a4d591

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

crates/okena-views-terminal/src/layout/navigation.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
220234
mod 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();

src/views/root/render.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::keybindings::{ShowKeybindings, ShowSessionManager, ShowThemeSelector, ShowCommandPalette, ShowSettings, OpenSettingsFile, ShowFileSearch, ShowContentSearch, ShowProjectSwitcher, ShowDiffViewer, ShowHookLog, NewProject, ToggleSidebar, ToggleSidebarAutoHide, TogglePaneSwitcher, CreateWorktree, CheckForUpdates, InstallUpdate, FocusSidebar, ShowPairingDialog, StartAllServices, StopAllServices, ClearFocus, EqualizeLayout};
22
use crate::settings::{open_settings_file, settings_entity};
33
use crate::theme::theme;
4-
use crate::views::layout::navigation::get_pane_map;
4+
use crate::views::layout::navigation::{get_pane_map, prune_pane_map};
55
use crate::views::layout::split_pane::{compute_resize, render_project_divider, render_sidebar_divider, DragState};
66
use crate::workspace::requests::OverlayRequest;
77
use crate::ui::tokens::{ui_text_md, ui_text_xl};
@@ -112,6 +112,14 @@ impl RootView {
112112

113113
let num_projects = visible_projects.len();
114114

115+
// Evict stale pane map entries for projects no longer rendered
116+
// (e.g. worktree columns hidden in overview mode)
117+
{
118+
let visible_ids: std::collections::HashSet<&str> = visible_projects.iter()
119+
.map(|s| s.as_str()).collect();
120+
prune_pane_map(&visible_ids);
121+
}
122+
115123
// Empty state when folder filter yields no results
116124
if num_projects == 0 {
117125
let has_folder_filter = self.workspace.read(cx).active_folder_filter().is_some();

0 commit comments

Comments
 (0)