aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-12 19:07:07 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-12 19:10:09 +0300
commitf7a7da03fc27cc6fe4e6a44dabddcd303f939bb9 (patch)
tree69a00241e4500703bb723b64dd0060cc83730a0b /src/app
parent53429206b736aa0fcfb1c5bfc1c0b11dcca3a059 (diff)
fix: stabilize public pane and tab ids
refs #569
Diffstat (limited to 'src/app')
-rw-r--r--src/app/actions.rs1
-rw-r--r--src/app/agent_resume.rs4
-rw-r--r--src/app/api.rs4
-rw-r--r--src/app/api/panes.rs14
-rw-r--r--src/app/api/tabs.rs18
-rw-r--r--src/app/api/workspaces.rs5
-rw-r--r--src/app/creation.rs10
-rw-r--r--src/app/ids.rs38
-rw-r--r--src/app/input/modal.rs8
-rw-r--r--src/app/input/navigate.rs4
-rw-r--r--src/app/input/sidebar.rs5
-rw-r--r--src/app/mod.rs10
12 files changed, 90 insertions, 31 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index b218e53c..c2b35dbd 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -3162,6 +3162,7 @@ mod tests {
events,
std::sync::Arc::new(tokio::sync::Notify::new()),
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
+ None,
)
.unwrap();
diff --git a/src/app/agent_resume.rs b/src/app/agent_resume.rs
index 894b0b37..cf691e33 100644
--- a/src/app/agent_resume.rs
+++ b/src/app/agent_resume.rs
@@ -223,6 +223,9 @@ impl App {
);
return false;
};
+ let public_pane_id = self
+ .find_pane(pane_id)
+ .and_then(|(ws_idx, _)| self.public_pane_id(ws_idx, pane_id));
let runtime = match crate::terminal::TerminalRuntime::spawn(
pane_id,
@@ -235,6 +238,7 @@ impl App {
self.event_tx.clone(),
self.render_notify.clone(),
self.render_dirty.clone(),
+ public_pane_id.as_deref(),
) {
Ok(runtime) => runtime,
Err(err) => {
diff --git a/src/app/api.rs b/src/app/api.rs
index ea4ea378..32192a07 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -327,6 +327,7 @@ impl App {
let Some((ws_idx, pane_state)) = self.find_pane(pane_id) else {
return false;
};
+ let public_pane_id = self.public_pane_id(ws_idx, pane_id);
let terminal_id = pane_state.attached_terminal_id.clone();
let Some(terminal) = self.state.terminals.get(&terminal_id) else {
return false;
@@ -349,6 +350,7 @@ impl App {
self.event_tx.clone(),
self.render_notify.clone(),
self.render_dirty.clone(),
+ public_pane_id.as_deref(),
) {
Ok(runtime) => runtime,
Err(err) => {
@@ -1236,6 +1238,7 @@ mod tests {
events,
std::sync::Arc::new(tokio::sync::Notify::new()),
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
+ None,
)
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
@@ -1327,6 +1330,7 @@ mod tests {
events,
std::sync::Arc::new(tokio::sync::Notify::new()),
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
+ None,
)
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs
index 5afac850..326fbd9f 100644
--- a/src/app/api/panes.rs
+++ b/src/app/api/panes.rs
@@ -599,6 +599,9 @@ impl App {
let Some((ws_idx, pane_id)) = self.parse_pane_id(&params.pane_id) else {
return pane_not_found(id, &params.pane_id);
};
+ let Some(public_pane_id) = self.public_pane_id(ws_idx, pane_id) else {
+ return pane_not_found(id, &params.pane_id);
+ };
let Some((pane, workspace_id)) = self.lookup_runtime(ws_idx, pane_id) else {
return pane_not_found(id, &params.pane_id);
};
@@ -630,7 +633,7 @@ impl App {
id,
ResponseResult::PaneRead {
read: PaneReadResult {
- pane_id: params.pane_id,
+ pane_id: public_pane_id,
workspace_id,
tab_id: self.public_tab_id(ws_idx, tab_idx).unwrap(),
source: params.source,
@@ -887,6 +890,10 @@ impl App {
let Some((ws_idx, pane_id)) = self.parse_pane_id(&target.pane_id) else {
return pane_not_found(id, &target.pane_id);
};
+ let Some(public_pane_id) = self.public_pane_id(ws_idx, pane_id) else {
+ return pane_not_found(id, &target.pane_id);
+ };
+ let workspace_id = self.public_workspace_id(ws_idx);
if self.state.close_pane_would_close_workspace(ws_idx, pane_id)
&& self.state.confirm_implicit_worktree_group_close(ws_idx)
{
@@ -896,7 +903,6 @@ impl App {
"closing this pane would close a worktree group",
);
}
- let workspace_id = self.state.workspaces[ws_idx].id.clone();
let terminal_id = self.state.terminal_id_for_pane(ws_idx, pane_id);
let should_close_workspace = {
let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
@@ -911,7 +917,7 @@ impl App {
self.emit_event(EventEnvelope {
event: EventKind::PaneClosed,
data: EventData::PaneClosed {
- pane_id: target.pane_id.clone(),
+ pane_id: public_pane_id,
workspace_id: workspace_id.clone(),
},
});
@@ -926,7 +932,7 @@ impl App {
self.emit_event(EventEnvelope {
event: EventKind::PaneClosed,
data: EventData::PaneClosed {
- pane_id: target.pane_id,
+ pane_id: public_pane_id,
workspace_id,
},
});
diff --git a/src/app/api/tabs.rs b/src/app/api/tabs.rs
index 4057eced..7c7ad9d7 100644
--- a/src/app/api/tabs.rs
+++ b/src/app/api/tabs.rs
@@ -98,9 +98,13 @@ 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!("{}:{}", workspace_id, tab_idx + 1));
+ 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)
+ )
+ });
if let Some(tab) = self
.state
.workspaces
@@ -186,6 +190,10 @@ impl App {
let Some((ws_idx, tab_idx)) = self.parse_tab_id(&target.tab_id) else {
return tab_not_found(id, &target.tab_id);
};
+ let Some(tab_id) = self.public_tab_id(ws_idx, tab_idx) else {
+ return tab_not_found(id, &target.tab_id);
+ };
+ let workspace_id = self.public_workspace_id(ws_idx);
let terminal_ids = self.state.terminal_ids_for_tab(ws_idx, tab_idx);
let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
return tab_not_found(id, &target.tab_id);
@@ -210,8 +218,8 @@ impl App {
self.emit_event(EventEnvelope {
event: EventKind::TabClosed,
data: EventData::TabClosed {
- tab_id: target.tab_id,
- workspace_id: self.public_workspace_id(ws_idx),
+ tab_id,
+ workspace_id,
},
});
diff --git a/src/app/api/workspaces.rs b/src/app/api/workspaces.rs
index 92b41486..e1966adf 100644
--- a/src/app/api/workspaces.rs
+++ b/src/app/api/workspaces.rs
@@ -146,14 +146,13 @@ impl App {
if self.state.workspaces.get(index).is_none() {
return workspace_not_found(id, &target.workspace_id);
}
+ let workspace_id = self.public_workspace_id(index);
self.state.selected = index;
self.state.close_selected_workspace();
self.shutdown_detached_terminal_runtimes();
self.emit_event(EventEnvelope {
event: EventKind::WorkspaceClosed,
- data: EventData::WorkspaceClosed {
- workspace_id: target.workspace_id,
- },
+ data: EventData::WorkspaceClosed { workspace_id },
});
encode_success(id, ResponseResult::Ok {})
diff --git a/src/app/creation.rs b/src/app/creation.rs
index 3f643986..7be324e5 100644
--- a/src/app/creation.rs
+++ b/src/app/creation.rs
@@ -122,9 +122,13 @@ 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!("{}:{}", workspace_id, idx + 1));
+ 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 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();
diff --git a/src/app/ids.rs b/src/app/ids.rs
index a18efd2d..0ce02c1f 100644
--- a/src/app/ids.rs
+++ b/src/app/ids.rs
@@ -18,8 +18,12 @@ 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)?;
- ws.tabs.get(tab_idx)?;
- Some(format!("{}:{}", ws.id, tab_idx + 1))
+ let tab_number = ws.public_tab_number(tab_idx)?;
+ Some(format!(
+ "{}:t{}",
+ ws.id,
+ crate::workspace::encode_public_number(tab_number)
+ ))
}
pub(super) fn public_pane_id(
@@ -29,7 +33,11 @@ impl App {
) -> Option<String> {
let ws = self.state.workspaces.get(ws_idx)?;
let pane_number = ws.public_pane_number(pane_id)?;
- Some(format!("{}-{pane_number}", ws.id))
+ Some(format!(
+ "{}:p{}",
+ ws.id,
+ crate::workspace::encode_public_number(pane_number)
+ ))
}
pub(super) fn parse_workspace_id(&self, id: &str) -> Option<usize> {
@@ -52,7 +60,18 @@ impl App {
let (ws_raw, tab_raw) = id.rsplit_once(':')?;
let ws_idx = self.parse_workspace_id(ws_raw)?;
- let tab_idx = tab_raw.parse::<usize>().ok()?.checked_sub(1)?;
+ let tab_number = if let Some(encoded) = tab_raw.strip_prefix('t') {
+ crate::workspace::decode_public_number(encoded)?
+ } else {
+ tab_raw.parse::<usize>().ok()?
+ };
+ 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))
}
@@ -81,6 +100,17 @@ impl App {
return self.find_pane(pane_id).map(|(ws_idx, _)| (ws_idx, pane_id));
}
+ if let Some((ws_raw, pane_number_raw)) = id.rsplit_once(":p") {
+ let ws_idx = self.parse_workspace_id(ws_raw)?;
+ let pane_number = crate::workspace::decode_public_number(pane_number_raw)?;
+ let ws = self.state.workspaces.get(ws_idx)?;
+ let pane_id = ws
+ .public_pane_numbers
+ .iter()
+ .find_map(|(pane_id, number)| (*number == pane_number).then_some(*pane_id))?;
+ return Some((ws_idx, pane_id));
+ }
+
let (ws_raw, pane_number_raw) = id.rsplit_once('-')?;
let ws_idx = self.parse_workspace_id(ws_raw)?;
let pane_number = pane_number_raw.parse::<usize>().ok()?;
diff --git a/src/app/input/modal.rs b/src/app/input/modal.rs
index 79d066f9..3d106765 100644
--- a/src/app/input/modal.rs
+++ b/src/app/input/modal.rs
@@ -355,7 +355,7 @@ fn next_new_tab_default_name(state: &AppState) -> String {
state
.active
.and_then(|i| state.workspaces.get(i))
- .map(|ws| (ws.tabs.len() + 1).to_string())
+ .map(|ws| ws.next_public_tab_number.to_string())
.unwrap_or_else(|| "1".to_string())
}
@@ -1288,7 +1288,7 @@ mod tests {
}
#[test]
- fn closing_first_auto_tab_resets_remaining_auto_tab_and_next_prompt() {
+ fn closing_first_auto_tab_keeps_remaining_auto_tab_number_and_next_prompt() {
let mut state = state_with_workspaces(&["test"]);
open_new_tab_dialog(&mut state);
handle_rename_key(
@@ -1303,11 +1303,11 @@ mod tests {
state.workspaces[0].close_tab(0);
state.workspaces[0].switch_tab(0);
- assert_eq!(state.workspaces[0].tabs[0].display_name(), "1");
+ assert_eq!(state.workspaces[0].tabs[0].display_name(), "2");
assert!(state.workspaces[0].tabs[0].custom_name.is_none());
open_new_tab_dialog(&mut state);
- assert_eq!(state.name_input, "2");
+ assert_eq!(state.name_input, "3");
}
#[test]
diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs
index 9d861f66..b27b5cb9 100644
--- a/src/app/input/navigate.rs
+++ b/src/app/input/navigate.rs
@@ -1943,8 +1943,8 @@ last_pane = "prefix+tab"
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], app.state.workspaces[0].id);
- assert_eq!(lines[1], format!("{}:1", app.state.workspaces[0].id));
- assert_eq!(lines[2], format!("{}-1", app.state.workspaces[0].id));
+ assert_eq!(lines[1], format!("{}:t1", app.state.workspaces[0].id));
+ assert_eq!(lines[2], format!("{}:p1", app.state.workspaces[0].id));
assert_eq!(app.state.mode, Mode::Terminal);
let _ = std::fs::remove_file(output_path);
diff --git a/src/app/input/sidebar.rs b/src/app/input/sidebar.rs
index 43093a82..c4bd52d9 100644
--- a/src/app/input/sidebar.rs
+++ b/src/app/input/sidebar.rs
@@ -1300,13 +1300,16 @@ mod tests {
.iter()
.map(|tab| tab.display_name())
.collect();
- assert_eq!(labels, vec!["foo", "2", "3"]);
+ assert_eq!(labels, vec!["foo", "3", "1"]);
assert_eq!(
app.state.workspaces[0].tabs[0].custom_name.as_deref(),
Some("foo")
);
assert!(app.state.workspaces[0].tabs[1].custom_name.is_none());
assert!(app.state.workspaces[0].tabs[2].custom_name.is_none());
+ assert_eq!(app.state.workspaces[0].tabs[0].number, 2);
+ assert_eq!(app.state.workspaces[0].tabs[1].number, 3);
+ assert_eq!(app.state.workspaces[0].tabs[2].number, 1);
assert_eq!(app.state.workspaces[0].tabs[2].root_pane, moved_root);
assert_eq!(app.state.workspaces[0].active_tab, 2);
}
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 99f4e461..5d09800b 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -2799,14 +2799,14 @@ mod tests {
id: "req_2".into(),
method: crate::api::schema::Method::WorkspaceFocus(
crate::api::schema::WorkspaceTarget {
- workspace_id: "w_1".into(),
+ workspace_id: "w1".into(),
},
),
};
let pane_rename = crate::api::schema::Request {
id: "req_3".into(),
method: crate::api::schema::Method::PaneRename(crate::api::schema::PaneRenameParams {
- pane_id: "w_1-1".into(),
+ pane_id: "w1:p1".into(),
label: Some("logs".into()),
}),
};
@@ -2825,7 +2825,7 @@ mod tests {
let pane_swap = crate::api::schema::Request {
id: "req_6".into(),
method: crate::api::schema::Method::PaneSwap(crate::api::schema::PaneSwapParams {
- pane_id: Some("w_1-1".into()),
+ pane_id: Some("w1:p1".into()),
direction: Some(crate::api::schema::PaneDirection::Right),
..crate::api::schema::PaneSwapParams::default()
}),
@@ -2834,7 +2834,7 @@ mod tests {
id: "req_7".into(),
method: crate::api::schema::Method::PaneFocusDirection(
crate::api::schema::PaneFocusDirectionParams {
- pane_id: Some("w_1-1".into()),
+ pane_id: Some("w1:p1".into()),
direction: crate::api::schema::PaneDirection::Right,
},
),
@@ -2842,7 +2842,7 @@ mod tests {
let pane_resize = crate::api::schema::Request {
id: "req_8".into(),
method: crate::api::schema::Method::PaneResize(crate::api::schema::PaneResizeParams {
- pane_id: Some("w_1-1".into()),
+ pane_id: Some("w1:p1".into()),
direction: crate::api::schema::PaneDirection::Right,
amount: Some(0.05),
}),