aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-08-21 22:42:08 +0300
committerGitHub <noreply@github.com>2026-08-21 22:42:08 +0300
commit25e35f079e5147b52a696e72e87df92108febf93 (patch)
treea1cd3cf8e1ab76c6d8fa7d8523277e3141a9f831
parent81864e516a55f8cea5490f28e0b93531703cf1ec (diff)
fix: preserve focus after background worktree removal (#3099)
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--src/app/api/worktrees.rs63
-rw-r--r--src/app/worktrees.rs4
3 files changed, 68 insertions, 0 deletions
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
@@ -2181,6 +2181,69 @@ mod tests {
}
#[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();
let mut app = test_app_with_event_hub(event_hub.clone());
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;
};