aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-14 05:30:20 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-14 05:30:20 +0300
commit226e87306deaba017d36583f51ae34e42b303aad (patch)
tree2a1155689b75007c15d93735aedfa58db7a05edf /src
parent17544c36ee397233e2f7b3319d86e3b517e3b5d0 (diff)
fix: preserve tab identity across restored sessions
Diffstat (limited to 'src')
-rw-r--r--src/app/actions.rs18
-rw-r--r--src/app/api/layouts.rs7
-rw-r--r--src/app/api/tabs.rs12
-rw-r--r--src/app/creation.rs18
-rw-r--r--src/app/ids.rs32
-rw-r--r--src/app/input/modal.rs10
-rw-r--r--src/app/mod.rs55
-rw-r--r--src/persist/restore.rs117
-rw-r--r--src/ui/mobile.rs68
-rw-r--r--src/workspace.rs4
-rw-r--r--src/workspace/aggregate.rs35
11 files changed, 315 insertions, 61 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index a7d99f35..99bd0ff6 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -31,6 +31,13 @@ fn is_completion_transition(change: &EffectiveStateChange) -> bool {
)
}
+fn public_tab_id_for_index(ws: &crate::workspace::Workspace, tab_idx: usize) -> Option<String> {
+ let tab_number = ws.public_tab_number(tab_idx)?;
+ Some(crate::workspace::public_tab_id_for_number(
+ &ws.id, tab_number,
+ ))
+}
+
pub fn is_completion_transition_parts(
previous_state: AgentState,
state: AgentState,
@@ -951,7 +958,8 @@ impl AppState {
if let Some(ws) = self.workspaces.get_mut(idx) {
let active_tab = ws.active_tab;
ws.switch_tab(active_tab);
- let tab_id = format!("{}:{}", workspace_id, active_tab + 1);
+ let tab_id =
+ public_tab_id_for_index(ws, active_tab).unwrap_or_else(|| workspace_id.clone());
crate::logging::tab_focused(&workspace_id, &tab_id);
}
self.tab_scroll_follow_active = true;
@@ -994,7 +1002,8 @@ impl AppState {
self.ensure_workspace_visible(ws_idx);
if let Some(ws) = self.workspaces.get_mut(ws_idx) {
ws.switch_tab(tab_idx);
- let tab_id = format!("{}:{}", workspace_id, tab_idx + 1);
+ let tab_id =
+ public_tab_id_for_index(ws, tab_idx).unwrap_or_else(|| workspace_id.clone());
crate::logging::tab_focused(&workspace_id, &tab_id);
}
self.tab_scroll_follow_active = true;
@@ -1092,7 +1101,7 @@ impl AppState {
};
ws.switch_tab(idx);
let workspace_id = ws.id.clone();
- let tab_id = format!("{}:{}", workspace_id, idx + 1);
+ let tab_id = public_tab_id_for_index(ws, idx).unwrap_or_else(|| workspace_id.clone());
crate::logging::tab_focused(&workspace_id, &tab_id);
self.mark_session_dirty();
self.tab_scroll_follow_active = true;
@@ -1853,7 +1862,8 @@ impl AppState {
return false;
};
let workspace_id = ws.id.clone();
- let closing_tab_id = format!("{}:{}", workspace_id, ws.active_tab + 1);
+ let closing_tab_id =
+ public_tab_id_for_index(ws, ws.active_tab).unwrap_or_else(|| workspace_id.clone());
ws.close_active_tab();
self.remove_plugin_pane_records(pane_ids);
self.remove_unattached_terminal_ids(terminal_ids);
diff --git a/src/app/api/layouts.rs b/src/app/api/layouts.rs
index e397ccf8..9c492937 100644
--- a/src/app/api/layouts.rs
+++ b/src/app/api/layouts.rs
@@ -151,10 +151,9 @@ impl App {
let closed_tab_id = self
.public_tab_id(target_ws_idx, target_tab_idx)
.unwrap_or_else(|| {
- format!(
- "{}:{}",
- self.public_workspace_id(target_ws_idx),
- target_tab_idx + 1
+ crate::workspace::public_tab_id_for_number(
+ &self.public_workspace_id(target_ws_idx),
+ target_tab_idx + 1,
)
});
let terminal_ids = self
diff --git a/src/app/api/tabs.rs b/src/app/api/tabs.rs
index c88a00d1..079c2ea4 100644
--- a/src/app/api/tabs.rs
+++ b/src/app/api/tabs.rs
@@ -105,11 +105,7 @@ impl App {
if let Some(label) = label {
let workspace_id = self.state.workspaces[ws_idx].id.clone();
let tab_id = self.public_tab_id(ws_idx, tab_idx).unwrap_or_else(|| {
- format!(
- "{}:t{}",
- workspace_id,
- crate::workspace::encode_public_number(tab_idx + 1)
- )
+ crate::workspace::public_tab_id_for_number(&workspace_id, tab_idx + 1)
});
if let Some(tab) = self
.state
@@ -165,9 +161,9 @@ impl App {
return tab_not_found(id, &params.tab_id);
};
let workspace_id = self.state.workspaces[ws_idx].id.clone();
- let tab_id = self
- .public_tab_id(ws_idx, tab_idx)
- .unwrap_or_else(|| format!("{}:{}", workspace_id, tab_idx + 1));
+ let tab_id = self.public_tab_id(ws_idx, tab_idx).unwrap_or_else(|| {
+ crate::workspace::public_tab_id_for_number(&workspace_id, tab_idx + 1)
+ });
let Some(tab) = self
.state
.workspaces
diff --git a/src/app/creation.rs b/src/app/creation.rs
index a45dae03..e53bce00 100644
--- a/src/app/creation.rs
+++ b/src/app/creation.rs
@@ -123,13 +123,9 @@ impl App {
self.state.mode = Mode::Terminal;
}
let workspace_id = self.state.workspaces[ws_idx].id.clone();
- let tab_id = self.public_tab_id(ws_idx, idx).unwrap_or_else(|| {
- format!(
- "{}:t{}",
- workspace_id,
- crate::workspace::encode_public_number(idx + 1)
- )
- });
+ let tab_id = self
+ .public_tab_id(ws_idx, idx)
+ .unwrap_or_else(|| crate::workspace::public_tab_id_for_number(&workspace_id, idx + 1));
let root_pane = self.state.workspaces[ws_idx].tabs[idx].root_pane.raw();
crate::logging::tab_created(&workspace_id, &tab_id, root_pane);
self.schedule_session_save();
@@ -240,7 +236,7 @@ impl App {
Some(crate::api::schema::TabInfo {
tab_id: self.public_tab_id(ws_idx, tab_idx)?,
workspace_id: self.public_workspace_id(ws_idx),
- number: tab_idx + 1,
+ number: tab.number,
label: tab.display_name(),
focused: self.state.active == Some(ws_idx) && ws.active_tab == tab_idx,
pane_count: tab.panes.len(),
@@ -349,9 +345,9 @@ impl App {
focused: self.state.active == Some(index),
pane_count: ws.public_pane_numbers.len(),
tab_count: ws.tabs.len(),
- active_tab_id: self
- .public_tab_id(index, ws.active_tab)
- .unwrap_or_else(|| format!("{}:{}", ws.id, ws.active_tab + 1)),
+ active_tab_id: self.public_tab_id(index, ws.active_tab).unwrap_or_else(|| {
+ crate::workspace::public_tab_id_for_number(&ws.id, ws.active_tab + 1)
+ }),
agent_status: pane_agent_status(agg_state, seen),
worktree: ws
.worktree_space()
diff --git a/src/app/ids.rs b/src/app/ids.rs
index 42cf47a8..9dc4371e 100644
--- a/src/app/ids.rs
+++ b/src/app/ids.rs
@@ -19,10 +19,8 @@ impl App {
pub(super) fn public_tab_id(&self, ws_idx: usize, tab_idx: usize) -> Option<String> {
let ws = self.state.workspaces.get(ws_idx)?;
let tab_number = ws.public_tab_number(tab_idx)?;
- Some(format!(
- "{}:t{}",
- ws.id,
- crate::workspace::encode_public_number(tab_number)
+ Some(crate::workspace::public_tab_id_for_number(
+ &ws.id, tab_number,
))
}
@@ -33,10 +31,9 @@ impl App {
) -> Option<String> {
let ws = self.state.workspaces.get(ws_idx)?;
let pane_number = ws.public_pane_number(pane_id)?;
- Some(format!(
- "{}:p{}",
- ws.id,
- crate::workspace::encode_public_number(pane_number)
+ Some(crate::workspace::public_pane_id_for_number(
+ &ws.id,
+ pane_number,
))
}
@@ -80,18 +77,17 @@ impl App {
let (ws_raw, tab_raw) = id.rsplit_once(':')?;
let ws_idx = self.parse_workspace_id(ws_raw)?;
- let tab_number = if let Some(encoded) = tab_raw.strip_prefix('t') {
- crate::workspace::decode_public_number(encoded)?
+ let tab_idx = if let Some(encoded) = tab_raw.strip_prefix('t') {
+ let tab_number = crate::workspace::decode_public_number(encoded)?;
+ self.state
+ .workspaces
+ .get(ws_idx)?
+ .tabs
+ .iter()
+ .position(|tab| tab.number == tab_number)?
} else {
- tab_raw.parse::<usize>().ok()?
+ tab_raw.parse::<usize>().ok()?.checked_sub(1)?
};
- let tab_idx = self
- .state
- .workspaces
- .get(ws_idx)?
- .tabs
- .iter()
- .position(|tab| tab.number == tab_number)?;
self.state.workspaces.get(ws_idx)?.tabs.get(tab_idx)?;
Some((ws_idx, tab_idx))
}
diff --git a/src/app/input/modal.rs b/src/app/input/modal.rs
index 3d106765..cff760bf 100644
--- a/src/app/input/modal.rs
+++ b/src/app/input/modal.rs
@@ -458,7 +458,15 @@ pub(super) fn apply_rename_action(state: &mut AppState, action: ModalAction) {
tab.is_auto_named() && new_name == tab.number.to_string();
if !new_name.is_empty() && !keep_auto_name {
tab.set_custom_name(new_name);
- let tab_id = format!("{}:{}", workspace_id, active_tab + 1);
+ let tab_id = ws
+ .public_tab_number(active_tab)
+ .map(|number| {
+ crate::workspace::public_tab_id_for_number(
+ &workspace_id,
+ number,
+ )
+ })
+ .unwrap_or_else(|| workspace_id.clone());
crate::logging::tab_renamed(&workspace_id, &tab_id);
state.mark_session_dirty();
}
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 356ecbab..36fedb71 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -2932,6 +2932,61 @@ mod tests {
}
#[test]
+ fn tab_info_number_uses_stable_public_tab_number() {
+ let mut app = test_app();
+ let mut workspace = Workspace::test_new("api-tab-public-number");
+ let removed_tab = workspace.test_add_tab(None);
+ let survivor_tab = workspace.test_add_tab(None);
+ let survivor_pane = workspace.tabs[survivor_tab].root_pane;
+ assert!(workspace.close_tab(removed_tab));
+ app.state.workspaces = vec![workspace];
+ app.state.ensure_test_terminals();
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ let survivor_idx = app.state.workspaces[0]
+ .find_tab_index_for_pane(survivor_pane)
+ .unwrap();
+
+ let tab = app.tab_info(0, survivor_idx).unwrap();
+
+ assert_eq!(tab.tab_id, format!("{}:t3", app.state.workspaces[0].id));
+ assert_eq!(tab.number, 3);
+ assert_eq!(tab.label, "3");
+ }
+
+ #[test]
+ fn legacy_bare_tab_id_uses_tab_position_not_public_tab_number() {
+ let mut app = test_app();
+ let mut workspace = Workspace::test_new("legacy-tab-id");
+ let removed_tab = workspace.test_add_tab(None);
+ workspace.test_add_tab(None);
+ let public_four_tab = workspace.test_add_tab(None);
+ let fourth_position_tab = workspace.test_add_tab(None);
+ let public_four_pane = workspace.tabs[public_four_tab].root_pane;
+ let fourth_position_pane = workspace.tabs[fourth_position_tab].root_pane;
+ assert!(workspace.close_tab(removed_tab));
+ app.state.workspaces = vec![workspace];
+
+ let public_four_idx = app.state.workspaces[0]
+ .find_tab_index_for_pane(public_four_pane)
+ .unwrap();
+ let fourth_position_idx = app.state.workspaces[0]
+ .find_tab_index_for_pane(fourth_position_pane)
+ .unwrap();
+
+ assert_eq!(app.state.workspaces[0].tabs[public_four_idx].number, 4);
+ assert_eq!(app.state.workspaces[0].tabs[fourth_position_idx].number, 5);
+ assert_eq!(
+ app.parse_tab_id(&format!("{}:t4", app.state.workspaces[0].id)),
+ Some((0, public_four_idx))
+ );
+ assert_eq!(
+ app.parse_tab_id(&format!("{}:4", app.state.workspaces[0].id)),
+ Some((0, fourth_position_idx))
+ );
+ }
+
+ #[test]
fn workspace_creation_in_navigate_mode_uses_selected_workspace_seed_cwd() {
let mut app = test_app();
let mut first = Workspace::test_new("herdr");
diff --git a/src/persist/restore.rs b/src/persist/restore.rs
index 229d565d..7c046698 100644
--- a/src/persist/restore.rs
+++ b/src/persist/restore.rs
@@ -509,11 +509,7 @@ fn restore_tab(
.map(|pane_id| {
PaneLaunchEnv::from_extra(Vec::new()).with_identity(
workspace_id.to_string(),
- format!(
- "{}:t{}",
- workspace_id,
- crate::workspace::encode_public_number(number)
- ),
+ crate::workspace::public_tab_id_for_number(workspace_id, number),
pane_id.to_string(),
)
})
@@ -1312,6 +1308,117 @@ mod tests {
assert_eq!(workspace.next_public_tab_number, 6);
}
+ #[tokio::test]
+ async fn restore_with_gapped_public_tab_numbers_keeps_agent_panel_tab_indices() {
+ let cwd = std::env::current_dir().unwrap();
+ let pane_snap = |id: &str| {
+ (
+ id.parse::<u32>().unwrap(),
+ super::super::snapshot::PaneSnapshot {
+ cwd: cwd.clone(),
+ label: None,
+ agent_name: None,
+ agent_session: None,
+ launch_argv: None,
+ },
+ )
+ };
+ let final_pane = super::super::snapshot::PaneSnapshot {
+ cwd: cwd.clone(),
+ label: Some("planner".into()),
+ agent_name: Some("planner".into()),
+ agent_session: Some(super::super::snapshot::PaneAgentSessionSnapshot {
+ source: "herdr:codex".into(),
+ agent: "codex".into(),
+ kind: crate::agent_resume::AgentSessionRefKind::Id,
+ value: "codex-session".into(),
+ }),
+ launch_argv: None,
+ };
+ let snapshot = SessionSnapshot {
+ version: super::super::snapshot::SNAPSHOT_VERSION,
+ workspaces: vec![WorkspaceSnapshot {
+ id: Some("w1".into()),
+ custom_name: None,
+ identity_cwd: cwd.clone(),
+ worktree_space: None,
+ public_pane_numbers: HashMap::from([(10, 1), (11, 2), (12, 3), (13, 4)]),
+ next_public_pane_number: 5,
+ public_tab_numbers: vec![1, 3, 4, 5],
+ next_public_tab_number: 6,
+ tabs: vec![
+ TabSnapshot {
+ custom_name: None,
+ layout: LayoutSnapshot::Pane(10),
+ panes: HashMap::from([pane_snap("10")]),
+ zoomed: false,
+ focused: Some(10),
+ root_pane: Some(10),
+ },
+ TabSnapshot {
+ custom_name: None,
+ layout: LayoutSnapshot::Pane(11),
+ panes: HashMap::from([pane_snap("11")]),
+ zoomed: false,
+ focused: Some(11),
+ root_pane: Some(11),
+ },
+ TabSnapshot {
+ custom_name: None,
+ layout: LayoutSnapshot::Pane(12),
+ panes: HashMap::from([pane_snap("12")]),
+ zoomed: false,
+ focused: Some(12),
+ root_pane: Some(12),
+ },
+ TabSnapshot {
+ custom_name: None,
+ layout: LayoutSnapshot::Pane(13),
+ panes: HashMap::from([(13, final_pane)]),
+ zoomed: false,
+ focused: Some(13),
+ root_pane: Some(13),
+ },
+ ],
+ active_tab: 3,
+ }],
+ active: Some(0),
+ selected: 0,
+ agent_panel_scope: Default::default(),
+ sidebar_width: None,
+ sidebar_section_split: None,
+ collapsed_space_keys: Default::default(),
+ };
+ let (events, _event_rx) = mpsc::channel(4);
+
+ let (workspaces, terminals, _runtimes) = restore(
+ &snapshot,
+ None,
+ 24,
+ 80,
+ 0,
+ test_restore_shell(),
+ crate::config::ShellModeConfig::NonLogin,
+ false,
+ events,
+ Arc::new(Notify::new()),
+ Arc::new(AtomicBool::new(false)),
+ );
+
+ let workspace = workspaces.first().expect("workspace should restore");
+ let agent_pane = workspace.tabs[3].root_pane;
+ let detail = workspace
+ .pane_details(&terminals)
+ .into_iter()
+ .find(|detail| detail.pane_id == agent_pane)
+ .expect("restored agent pane should be listed");
+
+ assert_eq!(workspace.active_tab, 3);
+ assert_eq!(workspace.tabs[3].number, 5);
+ assert_eq!(detail.tab_idx, 3);
+ assert_eq!(detail.agent_label, "planner");
+ }
+
#[test]
fn legacy_restore_precomputes_missing_public_pane_numbers() {
let cwd = std::env::current_dir().unwrap();
diff --git a/src/ui/mobile.rs b/src/ui/mobile.rs
index b8f23073..a9dc977f 100644
--- a/src/ui/mobile.rs
+++ b/src/ui/mobile.rs
@@ -290,7 +290,7 @@ fn render_header_status(
} else {
state_dot(state, seen, p)
};
- let tab_label = format!("tab {}/{}", ws.active_tab + 1, ws.tabs.len());
+ let tab_label = mobile_tab_status(ws);
let row1 = Rect::new(area.x, area.y, area.width, 1);
let tab_w = (tab_label.chars().count() as u16 + 1).min(area.width);
let name_w = area.width.saturating_sub(tab_w);
@@ -329,6 +329,17 @@ fn render_header_status(
}
}
+fn mobile_tab_status(ws: &crate::workspace::Workspace) -> String {
+ let tab_number = ws
+ .public_tab_number(ws.active_tab)
+ .unwrap_or(ws.active_tab + 1);
+ if ws.tabs.len() <= 1 {
+ format!("tab {tab_number}")
+ } else {
+ format!("tab {tab_number} · {}/{}", ws.active_tab + 1, ws.tabs.len())
+ }
+}
+
fn render_switch_button(app: &AppState, frame: &mut Frame, area: Rect) {
if area.width == 0 || area.height == 0 {
return;
@@ -471,10 +482,9 @@ fn render_mobile_switcher_content(
),
]);
let detail = format!(
- " {} · tab {}/{}",
+ " {} · {}",
ws.branch().unwrap_or_else(|| "shell".into()),
- ws.active_tab + 1,
- ws.tabs.len()
+ mobile_tab_status(ws)
);
render_two_line_item(
frame,
@@ -515,9 +525,9 @@ fn render_mobile_switcher_content(
let active = idx == ws.active_tab;
let bg = mobile_item_bg(false, active, p);
let label = if tab.is_auto_named() {
- format!("tab {}", idx + 1)
+ format!("tab {}", tab.display_name())
} else {
- format!("{} · {}", idx + 1, tab.display_name())
+ format!("{} · {}", tab.number, tab.display_name())
};
let title = Line::from(vec![
Span::styled(" ", Style::default().bg(bg)),
@@ -965,6 +975,52 @@ mod tests {
assert_eq!(mobile_agent_detail(&entry), " idle · pi");
}
+ #[test]
+ fn mobile_tab_status_uses_public_tab_number_and_position() {
+ let mut workspace = crate::workspace::Workspace::test_new("mobile-tabs");
+ let removed_tab = workspace.test_add_tab(None);
+ workspace.test_add_tab(None);
+ assert!(workspace.close_tab(removed_tab));
+ workspace.active_tab = 1;
+
+ assert_eq!(mobile_tab_status(&workspace), "tab 3 · 2/2");
+ }
+
+ #[test]
+ fn mobile_switcher_uses_stable_public_tab_number_for_auto_tab_labels() {
+ let mut app = crate::app::state::AppState::test_new();
+ let mut workspace = crate::workspace::Workspace::test_new("mobile-tabs");
+ let removed_tab = workspace.test_add_tab(None);
+ workspace.test_add_tab(None);
+ assert!(workspace.close_tab(removed_tab));
+ app.workspaces = vec![workspace];
+ app.ensure_test_terminals();
+ app.active = Some(0);
+ app.selected = 0;
+ app.view.mobile_header_rect = Rect::new(0, 0, 40, 2);
+ app.view.terminal_area = Rect::new(0, 2, 40, 18);
+
+ let backend = ratatui::backend::TestBackend::new(40, 20);
+ let mut terminal = ratatui::Terminal::new(backend).unwrap();
+ terminal
+ .draw(|frame| {
+ render_mobile_panel(
+ &app,
+ &TerminalRuntimeRegistry::new(),
+ frame,
+ Rect::new(0, 0, 40, 20),
+ )
+ })
+ .unwrap();
+
+ let row = (0..40)
+ .map(|x| terminal.backend().buffer()[(x, 10)].symbol())
+ .collect::<String>();
+
+ assert!(row.contains("tab 3"), "mobile tab row: {row:?}");
+ assert!(!row.contains("tab 2"), "mobile tab row: {row:?}");
+ }
+
#[cfg(unix)]
#[tokio::test]
async fn mobile_header_uses_live_root_runtime_cwd_for_workspace_label() {
diff --git a/src/workspace.rs b/src/workspace.rs
index c4c91785..f06fe8ab 100644
--- a/src/workspace.rs
+++ b/src/workspace.rs
@@ -109,11 +109,11 @@ pub(crate) fn public_workspace_number(id: &str) -> Option<usize> {
id.strip_prefix('w').and_then(decode_public_number)
}
-fn public_pane_id_for_number(workspace_id: &str, pane_number: usize) -> String {
+pub(crate) fn public_pane_id_for_number(workspace_id: &str, pane_number: usize) -> String {
format!("{workspace_id}:p{}", encode_public_number(pane_number))
}
-fn public_tab_id_for_number(workspace_id: &str, tab_number: usize) -> String {
+pub(crate) fn public_tab_id_for_number(workspace_id: &str, tab_number: usize) -> String {
format!("{workspace_id}:t{}", encode_public_number(tab_number))
}
diff --git a/src/workspace/aggregate.rs b/src/workspace/aggregate.rs
index c9c1266d..bf4c9408 100644
--- a/src/workspace/aggregate.rs
+++ b/src/workspace/aggregate.rs
@@ -48,7 +48,7 @@ impl Tab {
let presentation = terminal.effective_presentation();
Some(PaneDetail {
pane_id: *id,
- tab_idx: self.number.saturating_sub(1),
+ tab_idx: 0,
tab_label: self.display_name(),
label: agent_label.clone(),
agent_label,
@@ -98,7 +98,15 @@ impl Workspace {
let multi_tab = self.tabs.len() > 1;
self.tabs
.iter()
- .flat_map(|tab| tab.pane_details(terminals))
+ .enumerate()
+ .flat_map(|(tab_idx, tab)| {
+ tab.pane_details(terminals)
+ .into_iter()
+ .map(move |mut detail| {
+ detail.tab_idx = tab_idx;
+ detail
+ })
+ })
.map(|mut detail| {
if multi_tab {
detail.label = format!("{}·{}", detail.tab_label, detail.agent_label);
@@ -245,4 +253,27 @@ mod tests {
]
);
}
+
+ #[test]
+ fn pane_details_use_tab_vector_index_not_stable_public_tab_number() {
+ let mut ws = Workspace::test_new("test");
+ let removed_tab = ws.test_add_tab(Some("removed"));
+ let survivor_tab = ws.test_add_tab(Some("survivor"));
+ let survivor_pane = ws.tabs[survivor_tab].root_pane;
+ assert!(ws.close_tab(removed_tab));
+
+ let mut terminals = HashMap::new();
+ let mut terminal = terminal_for_pane(&ws, survivor_pane);
+ terminal.detected_agent = Some(Agent::Codex);
+ terminals.insert(terminal.id.clone(), terminal);
+
+ let details = ws.pane_details(&terminals);
+ let survivor = details
+ .iter()
+ .find(|detail| detail.pane_id == survivor_pane)
+ .expect("surviving tab agent should be listed");
+
+ assert_eq!(ws.tabs[1].number, 3);
+ assert_eq!(survivor.tab_idx, 1);
+ }
}