fix: preserve focus after background worktree removal (#3099)

This commit is contained in:
akbash
2026-08-21 22:42:08 +03:00
committed by GitHub
parent 81864e516a
commit 25e35f079e
3 changed files with 68 additions and 0 deletions
+1
View File
@@ -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)
+63
View File
@@ -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();
+4
View File
@@ -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;
};