From 4d4e5ec420719cba36d3f6a4f29ef6921d564d9d Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Sun, 31 May 2026 23:47:50 -0700 Subject: [PATCH] project: Scope terminal toolchain lookup to the terminal's worktree (#56787) `create_terminal_task` and `create_terminal_shell_internal` previously built their toolchain candidate list as `[active_editor_entry.path, ...visible_worktrees]` and used the first one with a persisted toolchain. A Python toolchain persisted for worktree A would leak into a terminal opened in worktree B of the same window: with a conda env selected for one project, every terminal in every other project of the window inherited that env's activation at startup. This scopes the candidate list to the worktree that contains the terminal's CWD. Both call sites now derive candidates from `find_worktree(path)`, where `path` is the terminal's resolved CWD (`spawn_task.cwd` or `active_project_directory`). If the path lives in a worktree, lookup runs scoped to that worktree's id. If not, no toolchain is applied. ## Design decision The previous fallback chain (active editor's worktree, then all visible worktrees) is removed entirely. Both fallbacks were sources of the leak: - "Active editor's worktree" is order-dependent: opening a terminal in worktree B picks up worktree A's toolchain because a file in A is focused. - "All visible worktrees" is worse, since the first worktree with any persisted toolchain wins. Workflows that relied on either fallback need to set the toolchain explicitly on the worktree where the terminal is opened. ## Coverage `cargo check -p project` is clean. There is no unit test for the cross-worktree case; that would require a multi-worktree Project fixture with persisted toolchains, which doesn't exist in the test suite today. Reproduction is straightforward: open a workspace with two folders, select a Python toolchain in one, open a terminal in the other, and observe the leaked activation script in the spawned shell. Release Notes: - Fixed Python toolchains persisted for one worktree leaking into terminals opened in other worktrees of the same workspace. --- crates/project/src/terminals.rs | 49 ++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b0fc16f3c8..5908408f7c 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -115,19 +115,23 @@ impl Project { let env_task = self.resolve_directory_environment(&shell, path.clone(), remote_client.clone(), cx); - let project_path_contexts = self - .active_entry() - .and_then(|entry_id| self.path_for_entry(entry_id, cx)) + // Scope the toolchain lookup to the worktree the terminal is being + // spawned in. Previously this iterated the active editor's worktree + // and then every visible worktree, so a Python toolchain persisted + // for worktree A would leak into a terminal opened in worktree B and + // inject (e.g.) `conda activate base` into a shell that has no + // business with conda. + let project_path_contexts: Vec = path + .as_ref() + .and_then(|p| self.find_worktree(p, cx)) + .map(|(worktree, relative_path)| ProjectPath { + worktree_id: worktree.read(cx).id(), + path: relative_path, + }) .into_iter() - .chain( - self.visible_worktrees(cx) - .map(|wt| wt.read(cx).id()) - .map(|worktree_id| ProjectPath { - worktree_id, - path: Arc::from(RelPath::empty()), - }), - ); + .collect(); let toolchains = project_path_contexts + .into_iter() .filter(|_| detect_venv) .map(|p| self.active_toolchain(p, LanguageName::new_static("Python"), cx)) .collect::>(); @@ -333,19 +337,20 @@ impl Project { let detect_venv = settings.detect_venv.as_option().is_some(); let local_path = if is_via_remote { None } else { path.clone() }; - let project_path_contexts = self - .active_entry() - .and_then(|entry_id| self.path_for_entry(entry_id, cx)) + // See create_terminal_task: scope the toolchain lookup to the + // worktree the terminal is opened in, not the active editor's + // worktree or other visible worktrees. + let project_path_contexts: Vec = path + .as_ref() + .and_then(|p| self.find_worktree(p, cx)) + .map(|(worktree, relative_path)| ProjectPath { + worktree_id: worktree.read(cx).id(), + path: relative_path, + }) .into_iter() - .chain( - self.visible_worktrees(cx) - .map(|wt| wt.read(cx).id()) - .map(|worktree_id| ProjectPath { - worktree_id, - path: RelPath::empty().into(), - }), - ); + .collect(); let toolchains = project_path_contexts + .into_iter() .filter(|_| detect_venv) .map(|p| self.active_toolchain(p, LanguageName::new_static("Python"), cx)) .collect::>();