diff options
| author | Season Saw <season.saw@gmail.com> | 2026-07-27 07:25:29 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-27 02:25:29 +0300 |
| commit | a79b3d558026d0703f848882d652bb5db65df26e (patch) | |
| tree | 8287cd9eb2d9884b544899c28e0e415c8f9000d0 /src | |
| parent | 7e96b1b7e8d3dcc1c12ae538598e5a955ac58c87 (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')
| -rw-r--r-- | src/app/api/tabs.rs | 97 |
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(); |
