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.
This commit is contained in:
Kyle Kelley authored and GitHub committed 2026-06-01 06:47:50 +00:00
1 parent ea4bf744d0
commit 4d4e5ec420
1 file changed
+27 -22
+27 -22
View File
@@ -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<ProjectPath> = 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::<Vec<_>>();
@@ -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<ProjectPath> = 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::<Vec<_>>();