aboutsummaryrefslogtreecommitdiff
path: root/src/app
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/app
parent17544c36ee397233e2f7b3319d86e3b517e3b5d0 (diff)
fix: preserve tab identity across restored sessions
Diffstat (limited to 'src/app')
-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
7 files changed, 106 insertions, 46 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");