diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-04 01:50:34 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-04 01:50:34 +0300 |
| commit | 831894facfc1339f819b42c3339668154121d24f (patch) | |
| tree | 3a49bad36a65341f6296b9f3d568317e15b6c21a | |
| parent | e0fb8ca6fa55acaa72902a15c5468cbe2787ddf7 (diff) | |
refactor: make workspace api ids stable
| -rw-r--r-- | SOCKET_API.md | 50 | ||||
| -rw-r--r-- | src/app/mod.rs | 39 | ||||
| -rw-r--r-- | src/persist.rs | 13 | ||||
| -rw-r--r-- | src/workspace.rs | 18 | ||||
| -rw-r--r-- | tests/api_ping.rs | 117 | ||||
| -rw-r--r-- | tests/cli_wrapper.rs | 116 |
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()); |
