aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorSeason Saw <season.saw@gmail.com>2026-07-27 07:25:29 +0800
committerGitHub <noreply@github.com>2026-07-27 02:25:29 +0300
commita79b3d558026d0703f848882d652bb5db65df26e (patch)
tree8287cd9eb2d9884b544899c28e0e415c8f9000d0 /src/app
parent7e96b1b7e8d3dcc1c12ae538598e5a955ac58c87 (diff)
fix: close workspace when api closes its last tab (#1899)
herdr tab close on a workspace's only tab returned tab_close_failed, while the TUI close-tab action on the same tab closes the tab and cascades to closing the workspace. Cascade in the API too, emitting tab.closed then workspace.closed. When the cascade would close a whole worktree group, return confirmation_required instead, matching pane.close and the TUI prompt. refs #1760
Diffstat (limited to 'src/app')
-rw-r--r--src/app/api/tabs.rs97
1 files changed, 85 insertions, 12 deletions
diff --git a/src/app/api/tabs.rs b/src/app/api/tabs.rs
index 6ae9d26e..2a2f3cdb 100644
--- a/src/app/api/tabs.rs
+++ b/src/app/api/tabs.rs
@@ -230,24 +230,50 @@ impl App {
return tab_not_found(id, &target.tab_id);
};
let workspace_id = self.public_workspace_id(ws_idx);
+ let Some(ws) = self.state.workspaces.get(ws_idx) else {
+ return tab_not_found(id, &target.tab_id);
+ };
+ let closes_workspace = ws.tabs.len() <= 1;
let terminal_ids = self.state.terminal_ids_for_tab(ws_idx, tab_idx);
- let pane_ids = self
- .state
- .workspaces
- .get(ws_idx)
- .and_then(|ws| ws.tabs.get(tab_idx))
+ let pane_ids = ws
+ .tabs
+ .get(tab_idx)
.map(|tab| tab.layout.pane_ids())
.unwrap_or_default();
+
+ if closes_workspace {
+ if self.state.confirm_implicit_worktree_group_close(ws_idx) {
+ return encode_error(
+ id,
+ "confirmation_required",
+ "closing this tab would close a worktree group",
+ );
+ }
+ let workspace = self.workspace_info(ws_idx);
+ self.state.selected = ws_idx;
+ self.state.close_selected_workspace();
+ self.state.remove_plugin_pane_records(pane_ids);
+ self.shutdown_detached_terminal_runtimes();
+ self.emit_event(EventEnvelope {
+ event: EventKind::TabClosed,
+ data: EventData::TabClosed {
+ tab_id,
+ workspace_id: workspace_id.clone(),
+ },
+ });
+ self.emit_event(EventEnvelope {
+ event: EventKind::WorkspaceClosed,
+ data: EventData::WorkspaceClosed {
+ workspace_id,
+ workspace: Some(workspace),
+ },
+ });
+ return encode_success(id, ResponseResult::Ok {});
+ }
+
let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
return tab_not_found(id, &target.tab_id);
};
- if ws.tabs.len() <= 1 {
- return encode_error(
- id,
- "tab_close_failed",
- "cannot close the last tab in a workspace",
- );
- }
if !ws.close_tab(tab_idx) {
return encode_error(
id,
@@ -306,6 +332,53 @@ mod tests {
};
#[test]
+ fn api_tab_close_last_tab_closes_workspace_and_emits_both_events() {
+ let event_hub = crate::api::EventHub::default();
+ let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
+ let mut app = App::new(&Config::default(), true, None, api_rx, event_hub.clone());
+ app.state.workspaces = vec![Workspace::test_new("tabs")];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ let tab_id = app.public_tab_id(0, 0).unwrap();
+ let workspace_id = app.public_workspace_id(0);
+
+ let response = app.handle_tab_close(
+ "req".into(),
+ TabTarget {
+ tab_id: tab_id.clone(),
+ },
+ );
+
+ let success: SuccessResponse = serde_json::from_str(&response).unwrap();
+ assert_eq!(success.result, ResponseResult::Ok {});
+ assert!(app.state.workspaces.is_empty());
+ assert!(app.state.active.is_none());
+ let events = event_hub.events_after(0);
+ assert_eq!(
+ events
+ .iter()
+ .map(|(_, event)| event.event)
+ .collect::<Vec<_>>(),
+ [EventKind::TabClosed, EventKind::WorkspaceClosed]
+ );
+ assert!(matches!(
+ &events[0].1.data,
+ EventData::TabClosed {
+ tab_id: closed_tab_id,
+ workspace_id: closed_workspace_id,
+ } if closed_tab_id == &tab_id && closed_workspace_id == &workspace_id
+ ));
+ assert!(matches!(
+ &events[1].1.data,
+ EventData::WorkspaceClosed {
+ workspace_id: closed_workspace_id,
+ workspace: Some(workspace),
+ } if closed_workspace_id == &workspace_id
+ && workspace.workspace_id == workspace_id
+ ));
+ }
+
+ #[test]
fn api_tab_move_reorders_tabs_in_target_workspace() {
let event_hub = crate::api::EventHub::default();
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();