aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-04 01:50:34 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-04 01:50:34 +0300
commit831894facfc1339f819b42c3339668154121d24f (patch)
tree3a49bad36a65341f6296b9f3d568317e15b6c21a
parente0fb8ca6fa55acaa72902a15c5468cbe2787ddf7 (diff)
refactor: make workspace api ids stable
-rw-r--r--SOCKET_API.md50
-rw-r--r--src/app/mod.rs39
-rw-r--r--src/persist.rs13
-rw-r--r--src/workspace.rs18
-rw-r--r--tests/api_ping.rs117
-rw-r--r--tests/cli_wrapper.rs116
6 files changed, 248 insertions, 105 deletions
diff --git a/SOCKET_API.md b/SOCKET_API.md
index 53fa9d52..24b3662e 100644
--- a/SOCKET_API.md
+++ b/SOCKET_API.md
@@ -69,30 +69,32 @@ errors look like:
## ids and numbering
-workspace ids look like:
+workspace ids are opaque, stable ids like:
-- `1`
-- `2`
+- `w64e95948145ed1`
+- `w64e95948146a82`
-pane ids look like:
+pane ids are workspace-scoped and stable across workspace reorder:
-- `1-1`
-- `1-2`
-- `2-1`
+- `w64e95948145ed1-1`
+- `w64e95948145ed1-2`
+- `w64e95948146a82-1`
that means:
-- first number = current workspace number
-- second number = current pane number within that workspace
+- workspace id = stable workspace identity
+- pane number = compact pane number within that workspace
-these are compact public ids for the current live session. they are **not durable database ids**. if a workspace or pane closes, higher numbers compact down.
+workspace ids are durable for the life of the workspace and survive display reordering. pane numbers are still compact public numbers, so if a pane closes, higher pane numbers in that same workspace compact down.
tabs are first-class socket api objects now.
-- tab ids look like `1:1`, `1:2`, `2:1`
-- first number = workspace number
-- second number = tab number within that workspace
-- pane ids still stay workspace-scoped like `1-2` rather than becoming `workspace-tab-pane` triples
+- tab ids look like `w64e95948145ed1:1`, `w64e95948145ed1:2`
+- workspace id = stable workspace identity
+- tab number = tab number within that workspace
+- pane ids still stay workspace-scoped like `w64e95948145ed1-2` rather than becoming `workspace-tab-pane` triples
+
+for backward compatibility, requests also accept the older positional forms like `1`, `1:2`, and `1-2` as shorthand for the current session order. responses use the stable ids.
## core objects
@@ -100,13 +102,13 @@ tabs are first-class socket api objects now.
```json
{
- "workspace_id": "1",
+ "workspace_id": "w64e95948145ed1",
"number": 1,
"label": "herdr",
"focused": true,
"pane_count": 1,
"tab_count": 1,
- "active_tab_id": "1:1",
+ "active_tab_id": "w64e95948145ed1:1",
"agent_state": "unknown"
}
```
@@ -115,8 +117,8 @@ tabs are first-class socket api objects now.
```json
{
- "tab_id": "1:1",
- "workspace_id": "1",
+ "tab_id": "w64e95948145ed1:1",
+ "workspace_id": "w64e95948145ed1",
"number": 1,
"label": "1",
"focused": true,
@@ -129,9 +131,9 @@ tabs are first-class socket api objects now.
```json
{
- "pane_id": "1-1",
- "workspace_id": "1",
- "tab_id": "1:1",
+ "pane_id": "w64e95948145ed1-1",
+ "workspace_id": "w64e95948145ed1",
+ "tab_id": "w64e95948145ed1:1",
"focused": true,
"cwd": "/home/can/Projects/herdr",
"agent": "pi",
@@ -144,9 +146,9 @@ tabs are first-class socket api objects now.
```json
{
- "pane_id": "1-1",
- "workspace_id": "1",
- "tab_id": "1:1",
+ "pane_id": "w64e95948145ed1-1",
+ "workspace_id": "w64e95948145ed1",
+ "tab_id": "w64e95948145ed1:1",
"source": "recent",
"text": "...",
"revision": 0,
diff --git a/src/app/mod.rs b/src/app/mod.rs
index a6f18177..c78d1c74 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -780,38 +780,41 @@ impl App {
}
fn public_workspace_id(&self, ws_idx: usize) -> String {
- (ws_idx + 1).to_string()
+ self.state.workspaces[ws_idx].id.clone()
}
fn public_tab_id(&self, ws_idx: usize, tab_idx: usize) -> Option<String> {
- self.state.workspaces.get(ws_idx)?.tabs.get(tab_idx)?;
- Some(format!("{}:{}", ws_idx + 1, tab_idx + 1))
+ let ws = self.state.workspaces.get(ws_idx)?;
+ ws.tabs.get(tab_idx)?;
+ Some(format!("{}:{}", ws.id, tab_idx + 1))
}
fn public_pane_id(&self, ws_idx: usize, pane_id: crate::layout::PaneId) -> Option<String> {
let ws = self.state.workspaces.get(ws_idx)?;
let pane_number = ws.public_pane_number(pane_id)?;
- Some(format!("{}-{pane_number}", ws_idx + 1))
+ Some(format!("{}-{pane_number}", ws.id))
}
fn parse_workspace_id(&self, id: &str) -> Option<usize> {
- if let Some(raw) = id.strip_prefix("w_") {
- return raw.parse::<usize>().ok()?.checked_sub(1);
- }
- id.parse::<usize>().ok()?.checked_sub(1)
+ self.state
+ .workspaces
+ .iter()
+ .position(|workspace| workspace.id == id)
+ .or_else(|| id.strip_prefix("w_")?.parse::<usize>().ok()?.checked_sub(1))
+ .or_else(|| id.parse::<usize>().ok()?.checked_sub(1))
}
fn parse_tab_id(&self, id: &str) -> Option<(usize, usize)> {
if let Some(rest) = id.strip_prefix("t_") {
- let (ws_raw, tab_raw) = rest.split_once('_')?;
- let ws_idx = ws_raw.parse::<usize>().ok()?.checked_sub(1)?;
+ let (ws_raw, tab_raw) = rest.rsplit_once('_')?;
+ let ws_idx = self.parse_workspace_id(ws_raw)?;
let tab_idx = tab_raw.parse::<usize>().ok()?.checked_sub(1)?;
self.state.workspaces.get(ws_idx)?.tabs.get(tab_idx)?;
return Some((ws_idx, tab_idx));
}
- let (ws_raw, tab_raw) = id.split_once(':')?;
- let ws_idx = ws_raw.parse::<usize>().ok()?.checked_sub(1)?;
+ 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)?;
self.state.workspaces.get(ws_idx)?.tabs.get(tab_idx)?;
Some((ws_idx, tab_idx))
@@ -819,8 +822,8 @@ impl App {
fn parse_pane_id(&self, id: &str) -> Option<(usize, crate::layout::PaneId)> {
if let Some(rest) = id.strip_prefix("p_") {
- if let Some((ws_raw, pane_raw)) = rest.split_once('_') {
- let ws_idx = ws_raw.parse::<usize>().ok()?.checked_sub(1)?;
+ if let Some((ws_raw, pane_raw)) = rest.rsplit_once('_') {
+ let ws_idx = self.parse_workspace_id(ws_raw)?;
let pane_id = crate::layout::PaneId::from_raw(pane_raw.parse::<u32>().ok()?);
return Some((ws_idx, pane_id));
}
@@ -829,8 +832,8 @@ impl App {
return self.find_pane(pane_id).map(|(ws_idx, _)| (ws_idx, pane_id));
}
- let (ws_raw, pane_number_raw) = id.split_once('-')?;
- let ws_idx = ws_raw.parse::<usize>().ok()?.checked_sub(1)?;
+ 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()?;
let ws = self.state.workspaces.get(ws_idx)?;
let pane_id = ws
@@ -1603,6 +1606,7 @@ impl App {
})
.unwrap();
};
+ let workspace_id = self.state.workspaces[ws_idx].id.clone();
let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
return serde_json::to_string(&ErrorResponse {
id: request.id,
@@ -1613,7 +1617,6 @@ impl App {
})
.unwrap();
};
- let workspace_id = format!("w_{}", ws_idx + 1);
let pane_count = ws.layout.pane_count();
if pane_count <= 1 {
self.state.selected = ws_idx;
@@ -2039,7 +2042,7 @@ impl App {
tab_count: ws.tabs.len(),
active_tab_id: self
.public_tab_id(index, ws.active_tab)
- .unwrap_or_else(|| format!("{}:{}", index + 1, ws.active_tab + 1)),
+ .unwrap_or_else(|| format!("{}:{}", ws.id, ws.active_tab + 1)),
agent_state: pane_agent_state(agg_state),
}
}
diff --git a/src/persist.rs b/src/persist.rs
index fb8ab2d8..3b181b06 100644
--- a/src/persist.rs
+++ b/src/persist.rs
@@ -35,6 +35,8 @@ pub struct SessionSnapshot {
#[derive(Serialize)]
pub struct WorkspaceSnapshot {
#[serde(default)]
+ pub id: Option<String>,
+ #[serde(default)]
pub custom_name: Option<String>,
pub identity_cwd: PathBuf,
pub tabs: Vec<TabSnapshot>,
@@ -45,6 +47,8 @@ pub struct WorkspaceSnapshot {
#[derive(Deserialize)]
struct WorkspaceSnapshotWire {
#[serde(default)]
+ id: Option<String>,
+ #[serde(default)]
custom_name: Option<String>,
#[serde(default)]
identity_cwd: Option<PathBuf>,
@@ -126,6 +130,7 @@ impl From<LegacyWorkspaceSnapshot> for WorkspaceSnapshot {
};
Self {
+ id: None,
custom_name: snap.custom_name,
identity_cwd,
tabs: vec![tab],
@@ -143,6 +148,7 @@ impl<'de> Deserialize<'de> for WorkspaceSnapshot {
if let Some(identity_cwd) = snap.identity_cwd {
return Ok(Self {
+ id: snap.id,
custom_name: snap.custom_name,
identity_cwd,
tabs: snap.tabs,
@@ -220,6 +226,7 @@ pub fn capture(
fn capture_workspace(ws: &Workspace) -> WorkspaceSnapshot {
WorkspaceSnapshot {
+ id: Some(ws.id.clone()),
custom_name: ws.custom_name.clone(),
identity_cwd: ws
.resolved_identity_cwd()
@@ -328,6 +335,10 @@ fn restore_workspace(
}
Some(Workspace {
+ id: snap
+ .id
+ .clone()
+ .unwrap_or_else(crate::workspace::generate_workspace_id),
custom_name: snap.custom_name.clone(),
identity_cwd: snap.identity_cwd.clone(),
cached_git_ahead_behind: None,
@@ -592,6 +603,7 @@ mod tests {
let snap = SessionSnapshot {
workspaces: vec![WorkspaceSnapshot {
+ id: Some("wproj".to_string()),
custom_name: Some("pi-mono".to_string()),
identity_cwd: PathBuf::from("/home/can/Projects/herdr"),
tabs: vec![TabSnapshot {
@@ -618,6 +630,7 @@ mod tests {
let restored: SessionSnapshot = serde_json::from_str(&json).unwrap();
assert_eq!(restored.workspaces.len(), 1);
+ assert_eq!(restored.workspaces[0].id.as_deref(), Some("wproj"));
assert_eq!(
restored.workspaces[0].custom_name.as_deref(),
Some("pi-mono")
diff --git a/src/workspace.rs b/src/workspace.rs
index 4090593c..18825cb9 100644
--- a/src/workspace.rs
+++ b/src/workspace.rs
@@ -1,8 +1,9 @@
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
-use std::sync::atomic::AtomicBool;
+use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
+use std::time::{SystemTime, UNIX_EPOCH};
use ratatui::layout::Direction;
use tokio::sync::{mpsc, Notify};
@@ -13,6 +14,17 @@ use crate::events::AppEvent;
use crate::layout::{PaneId, TileLayout};
use crate::pane::{PaneRuntime, PaneState};
+static NEXT_WORKSPACE_ID: AtomicU64 = AtomicU64::new(1);
+
+pub(crate) fn generate_workspace_id() -> String {
+ let micros = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|duration| duration.as_micros())
+ .unwrap_or(0);
+ let counter = NEXT_WORKSPACE_ID.fetch_add(1, Ordering::Relaxed);
+ format!("w{micros:x}{counter:x}")
+}
+
pub struct Tab {
pub custom_name: Option<String>,
pub number: usize,
@@ -228,6 +240,8 @@ impl Tab {
/// A named workspace containing tabs.
pub struct Workspace {
+ /// Stable public workspace identity, independent of display order.
+ pub id: String,
/// User-provided override. If set, auto-derived identity stops updating.
pub custom_name: Option<String>,
/// Fallback workspace identity source for tests, old snapshots, or missing runtimes.
@@ -280,6 +294,7 @@ impl Workspace {
public_pane_numbers.insert(tab.root_pane, 1);
info!(root_pane = tab.root_pane.raw(), "workspace created");
Ok(Self {
+ id: generate_workspace_id(),
custom_name: None,
identity_cwd: initial_cwd,
cached_git_ahead_behind: None,
@@ -712,6 +727,7 @@ impl Workspace {
let mut public_pane_numbers = HashMap::new();
public_pane_numbers.insert(tab.root_pane, 1);
Self {
+ id: generate_workspace_id(),
custom_name: Some(name.to_string()),
identity_cwd,
cached_git_ahead_behind: None,
diff --git a/tests/api_ping.rs b/tests/api_ping.rs
index 94e1e3f9..fd113917 100644
--- a/tests/api_ping.rs
+++ b/tests/api_ping.rs
@@ -227,11 +227,18 @@ fn workspace_list_and_create_round_trip() {
);
assert_eq!(created["id"], "req_3");
assert_eq!(created["result"]["type"], "workspace_info");
- assert_eq!(created["result"]["workspace"]["workspace_id"], "1");
+ let workspace_id = created["result"]["workspace"]["workspace_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
+ let active_tab_id = created["result"]["workspace"]["active_tab_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
assert_eq!(created["result"]["workspace"]["number"], 1);
assert_eq!(created["result"]["workspace"]["focused"], true);
assert_eq!(created["result"]["workspace"]["tab_count"], 1);
- assert_eq!(created["result"]["workspace"]["active_tab_id"], "1:1");
+ assert_eq!(active_tab_id, format!("{workspace_id}:1"));
let listed = send_request(
&socket_path,
@@ -239,13 +246,16 @@ fn workspace_list_and_create_round_trip() {
);
let workspaces = listed["result"]["workspaces"].as_array().unwrap();
assert_eq!(workspaces.len(), 1);
- assert_eq!(workspaces[0]["workspace_id"], "1");
+ assert_eq!(workspaces[0]["workspace_id"], workspace_id);
let fetched = send_request(
&socket_path,
- r#"{"id":"req_5","method":"workspace.get","params":{"workspace_id":"1"}}"#,
+ &format!(
+ r#"{{"id":"req_5","method":"workspace.get","params":{{"workspace_id":"{}"}}}}"#,
+ workspace_id
+ ),
);
- assert_eq!(fetched["result"]["workspace"]["workspace_id"], "1");
+ assert_eq!(fetched["result"]["workspace"]["workspace_id"], workspace_id);
let panes = send_request(
&socket_path,
@@ -253,8 +263,8 @@ fn workspace_list_and_create_round_trip() {
);
let panes = panes["result"]["panes"].as_array().unwrap();
assert_eq!(panes.len(), 1);
- assert_eq!(panes[0]["workspace_id"], "1");
- assert_eq!(panes[0]["tab_id"], "1:1");
+ assert_eq!(panes[0]["workspace_id"], workspace_id);
+ assert_eq!(panes[0]["tab_id"], active_tab_id);
let pane_id = panes[0]["pane_id"].as_str().unwrap().to_string();
let pane = send_request(
@@ -274,7 +284,7 @@ fn workspace_list_and_create_round_trip() {
),
);
assert_eq!(read["result"]["read"]["pane_id"], pane_id);
- assert_eq!(read["result"]["read"]["tab_id"], "1:1");
+ assert_eq!(read["result"]["read"]["tab_id"], active_tab_id);
assert!(read["result"]["read"]["text"].is_string());
let send_text = send_request(
@@ -368,47 +378,77 @@ fn tab_methods_round_trip_over_socket() {
base.display()
),
);
- assert_eq!(created["result"]["workspace"]["active_tab_id"], "1:1");
+ let workspace_id = created["result"]["workspace"]["workspace_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
+ let first_tab_id = created["result"]["workspace"]["active_tab_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
+ assert_eq!(first_tab_id, format!("{workspace_id}:1"));
let tab_created = send_request(
&socket_path,
- r#"{"id":"req_t2","method":"tab.create","params":{"workspace_id":"1","focus":true}}"#,
+ &format!(
+ r#"{{"id":"req_t2","method":"tab.create","params":{{"workspace_id":"{}","focus":true}}}}"#,
+ workspace_id
+ ),
);
assert_eq!(tab_created["result"]["type"], "tab_info");
- assert_eq!(tab_created["result"]["tab"]["tab_id"], "1:2");
+ let second_tab_id = tab_created["result"]["tab"]["tab_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
+ assert_eq!(second_tab_id, format!("{workspace_id}:2"));
assert_eq!(tab_created["result"]["tab"]["focused"], true);
let tab_list = send_request(
&socket_path,
- r#"{"id":"req_t3","method":"tab.list","params":{"workspace_id":"1"}}"#,
+ &format!(
+ r#"{{"id":"req_t3","method":"tab.list","params":{{"workspace_id":"{}"}}}}"#,
+ workspace_id
+ ),
);
let tabs = tab_list["result"]["tabs"].as_array().unwrap();
assert_eq!(tabs.len(), 2);
- assert_eq!(tabs[0]["tab_id"], "1:1");
- assert_eq!(tabs[1]["tab_id"], "1:2");
+ assert_eq!(tabs[0]["tab_id"], first_tab_id);
+ assert_eq!(tabs[1]["tab_id"], second_tab_id);
let tab_get = send_request(
&socket_path,
- r#"{"id":"req_t4","method":"tab.get","params":{"tab_id":"1:2"}}"#,
+ &format!(
+ r#"{{"id":"req_t4","method":"tab.get","params":{{"tab_id":"{}"}}}}"#,
+ second_tab_id
+ ),
);
- assert_eq!(tab_get["result"]["tab"]["tab_id"], "1:2");
+ assert_eq!(tab_get["result"]["tab"]["tab_id"], second_tab_id);
let renamed = send_request(
&socket_path,
- r#"{"id":"req_t5","method":"tab.rename","params":{"tab_id":"1:2","label":"logs"}}"#,
+ &format!(
+ r#"{{"id":"req_t5","method":"tab.rename","params":{{"tab_id":"{}","label":"logs"}}}}"#,
+ second_tab_id
+ ),
);
assert_eq!(renamed["result"]["tab"]["label"], "logs");
let focused = send_request(
&socket_path,
- r#"{"id":"req_t6","method":"tab.focus","params":{"tab_id":"1:1"}}"#,
+ &format!(
+ r#"{{"id":"req_t6","method":"tab.focus","params":{{"tab_id":"{}"}}}}"#,
+ first_tab_id
+ ),
);
- assert_eq!(focused["result"]["tab"]["tab_id"], "1:1");
+ assert_eq!(focused["result"]["tab"]["tab_id"], first_tab_id);
assert_eq!(focused["result"]["tab"]["focused"], true);
let closed = send_request(
&socket_path,
- r#"{"id":"req_t7","method":"tab.close","params":{"tab_id":"1:2"}}"#,
+ &format!(
+ r#"{{"id":"req_t7","method":"tab.close","params":{{"tab_id":"{}"}}}}"#,
+ second_tab_id
+ ),
);
assert_eq!(closed["result"]["type"], "ok");
@@ -480,10 +520,11 @@ fn events_subscribe_streams_lifecycle_and_agent_events() {
let workspace_focused =
wait_for_event(&mut reader, "workspace_focused", Duration::from_secs(2));
assert_eq!(workspace_focused["data"]["workspace_id"], workspace_id);
+ let first_tab_id = format!("{workspace_id}:1");
let tab_created = wait_for_event(&mut reader, "tab_created", Duration::from_secs(2));
- assert_eq!(tab_created["data"]["tab"]["tab_id"], "1:1");
+ assert_eq!(tab_created["data"]["tab"]["tab_id"], first_tab_id);
let tab_focused = wait_for_event(&mut reader, "tab_focused", Duration::from_secs(2));
- assert_eq!(tab_focused["data"]["tab_id"], "1:1");
+ assert_eq!(tab_focused["data"]["tab_id"], first_tab_id);
let pane_created = wait_for_event(&mut reader, "pane_created", Duration::from_secs(2));
let pane_id = pane_created["data"]["pane"]["pane_id"]
.as_str()
@@ -520,24 +561,35 @@ fn events_subscribe_streams_lifecycle_and_agent_events() {
workspace_id
),
);
- assert_eq!(new_tab["result"]["tab"]["tab_id"], "1:2");
+ let second_tab_id = new_tab["result"]["tab"]["tab_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
+ assert_eq!(second_tab_id, format!("{workspace_id}:2"));
let created_tab_event = wait_for_event(&mut reader, "tab_created", Duration::from_secs(2));
- assert_eq!(created_tab_event["data"]["tab"]["tab_id"], "1:2");
+ assert_eq!(created_tab_event["data"]["tab"]["tab_id"], second_tab_id);
let focused_tab_event = wait_for_event(&mut reader, "tab_focused", Duration::from_secs(2));
- assert_eq!(focused_tab_event["data"]["tab_id"], "1:2");
+ assert_eq!(focused_tab_event["data"]["tab_id"], second_tab_id);
let renamed_tab = send_request(
&socket_path,
- r#"{"id":"req_l5","method":"tab.rename","params":{"tab_id":"1:2","label":"logs"}}"#,
+ &format!(
+ r#"{{"id":"req_l5","method":"tab.rename","params":{{"tab_id":"{}","label":"logs"}}}}"#,
+ second_tab_id
+ ),
);
assert_eq!(renamed_tab["result"]["tab"]["label"], "logs");
let renamed_event = wait_for_event(&mut reader, "tab_renamed", Duration::from_secs(2));
- assert_eq!(renamed_event["data"]["tab_id"], "1:2");
+ assert_eq!(renamed_event["data"]["tab_id"], second_tab_id);
assert_eq!(renamed_event["data"]["label"], "logs");
+ let second_pane_id = format!("{}-1", workspace_id);
let split = send_request(
&socket_path,
- r#"{"id":"req_l6","method":"pane.split","params":{"target_pane_id":"1-2","direction":"right","focus":true}}"#,
+ &format!(
+ r#"{{"id":"req_l6","method":"pane.split","params":{{"target_pane_id":"{}","direction":"right","focus":true}}}}"#,
+ second_pane_id
+ ),
);
let split_pane_id = split["result"]["pane"]["pane_id"]
.as_str()
@@ -559,11 +611,14 @@ fn events_subscribe_streams_lifecycle_and_agent_events() {
let closed_tab = send_request(
&socket_path,
- r#"{"id":"req_l8","method":"tab.close","params":{"tab_id":"1:2"}}"#,
+ &format!(
+ r#"{{"id":"req_l8","method":"tab.close","params":{{"tab_id":"{}"}}}}"#,
+ second_tab_id
+ ),
);
assert_eq!(closed_tab["result"]["type"], "ok");
let tab_closed = wait_for_event(&mut reader, "tab_closed", Duration::from_secs(2));
- assert_eq!(tab_closed["data"]["tab_id"], "1:2");
+ assert_eq!(tab_closed["data"]["tab_id"], second_tab_id);
let closed_ws = send_request(
&socket_path,
@@ -992,7 +1047,7 @@ fn events_subscribe_streams_output_and_agent_state_events() {
base.display()
),
);
- assert_eq!(created["result"]["workspace"]["workspace_id"], "1");
+ assert!(created["result"]["workspace"]["workspace_id"].is_string());
let panes = send_request(
&socket_path,
diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs
index 18588605..190ae70c 100644
--- a/tests/cli_wrapper.rs
+++ b/tests/cli_wrapper.rs
@@ -153,7 +153,10 @@ fn workspace_and_pane_management_commands_work() {
);
assert!(created.status.success());
let created_json: serde_json::Value = serde_json::from_slice(&created.stdout).unwrap();
- assert_eq!(created_json["result"]["workspace"]["workspace_id"], "1");
+ let workspace_id = created_json["result"]["workspace"]["workspace_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
let panes = run_cli(&socket_path, &["pane", "list", "--workspace", "1"]);
assert!(panes.status.success());
@@ -182,15 +185,18 @@ fn workspace_and_pane_management_commands_work() {
let closed_json: serde_json::Value = serde_json::from_slice(&closed.stdout).unwrap();
assert_eq!(closed_json["result"]["type"], "ok");
- let renamed = run_cli(&socket_path, &["workspace", "rename", "1", "demo"]);
+ let renamed = run_cli(
+ &socket_path,
+ &["workspace", "rename", &workspace_id, "demo"],
+ );
assert!(renamed.status.success());
let renamed_json: serde_json::Value = serde_json::from_slice(&renamed.stdout).unwrap();
assert_eq!(renamed_json["result"]["workspace"]["label"], "demo");
- let focused = run_cli(&socket_path, &["workspace", "focus", "1"]);
+ let focused = run_cli(&socket_path, &["workspace", "focus", &workspace_id]);
assert!(focused.status.success());
- let closed_workspace = run_cli(&socket_path, &["workspace", "close", "1"]);
+ let closed_workspace = run_cli(&socket_path, &["workspace", "close", &workspace_id]);
assert!(closed_workspace.status.success());
let closed_workspace_json: serde_json::Value =
serde_json::from_slice(&closed_workspace.stdout).unwrap();
@@ -214,13 +220,29 @@ fn tab_management_commands_work() {
&["workspace", "create", "--cwd", base.to_str().unwrap()],
);
assert!(created.status.success());
+ let created_json: serde_json::Value = serde_json::from_slice(&created.stdout).unwrap();
+ let workspace_id = created_json["result"]["workspace"]["workspace_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
+ let first_tab_id = created_json["result"]["workspace"]["active_tab_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
- let created_tab = run_cli(&socket_path, &["tab", "create", "--workspace", "1"]);
+ let created_tab = run_cli(
+ &socket_path,
+ &["tab", "create", "--workspace", &workspace_id],
+ );
assert!(created_tab.status.success());
let created_tab_json: serde_json::Value = serde_json::from_slice(&created_tab.stdout).unwrap();
- assert_eq!(created_tab_json["result"]["tab"]["tab_id"], "1:2");
+ let second_tab_id = created_tab_json["result"]["tab"]["tab_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
+ assert_eq!(second_tab_id, format!("{workspace_id}:2"));
- let listed_tabs = run_cli(&socket_path, &["tab", "list", "--workspace", "1"]);
+ let listed_tabs = run_cli(&socket_path, &["tab", "list", "--workspace", &workspace_id]);
assert!(listed_tabs.status.success());
let listed_tabs_json: serde_json::Value = serde_json::from_slice(&listed_tabs.stdout).unwrap();
assert_eq!(
@@ -228,22 +250,22 @@ fn tab_management_commands_work() {
2
);
- let renamed_tab = run_cli(&socket_path, &["tab", "rename", "1:2", "logs"]);
+ let renamed_tab = run_cli(&socket_path, &["tab", "rename", &second_tab_id, "logs"]);
assert!(renamed_tab.status.success());
let renamed_tab_json: serde_json::Value = serde_json::from_slice(&renamed_tab.stdout).unwrap();
assert_eq!(renamed_tab_json["result"]["tab"]["label"], "logs");
- let focused_tab = run_cli(&socket_path, &["tab", "focus", "1:1"]);
+ let focused_tab = run_cli(&socket_path, &["tab", "focus", &first_tab_id]);
assert!(focused_tab.status.success());
let focused_tab_json: serde_json::Value = serde_json::from_slice(&focused_tab.stdout).unwrap();
- assert_eq!(focused_tab_json["result"]["tab"]["tab_id"], "1:1");
+ assert_eq!(focused_tab_json["result"]["tab"]["tab_id"], first_tab_id);
- let tab_get = run_cli(&socket_path, &["tab", "get", "1:2"]);
+ let tab_get = run_cli(&socket_path, &["tab", "get", &second_tab_id]);
assert!(tab_get.status.success());
let tab_get_json: serde_json::Value = serde_json::from_slice(&tab_get.stdout).unwrap();
- assert_eq!(tab_get_json["result"]["tab"]["tab_id"], "1:2");
+ assert_eq!(tab_get_json["result"]["tab"]["tab_id"], second_tab_id);
- let closed_tab = run_cli(&socket_path, &["tab", "close", "1:2"]);
+ let closed_tab = run_cli(&socket_path, &["tab", "close", &second_tab_id]);
assert!(closed_tab.status.success());
let closed_tab_json: serde_json::Value = serde_json::from_slice(&closed_tab.stdout).unwrap();
assert_eq!(closed_tab_json["result"]["type"], "ok");
@@ -268,7 +290,7 @@ fn pane_run_read_and_wait_commands_work() {
base.display()
),
);
- assert_eq!(created["result"]["workspace"]["workspace_id"], "1");
+ assert!(created["result"]["workspace"]["workspace_id"].is_string());
let create = run_cli(
&socket_path,
@@ -436,7 +458,7 @@ fn closing_workspace_terminates_processes_inside_it() {
}
#[test]
-fn ids_are_compact_and_positional() {
+fn workspace_ids_are_stable_and_pane_numbers_stay_compact() {
let base = unique_test_dir();
let config_home = base.join("config");
let runtime_dir = base.join("runtime");
@@ -450,45 +472,68 @@ fn ids_are_compact_and_positional() {
&["workspace", "create", "--cwd", base.to_str().unwrap()],
);
assert!(ws1.status.success());
+ let ws1_json: serde_json::Value = serde_json::from_slice(&ws1.stdout).unwrap();
+ let ws1_id = ws1_json["result"]["workspace"]["workspace_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
let split_12 = run_cli(
&socket_path,
&["pane", "split", "1-1", "--direction", "right", "--no-focus"],
);
let split_12_json: serde_json::Value = serde_json::from_slice(&split_12.stdout).unwrap();
- assert_eq!(split_12_json["result"]["pane"]["pane_id"], "1-2");
+ assert_eq!(
+ split_12_json["result"]["pane"]["pane_id"],
+ format!("{ws1_id}-2")
+ );
let split_13 = run_cli(
&socket_path,
&["pane", "split", "1-1", "--direction", "down", "--no-focus"],
);
let split_13_json: serde_json::Value = serde_json::from_slice(&split_13.stdout).unwrap();
- assert_eq!(split_13_json["result"]["pane"]["pane_id"], "1-3");
+ assert_eq!(
+ split_13_json["result"]["pane"]["pane_id"],
+ format!("{ws1_id}-3")
+ );
let ws2 = run_cli(
&socket_path,
&["workspace", "create", "--cwd", "/tmp", "--no-focus"],
);
let ws2_json: serde_json::Value = serde_json::from_slice(&ws2.stdout).unwrap();
- assert_eq!(ws2_json["result"]["workspace"]["workspace_id"], "2");
+ let ws2_id = ws2_json["result"]["workspace"]["workspace_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
+ assert_ne!(ws2_id, ws1_id);
- let ws2_focus = run_cli(&socket_path, &["workspace", "focus", "2"]);
+ let ws2_focus = run_cli(&socket_path, &["workspace", "focus", &ws2_id]);
assert!(ws2_focus.status.success());
let ws2_split = run_cli(
&socket_path,
&["pane", "split", "2-1", "--direction", "right", "--no-focus"],
);
let ws2_split_json: serde_json::Value = serde_json::from_slice(&ws2_split.stdout).unwrap();
- assert_eq!(ws2_split_json["result"]["pane"]["pane_id"], "2-2");
+ assert_eq!(
+ ws2_split_json["result"]["pane"]["pane_id"],
+ format!("{ws2_id}-2")
+ );
let ws3 = run_cli(
&socket_path,
&["workspace", "create", "--cwd", "/", "--no-focus"],
);
let ws3_json: serde_json::Value = serde_json::from_slice(&ws3.stdout).unwrap();
- assert_eq!(ws3_json["result"]["workspace"]["workspace_id"], "3");
+ let ws3_id = ws3_json["result"]["workspace"]["workspace_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
+ assert_ne!(ws3_id, ws1_id);
+ assert_ne!(ws3_id, ws2_id);
- let close_ws2 = run_cli(&socket_path, &["workspace", "close", "2"]);
+ let close_ws2 = run_cli(&socket_path, &["workspace", "close", &ws2_id]);
assert!(close_ws2.status.success());
let workspaces = run_cli(&socket_path, &["workspace", "list"]);
@@ -499,22 +544,31 @@ fn ids_are_compact_and_positional() {
.iter()
.map(|ws| ws["workspace_id"].as_str().unwrap().to_string())
.collect();
- assert_eq!(ids, vec!["1".to_string(), "2".to_string()]);
+ assert_eq!(ids, vec![ws1_id.clone(), ws3_id.clone()]);
let new_ws = run_cli(
&socket_path,
&["workspace", "create", "--cwd", "/var/tmp", "--no-focus"],
);
let new_ws_json: serde_json::Value = serde_json::from_slice(&new_ws.stdout).unwrap();
- assert_eq!(new_ws_json["result"]["workspace"]["workspace_id"], "3");
+ let new_ws_id = new_ws_json["result"]["workspace"]["workspace_id"]
+ .as_str()
+ .unwrap()
+ .to_string();
+ assert_ne!(new_ws_id, ws1_id);
+ assert_ne!(new_ws_id, ws2_id);
+ assert_ne!(new_ws_id, ws3_id);
- let ws3_panes = run_cli(&socket_path, &["pane", "list", "--workspace", "3"]);
+ let ws3_panes = run_cli(&socket_path, &["pane", "list", "--workspace", &ws3_id]);
let ws3_panes_json: serde_json::Value = serde_json::from_slice(&ws3_panes.stdout).unwrap();
- assert_eq!(ws3_panes_json["result"]["panes"][0]["pane_id"], "3-1");
+ assert_eq!(
+ ws3_panes_json["result"]["panes"][0]["pane_id"],
+ format!("{ws3_id}-1")
+ );
- let close_middle = run_cli(&socket_path, &["pane", "close", "1-2"]);
+ let close_middle = run_cli(&socket_path, &["pane", "close", &format!("{ws1_id}-2")]);
assert!(close_middle.status.success());
- let ws1_panes = run_cli(&socket_path, &["pane", "list", "--workspace", "1"]);
+ let ws1_panes = run_cli(&socket_path, &["pane", "list", "--workspace", &ws1_id]);
let ws1_panes_json: serde_json::Value = serde_json::from_slice(&ws1_panes.stdout).unwrap();
let pane_ids: Vec<String> = ws1_panes_json["result"]["panes"]
.as_array()
@@ -522,7 +576,7 @@ fn ids_are_compact_and_positional() {
.iter()
.map(|pane| pane["pane_id"].as_str().unwrap().to_string())
.collect();
- assert_eq!(pane_ids, vec!["1-1".to_string(), "1-2".to_string()]);
+ assert_eq!(pane_ids, vec![format!("{ws1_id}-1"), format!("{ws1_id}-2")]);
cleanup_spawned_herdr(herdr, base);
}
@@ -544,7 +598,7 @@ fn pane_shell_gets_herdr_socket_and_pane_env() {
base.display()
),
);
- assert_eq!(created["result"]["workspace"]["workspace_id"], "1");
+ assert!(created["result"]["workspace"]["workspace_id"].is_string());
let env_capture = base.join("pane-env.txt");
let ran = run_cli(
@@ -643,7 +697,7 @@ fn wait_agent_state_exits_when_state_matches() {
base.display()
),
);
- assert_eq!(created["result"]["workspace"]["workspace_id"], "1");
+ assert!(created["result"]["workspace"]["workspace_id"].is_string());
let start_pi = run_cli(&socket_path, &["pane", "run", "1-1", "pi"]);
assert!(start_pi.status.success());