diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index e6457972..d2c0f85d 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Fixed +- Removing a background worktree workspace no longer changes focus to its parent workspace. (#3098) - Prefix bindings such as `prefix+|` now recognize characters produced by macOS Option and custom keyboard layouts, while exact chords such as `prefix+alt+w` keep priority. (#3079, thanks @vlcinsky) - Direct terminal attaches now preserve multiline pastes as one paste instead of submitting each line separately. (#3054) - Windows panes now keep bare `cursor-agent` launches detected after Cursor hands off to its bundled Node process. (#3032) diff --git a/src/app/api/worktrees.rs b/src/app/api/worktrees.rs index 7aa8c00f..774b981a 100644 --- a/src/app/api/worktrees.rs +++ b/src/app/api/worktrees.rs @@ -2180,6 +2180,69 @@ mod tests { let _ = std::fs::remove_dir_all(repo); } + #[test] + fn deferred_api_background_worktree_remove_preserves_focused_workspace() { + let mut app = test_app(); + let checkout = PathBuf::from("/repo/herdr-issue"); + let membership = crate::workspace::WorktreeSpaceMembership { + key: "repo-key".into(), + label: "herdr".into(), + repo_root: "/repo/herdr".into(), + checkout_path: checkout.clone(), + is_linked_worktree: true, + }; + let mut parent = Workspace::test_new("parent"); + parent.worktree_space = Some(crate::workspace::WorktreeSpaceMembership { + is_linked_worktree: false, + checkout_path: "/repo/herdr".into(), + ..membership.clone() + }); + let mut child = Workspace::test_new("child"); + child.worktree_space = Some(membership.clone()); + let foreground = Workspace::test_new("foreground"); + let child_id = child.id.clone(); + let foreground_id = foreground.id.clone(); + app.state.workspaces = vec![parent, child, foreground]; + app.state.active = Some(2); + app.state.selected = 2; + let workspace_snapshot = app.workspace_info(1); + let worktree_snapshot = app.worktree_info_for_membership(&membership, None); + app.pending_api_worktree_removes.insert(child_id.clone(), 7); + app.pending_api_worktree_remove_paths + .insert(crate::worktree::canonical_or_original(&checkout), 7); + let (respond_to, response_rx) = response_channel(); + + app.handle_api_worktree_remove_finished(WorktreeRemoveResult { + workspace_id: child_id, + path: checkout.clone(), + workspace: Some(Box::new(workspace_snapshot)), + worktree: Some(Box::new(worktree_snapshot)), + forced: false, + api_request: Some(ApiWorktreeRemoveRequest { + id: "req".into(), + operation_id: 7, + checkout_key: crate::worktree::canonical_or_original(&checkout), + respond_to, + }), + result: Ok(()), + }); + + let response = response_rx + .recv_timeout(std::time::Duration::from_secs(2)) + .expect("background worktree remove completion should respond"); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + assert!(matches!( + success.result, + ResponseResult::WorktreeRemoved { .. } + )); + assert_eq!(app.state.workspaces.len(), 2); + assert_eq!( + app.state.active.map(|idx| &app.state.workspaces[idx].id), + Some(&foreground_id) + ); + assert_eq!(app.state.workspaces[app.state.selected].id, foreground_id); + } + #[test] fn deferred_api_worktree_remove_emits_removed_after_workspace_changes() { let event_hub = crate::api::EventHub::default(); diff --git a/src/app/worktrees.rs b/src/app/worktrees.rs index 0087c8fe..414d74b5 100644 --- a/src/app/worktrees.rs +++ b/src/app/worktrees.rs @@ -968,6 +968,7 @@ impl App { } pub(crate) fn close_removed_linked_worktree_workspace(&mut self, ws_idx: usize) { + let removed_workspace_was_active = self.state.active == Some(ws_idx); let parent_key = self .state .workspaces @@ -979,6 +980,9 @@ impl App { self.state.selected = ws_idx; self.state.close_selected_workspace(); + if !removed_workspace_was_active { + return; + } let Some(parent_key) = parent_key else { return; };