fix: preserve workspace when closing the last pane in a tab

This commit is contained in:
Ogulcan Celik 2026-04-10 15:00:32 +03:00
parent a763406db2
commit 08eccb89d2
2 changed files with 148 additions and 12 deletions

View File

@ -1876,18 +1876,20 @@ 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,
error: ErrorBody {
code: "pane_not_found".into(),
message: format!("pane {} not found", target.pane_id),
},
})
.unwrap();
let should_close_workspace = {
let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
return serde_json::to_string(&ErrorResponse {
id: request.id,
error: ErrorBody {
code: "pane_not_found".into(),
message: format!("pane {} not found", target.pane_id),
},
})
.unwrap();
};
ws.close_pane(pane_id)
};
let pane_count = ws.layout.pane_count();
if pane_count <= 1 {
if should_close_workspace {
self.state.selected = ws_idx;
self.state.close_selected_workspace();
self.emit_event(crate::api::schema::EventEnvelope {
@ -1902,7 +1904,6 @@ impl App {
data: crate::api::schema::EventData::WorkspaceClosed { workspace_id },
});
} else {
ws.close_pane(pane_id);
self.schedule_session_save();
self.emit_event(crate::api::schema::EventEnvelope {
event: crate::api::schema::EventKind::PaneClosed,
@ -2794,6 +2795,56 @@ mod tests {
assert_eq!(seed_cwd, std::path::PathBuf::from("/tmp/pion"));
}
#[test]
fn pane_close_request_closes_only_the_target_tab_when_other_tabs_exist() {
let mut app = test_app();
let mut workspace = Workspace::test_new("api-pane-close");
let second_tab = workspace.test_add_tab(Some("logs"));
workspace.switch_tab(second_tab);
app.state.workspaces = vec![workspace];
app.state.active = Some(0);
app.state.selected = 0;
let target_pane = app.state.workspaces[0].tabs[second_tab].root_pane;
let target_pane_id = app.pane_info(0, target_pane).unwrap().pane_id;
let response = app.handle_api_request(crate::api::schema::Request {
id: "req_pane_close".into(),
method: crate::api::schema::Method::PaneClose(crate::api::schema::PaneTarget {
pane_id: target_pane_id,
}),
});
let response: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(response["result"]["type"], "ok");
assert_eq!(app.state.workspaces.len(), 1);
assert_eq!(app.state.workspaces[0].tabs.len(), 1);
assert_eq!(app.state.workspaces[0].display_name(), "api-pane-close");
}
#[test]
fn pane_close_request_closes_workspace_when_it_removes_the_last_pane() {
let mut app = test_app();
let workspace = Workspace::test_new("api-pane-close-last");
app.state.workspaces = vec![workspace];
app.state.active = Some(0);
app.state.selected = 0;
let target_pane = app.state.workspaces[0].tabs[0].root_pane;
let target_pane_id = app.pane_info(0, target_pane).unwrap().pane_id;
let response = app.handle_api_request(crate::api::schema::Request {
id: "req_pane_close_last".into(),
method: crate::api::schema::Method::PaneClose(crate::api::schema::PaneTarget {
pane_id: target_pane_id,
}),
});
let response: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(response["result"]["type"], "ok");
assert!(app.state.workspaces.is_empty());
}
#[test]
fn session_dirty_flag_schedules_debounced_save() {
let mut app = test_app();

View File

@ -356,6 +356,91 @@ fn tab_management_commands_work() {
cleanup_spawned_herdr(herdr, base);
}
#[test]
fn pane_close_only_removes_the_target_tab_when_other_tabs_exist() {
let base = unique_test_dir();
let config_home = base.join("config");
let runtime_dir = base.join("runtime");
let socket_path = runtime_dir.join("herdr.sock");
let herdr = spawn_herdr(&config_home, &runtime_dir, &socket_path);
wait_for_socket(&socket_path, Duration::from_secs(5));
let created = run_cli(
&socket_path,
&["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 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();
let second_root_pane_id = created_tab_json["result"]["root_pane"]["pane_id"]
.as_str()
.unwrap()
.to_string();
let closed = run_cli(&socket_path, &["pane", "close", &second_root_pane_id]);
assert!(closed.status.success());
let closed_json: serde_json::Value = serde_json::from_slice(&closed.stdout).unwrap();
assert_eq!(closed_json["result"]["type"], "ok");
let workspaces = run_cli(&socket_path, &["workspace", "list"]);
assert!(workspaces.status.success());
let workspaces_json: serde_json::Value = serde_json::from_slice(&workspaces.stdout).unwrap();
assert_eq!(workspaces_json["result"]["workspaces"].as_array().unwrap().len(), 1);
assert_eq!(workspaces_json["result"]["workspaces"][0]["workspace_id"], workspace_id);
let tabs = run_cli(&socket_path, &["tab", "list", "--workspace", &workspace_id]);
assert!(tabs.status.success());
let tabs_json: serde_json::Value = serde_json::from_slice(&tabs.stdout).unwrap();
assert_eq!(tabs_json["result"]["tabs"].as_array().unwrap().len(), 1);
cleanup_spawned_herdr(herdr, base);
}
#[test]
fn pane_close_removes_the_workspace_when_it_closes_the_last_pane() {
let base = unique_test_dir();
let config_home = base.join("config");
let runtime_dir = base.join("runtime");
let socket_path = runtime_dir.join("herdr.sock");
let herdr = spawn_herdr(&config_home, &runtime_dir, &socket_path);
wait_for_socket(&socket_path, Duration::from_secs(5));
let created = run_cli(
&socket_path,
&["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 root_pane_id = created_json["result"]["root_pane"]["pane_id"]
.as_str()
.unwrap()
.to_string();
let closed = run_cli(&socket_path, &["pane", "close", &root_pane_id]);
assert!(closed.status.success());
let closed_json: serde_json::Value = serde_json::from_slice(&closed.stdout).unwrap();
assert_eq!(closed_json["result"]["type"], "ok");
let workspaces = run_cli(&socket_path, &["workspace", "list"]);
assert!(workspaces.status.success());
let workspaces_json: serde_json::Value = serde_json::from_slice(&workspaces.stdout).unwrap();
assert!(workspaces_json["result"]["workspaces"].as_array().unwrap().is_empty());
cleanup_spawned_herdr(herdr, base);
}
#[test]
fn pane_run_read_and_wait_commands_work() {
let base = unique_test_dir();