Inject venv environment via the toolchain (#36576)

Lukas Wirth created

Instead of manually constructing the venv we now ask the python
toolchain for the relevant information, unifying the approach of vent
inspection

Fixes https://github.com/zed-industries/zed/issues/27350

Release Notes:

- Improved the detection of python virtual environments for terminals
and tasks in remote projects.

Change summary

crates/agent2/src/tools/terminal_tool.rs              |   8 
crates/assistant_tools/src/terminal_tool.rs           |   9 
crates/debugger_ui/src/session/running.rs             |  15 
crates/language/src/toolchain.rs                      |  30 
crates/languages/src/python.rs                        |  44 
crates/project/src/debugger/dap_store.rs              |   1 
crates/project/src/project_tests.rs                   |   3 
crates/project/src/terminals.rs                       | 789 +++++-------
crates/proto/build.rs                                 |   1 
crates/remote/src/remote_client.rs                    |  12 
crates/remote/src/transport/ssh.rs                    |  13 
crates/task/src/shell_builder.rs                      |  39 
crates/terminal/src/terminal.rs                       |  63 
crates/terminal_view/src/persistence.rs               |  12 
crates/terminal_view/src/terminal_panel.rs            | 253 ++-
crates/terminal_view/src/terminal_path_like_target.rs |   6 
crates/terminal_view/src/terminal_view.rs             |  29 
crates/util/src/util.rs                               |  12 
crates/workspace/src/persistence.rs                   |  16 
19 files changed, 704 insertions(+), 651 deletions(-)

Detailed changes

crates/agent2/src/tools/terminal_tool.rs 🔗

@@ -2,7 +2,7 @@ use agent_client_protocol as acp;
 use anyhow::Result;
 use futures::{FutureExt as _, future::Shared};
 use gpui::{App, AppContext, Entity, SharedString, Task};
-use project::{Project, terminals::TerminalKind};
+use project::Project;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::{
@@ -144,14 +144,14 @@ impl AgentTool for TerminalTool {
                 let terminal = self
                     .project
                     .update(cx, |project, cx| {
-                        project.create_terminal(
-                            TerminalKind::Task(task::SpawnInTerminal {
+                        project.create_terminal_task(
+                            task::SpawnInTerminal {
                                 command: Some(program),
                                 args,
                                 cwd: working_dir.clone(),
                                 env,
                                 ..Default::default()
-                            }),
+                            },
                             cx,
                         )
                     })?

crates/assistant_tools/src/terminal_tool.rs 🔗

@@ -15,7 +15,7 @@ use language::LineEnding;
 use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
 use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 use portable_pty::{CommandBuilder, PtySize, native_pty_system};
-use project::{Project, terminals::TerminalKind};
+use project::Project;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
@@ -213,17 +213,16 @@ impl Tool for TerminalTool {
             async move |cx| {
                 let program = program.await;
                 let env = env.await;
-
                 project
                     .update(cx, |project, cx| {
-                        project.create_terminal(
-                            TerminalKind::Task(task::SpawnInTerminal {
+                        project.create_terminal_task(
+                            task::SpawnInTerminal {
                                 command: Some(program),
                                 args,
                                 cwd,
                                 env,
                                 ..Default::default()
-                            }),
+                            },
                             cx,
                         )
                     })?

crates/debugger_ui/src/session/running.rs 🔗

@@ -36,7 +36,6 @@ use module_list::ModuleList;
 use project::{
     DebugScenarioContext, Project, WorktreeId,
     debugger::session::{self, Session, SessionEvent, SessionStateEvent, ThreadId, ThreadStatus},
-    terminals::TerminalKind,
 };
 use rpc::proto::ViewId;
 use serde_json::Value;
@@ -1040,12 +1039,11 @@ impl RunningState {
                 };
                 let terminal = project
                     .update(cx, |project, cx| {
-                        project.create_terminal(
-                            TerminalKind::Task(task_with_shell.clone()),
+                        project.create_terminal_task(
+                            task_with_shell.clone(),
                             cx,
                         )
-                    })?
-                    .await?;
+                    })?.await?;
 
                 let terminal_view = cx.new_window_entity(|window, cx| {
                     TerminalView::new(
@@ -1189,7 +1187,7 @@ impl RunningState {
             .filter(|title| !title.is_empty())
             .or_else(|| command.clone())
             .unwrap_or_else(|| "Debug terminal".to_string());
-        let kind = TerminalKind::Task(task::SpawnInTerminal {
+        let kind = task::SpawnInTerminal {
             id: task::TaskId("debug".to_string()),
             full_label: title.clone(),
             label: title.clone(),
@@ -1207,12 +1205,13 @@ impl RunningState {
             show_summary: false,
             show_command: false,
             show_rerun: false,
-        });
+        };
 
         let workspace = self.workspace.clone();
         let weak_project = project.downgrade();
 
-        let terminal_task = project.update(cx, |project, cx| project.create_terminal(kind, cx));
+        let terminal_task =
+            project.update(cx, |project, cx| project.create_terminal_task(kind, cx));
         let terminal_task = cx.spawn_in(window, async move |_, cx| {
             let terminal = terminal_task.await?;
 

crates/language/src/toolchain.rs 🔗

@@ -11,13 +11,14 @@ use std::{
 
 use async_trait::async_trait;
 use collections::HashMap;
+use fs::Fs;
 use gpui::{AsyncApp, SharedString};
 use settings::WorktreeId;
 
 use crate::{LanguageName, ManifestName};
 
 /// Represents a single toolchain.
-#[derive(Clone, Debug, Eq)]
+#[derive(Clone, Eq, Debug)]
 pub struct Toolchain {
     /// User-facing label
     pub name: SharedString,
@@ -29,21 +30,29 @@ pub struct Toolchain {
 
 impl std::hash::Hash for Toolchain {
     fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        self.name.hash(state);
-        self.path.hash(state);
-        self.language_name.hash(state);
+        let Self {
+            name,
+            path,
+            language_name,
+            as_json: _,
+        } = self;
+        name.hash(state);
+        path.hash(state);
+        language_name.hash(state);
     }
 }
 
 impl PartialEq for Toolchain {
     fn eq(&self, other: &Self) -> bool {
+        let Self {
+            name,
+            path,
+            language_name,
+            as_json: _,
+        } = self;
         // Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced.
         // Thus, there could be multiple entries that look the same in the UI.
-        (&self.name, &self.path, &self.language_name).eq(&(
-            &other.name,
-            &other.path,
-            &other.language_name,
-        ))
+        (name, path, language_name).eq(&(&other.name, &other.path, &other.language_name))
     }
 }
 
@@ -59,6 +68,7 @@ pub trait ToolchainLister: Send + Sync {
     fn term(&self) -> SharedString;
     /// Returns the name of the manifest file for this toolchain.
     fn manifest_name(&self) -> ManifestName;
+    async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option<String>;
 }
 
 #[async_trait(?Send)]
@@ -82,7 +92,7 @@ pub trait LocalLanguageToolchainStore: Send + Sync + 'static {
     ) -> Option<Toolchain>;
 }
 
-#[async_trait(?Send )]
+#[async_trait(?Send)]
 impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
     async fn active_toolchain(
         self: Arc<Self>,

crates/languages/src/python.rs 🔗

@@ -2,6 +2,7 @@ use anyhow::{Context as _, ensure};
 use anyhow::{Result, anyhow};
 use async_trait::async_trait;
 use collections::HashMap;
+use futures::AsyncBufReadExt;
 use gpui::{App, Task};
 use gpui::{AsyncApp, SharedString};
 use language::Toolchain;
@@ -30,8 +31,6 @@ use std::{
     borrow::Cow,
     ffi::OsString,
     fmt::Write,
-    fs,
-    io::{self, BufRead},
     path::{Path, PathBuf},
     sync::Arc,
 };
@@ -741,14 +740,16 @@ fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
 /// Return the name of environment declared in <worktree-root/.venv.
 ///
 /// https://virtualfish.readthedocs.io/en/latest/plugins.html#auto-activation-auto-activation
-fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
-    fs::File::open(worktree_root.join(".venv"))
-        .and_then(|file| {
-            let mut venv_name = String::new();
-            io::BufReader::new(file).read_line(&mut venv_name)?;
-            Ok(venv_name.trim().to_string())
-        })
-        .ok()
+async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
+    let file = async_fs::File::open(worktree_root.join(".venv"))
+        .await
+        .ok()?;
+    let mut venv_name = String::new();
+    smol::io::BufReader::new(file)
+        .read_line(&mut venv_name)
+        .await
+        .ok()?;
+    Some(venv_name.trim().to_string())
 }
 
 #[async_trait]
@@ -793,7 +794,7 @@ impl ToolchainLister for PythonToolchainProvider {
             .map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
 
         let wr = worktree_root;
-        let wr_venv = get_worktree_venv_declaration(&wr);
+        let wr_venv = get_worktree_venv_declaration(&wr).await;
         // Sort detected environments by:
         //     environment name matching activation file (<workdir>/.venv)
         //     environment project dir matching worktree_root
@@ -858,7 +859,7 @@ impl ToolchainLister for PythonToolchainProvider {
             .into_iter()
             .filter_map(|toolchain| {
                 let mut name = String::from("Python");
-                if let Some(ref version) = toolchain.version {
+                if let Some(version) = &toolchain.version {
                     _ = write!(name, " {version}");
                 }
 
@@ -879,7 +880,7 @@ impl ToolchainLister for PythonToolchainProvider {
                     name: name.into(),
                     path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(),
                     language_name: LanguageName::new("Python"),
-                    as_json: serde_json::to_value(toolchain).ok()?,
+                    as_json: serde_json::to_value(toolchain.clone()).ok()?,
                 })
             })
             .collect();
@@ -893,6 +894,23 @@ impl ToolchainLister for PythonToolchainProvider {
     fn term(&self) -> SharedString {
         self.term.clone()
     }
+    async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option<String> {
+        let toolchain = serde_json::from_value::<pet_core::python_environment::PythonEnvironment>(
+            toolchain.as_json.clone(),
+        )
+        .ok()?;
+        let mut activation_script = None;
+        if let Some(prefix) = &toolchain.prefix {
+            #[cfg(not(target_os = "windows"))]
+            let path = prefix.join(BINARY_DIR).join("activate");
+            #[cfg(target_os = "windows")]
+            let path = prefix.join(BINARY_DIR).join("activate.ps1");
+            if fs.is_file(&path).await {
+                activation_script = Some(format!(". {}", path.display()));
+            }
+        }
+        activation_script
+    }
 }
 
 pub struct EnvironmentApi<'a> {

crates/project/src/project_tests.rs 🔗

@@ -9222,6 +9222,9 @@ fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
         fn manifest_name(&self) -> ManifestName {
             SharedString::new_static("pyproject.toml").into()
         }
+        async fn activation_script(&self, _: &Toolchain, _: &dyn Fs) -> Option<String> {
+            None
+        }
     }
     Arc::new(
         Language::new(

crates/project/src/terminals.rs 🔗

@@ -1,44 +1,28 @@
-use crate::{Project, ProjectPath};
-use anyhow::{Context as _, Result};
+use anyhow::Result;
 use collections::HashMap;
 use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
+use itertools::Itertools;
 use language::LanguageName;
 use remote::RemoteClient;
 use settings::{Settings, SettingsLocation};
 use smol::channel::bounded;
 use std::{
-    env::{self},
+    borrow::Cow,
     path::{Path, PathBuf},
     sync::Arc,
 };
 use task::{Shell, ShellBuilder, SpawnInTerminal};
 use terminal::{
-    TaskState, TaskStatus, Terminal, TerminalBuilder,
-    terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings},
+    TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings,
 };
-use util::{ResultExt, paths::RemotePathBuf};
+use util::{get_default_system_shell, get_system_shell, maybe};
 
-/// The directory inside a Python virtual environment that contains executables
-const PYTHON_VENV_BIN_DIR: &str = if cfg!(target_os = "windows") {
-    "Scripts"
-} else {
-    "bin"
-};
+use crate::{Project, ProjectPath};
 
 pub struct Terminals {
     pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>,
 }
 
-/// Terminals are opened either for the users shell, or to run a task.
-
-#[derive(Debug)]
-pub enum TerminalKind {
-    /// Run a shell at the given path (or $HOME if None)
-    Shell(Option<PathBuf>),
-    /// Run a task.
-    Task(SpawnInTerminal),
-}
-
 impl Project {
     pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
         self.active_entry()
@@ -58,54 +42,33 @@ impl Project {
         }
     }
 
-    pub fn create_terminal(
+    pub fn create_terminal_task(
         &mut self,
-        kind: TerminalKind,
+        spawn_task: SpawnInTerminal,
         cx: &mut Context<Self>,
     ) -> Task<Result<Entity<Terminal>>> {
-        let path: Option<Arc<Path>> = match &kind {
-            TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())),
-            TerminalKind::Task(spawn_task) => {
-                if let Some(cwd) = &spawn_task.cwd {
-                    Some(Arc::from(cwd.as_ref()))
-                } else {
-                    self.active_project_directory(cx)
-                }
-            }
-        };
-
-        let mut settings_location = None;
-        if let Some(path) = path.as_ref()
-            && let Some((worktree, _)) = self.find_worktree(path, cx)
-        {
-            settings_location = Some(SettingsLocation {
-                worktree_id: worktree.read(cx).id(),
-                path,
+        let is_via_remote = self.remote_client.is_some();
+        let project_path_context = self
+            .active_entry()
+            .and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx))
+            .or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id()))
+            .map(|worktree_id| ProjectPath {
+                worktree_id,
+                path: Arc::from(Path::new("")),
             });
-        }
-        let venv = TerminalSettings::get(settings_location, cx)
-            .detect_venv
-            .clone();
 
-        cx.spawn(async move |project, cx| {
-            let python_venv_directory = if let Some(path) = path {
-                project
-                    .update(cx, |this, cx| this.python_venv_directory(path, venv, cx))?
-                    .await
+        let path: Option<Arc<Path>> = if let Some(cwd) = &spawn_task.cwd {
+            if is_via_remote {
+                Some(Arc::from(cwd.as_ref()))
             } else {
-                None
-            };
-            project.update(cx, |project, cx| {
-                project.create_terminal_with_venv(kind, python_venv_directory, cx)
-            })?
-        })
-    }
+                let cwd = cwd.to_string_lossy();
+                let tilde_substituted = shellexpand::tilde(&cwd);
+                Some(Arc::from(Path::new(tilde_substituted.as_ref())))
+            }
+        } else {
+            self.active_project_directory(cx)
+        };
 
-    pub fn terminal_settings<'a>(
-        &'a self,
-        path: &'a Option<PathBuf>,
-        cx: &'a App,
-    ) -> &'a TerminalSettings {
         let mut settings_location = None;
         if let Some(path) = path.as_ref()
             && let Some((worktree, _)) = self.find_worktree(path, cx)
@@ -115,74 +78,176 @@ impl Project {
                 path,
             });
         }
-        TerminalSettings::get(settings_location, cx)
-    }
+        let settings = TerminalSettings::get(settings_location, cx).clone();
+        let detect_venv = settings.detect_venv.as_option().is_some();
 
-    pub fn exec_in_shell(&self, command: String, cx: &App) -> Result<std::process::Command> {
-        let path = self.first_project_directory(cx);
-        let remote_client = self.remote_client.as_ref();
-        let settings = self.terminal_settings(&path, cx).clone();
-        let remote_shell = remote_client
-            .as_ref()
-            .and_then(|remote_client| remote_client.read(cx).shell());
-        let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive();
-        let (command, args) = builder.build(Some(command), &Vec::new());
+        let (completion_tx, completion_rx) = bounded(1);
 
+        // Start with the environment that we might have inherited from the Zed CLI.
         let mut env = self
             .environment
             .read(cx)
             .get_cli_environment()
             .unwrap_or_default();
+        // Then extend it with the explicit env variables from the settings, so they take
+        // precedence.
         env.extend(settings.env);
 
-        match remote_client {
-            Some(remote_client) => {
-                let command_template =
-                    remote_client
-                        .read(cx)
-                        .build_command(Some(command), &args, &env, None, None)?;
-                let mut command = std::process::Command::new(command_template.program);
-                command.args(command_template.args);
-                command.envs(command_template.env);
-                Ok(command)
-            }
-            None => {
-                let mut command = std::process::Command::new(command);
-                command.args(args);
-                command.envs(env);
-                if let Some(path) = path {
-                    command.current_dir(path);
-                }
-                Ok(command)
-            }
-        }
+        let local_path = if is_via_remote { None } else { path.clone() };
+        let task_state = Some(TaskState {
+            id: spawn_task.id,
+            full_label: spawn_task.full_label,
+            label: spawn_task.label,
+            command_label: spawn_task.command_label,
+            hide: spawn_task.hide,
+            status: TaskStatus::Running,
+            show_summary: spawn_task.show_summary,
+            show_command: spawn_task.show_command,
+            show_rerun: spawn_task.show_rerun,
+            completion_rx,
+        });
+        let remote_client = self.remote_client.clone();
+        let shell = match &remote_client {
+            Some(remote_client) => remote_client
+                .read(cx)
+                .shell()
+                .unwrap_or_else(get_default_system_shell),
+            None => match &settings.shell {
+                Shell::Program(program) => program.clone(),
+                Shell::WithArguments {
+                    program,
+                    args: _,
+                    title_override: _,
+                } => program.clone(),
+                Shell::System => get_system_shell(),
+            },
+        };
+
+        let toolchain = project_path_context
+            .filter(|_| detect_venv)
+            .map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx));
+        let lang_registry = self.languages.clone();
+        let fs = self.fs.clone();
+        cx.spawn(async move |project, cx| {
+            let activation_script = maybe!(async {
+                let toolchain = toolchain?.await?;
+                lang_registry
+                    .language_for_name(&toolchain.language_name.0)
+                    .await
+                    .ok()?
+                    .toolchain_lister()?
+                    .activation_script(&toolchain, fs.as_ref())
+                    .await
+            })
+            .await;
+
+            project.update(cx, move |this, cx| {
+                let shell = {
+                    env.extend(spawn_task.env);
+                    match remote_client {
+                        Some(remote_client) => create_remote_shell(
+                            spawn_task
+                                .command
+                                .as_ref()
+                                .map(|command| (command, &spawn_task.args)),
+                            &mut env,
+                            path,
+                            remote_client,
+                            activation_script.clone(),
+                            cx,
+                        )?,
+                        None => match activation_script.clone() {
+                            Some(activation_script) => {
+                                let to_run = if let Some(command) = spawn_task.command {
+                                    let command: Option<Cow<str>> = shlex::try_quote(&command).ok();
+                                    let args = spawn_task
+                                        .args
+                                        .iter()
+                                        .filter_map(|arg| shlex::try_quote(arg).ok());
+                                    command.into_iter().chain(args).join(" ")
+                                } else {
+                                    format!("exec {shell} -l")
+                                };
+                                Shell::WithArguments {
+                                    program: get_default_system_shell(),
+                                    args: vec![
+                                        "-c".to_owned(),
+                                        format!("{activation_script}; {to_run}",),
+                                    ],
+                                    title_override: None,
+                                }
+                            }
+                            None => {
+                                if let Some(program) = spawn_task.command {
+                                    Shell::WithArguments {
+                                        program,
+                                        args: spawn_task.args,
+                                        title_override: None,
+                                    }
+                                } else {
+                                    Shell::System
+                                }
+                            }
+                        },
+                    }
+                };
+                TerminalBuilder::new(
+                    local_path.map(|path| path.to_path_buf()),
+                    task_state,
+                    shell,
+                    env,
+                    settings.cursor_shape.unwrap_or_default(),
+                    settings.alternate_scroll,
+                    settings.max_scroll_history_lines,
+                    is_via_remote,
+                    cx.entity_id().as_u64(),
+                    Some(completion_tx),
+                    cx,
+                    activation_script,
+                )
+                .map(|builder| {
+                    let terminal_handle = cx.new(|cx| builder.subscribe(cx));
+
+                    this.terminals
+                        .local_handles
+                        .push(terminal_handle.downgrade());
+
+                    let id = terminal_handle.entity_id();
+                    cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
+                        let handles = &mut project.terminals.local_handles;
+
+                        if let Some(index) = handles
+                            .iter()
+                            .position(|terminal| terminal.entity_id() == id)
+                        {
+                            handles.remove(index);
+                            cx.notify();
+                        }
+                    })
+                    .detach();
+
+                    terminal_handle
+                })
+            })?
+        })
     }
 
-    pub fn create_terminal_with_venv(
+    pub fn create_terminal_shell(
         &mut self,
-        kind: TerminalKind,
-        python_venv_directory: Option<PathBuf>,
+        cwd: Option<PathBuf>,
         cx: &mut Context<Self>,
-    ) -> Result<Entity<Terminal>> {
+    ) -> Task<Result<Entity<Terminal>>> {
+        let project_path_context = self
+            .active_entry()
+            .and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx))
+            .or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id()))
+            .map(|worktree_id| ProjectPath {
+                worktree_id,
+                path: Arc::from(Path::new("")),
+            });
+        let path = cwd.map(|p| Arc::from(&*p));
         let is_via_remote = self.remote_client.is_some();
 
-        let path: Option<Arc<Path>> = match &kind {
-            TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())),
-            TerminalKind::Task(spawn_task) => {
-                if let Some(cwd) = &spawn_task.cwd {
-                    if is_via_remote {
-                        Some(Arc::from(cwd.as_ref()))
-                    } else {
-                        let cwd = cwd.to_string_lossy();
-                        let tilde_substituted = shellexpand::tilde(&cwd);
-                        Some(Arc::from(Path::new(tilde_substituted.as_ref())))
-                    }
-                } else {
-                    self.active_project_directory(cx)
-                }
-            }
-        };
-
         let mut settings_location = None;
         if let Some(path) = path.as_ref()
             && let Some((worktree, _)) = self.find_worktree(path, cx)
@@ -193,8 +258,7 @@ impl Project {
             });
         }
         let settings = TerminalSettings::get(settings_location, cx).clone();
-
-        let (completion_tx, completion_rx) = bounded(1);
+        let detect_venv = settings.detect_venv.as_option().is_some();
 
         // Start with the environment that we might have inherited from the Zed CLI.
         let mut env = self
@@ -208,107 +272,111 @@ impl Project {
 
         let local_path = if is_via_remote { None } else { path.clone() };
 
-        let mut python_venv_activate_command = Task::ready(None);
-
+        let toolchain = project_path_context
+            .filter(|_| detect_venv)
+            .map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx));
         let remote_client = self.remote_client.clone();
-        let spawn_task;
-        let shell;
-        match kind {
-            TerminalKind::Shell(_) => {
-                if let Some(python_venv_directory) = &python_venv_directory {
-                    python_venv_activate_command = self.python_activate_command(
-                        python_venv_directory,
-                        &settings.detect_venv,
-                        &settings.shell,
-                        cx,
-                    );
-                }
-
-                spawn_task = None;
-                shell = match remote_client {
-                    Some(remote_client) => {
-                        create_remote_shell(None, &mut env, path, remote_client, cx)?
-                    }
-                    None => settings.shell,
-                };
-            }
-            TerminalKind::Task(task) => {
-                env.extend(task.env);
-
-                if let Some(venv_path) = &python_venv_directory {
-                    env.insert(
-                        "VIRTUAL_ENV".to_string(),
-                        venv_path.to_string_lossy().to_string(),
-                    );
-                }
-
-                spawn_task = Some(TaskState {
-                    id: task.id,
-                    full_label: task.full_label,
-                    label: task.label,
-                    command_label: task.command_label,
-                    hide: task.hide,
-                    status: TaskStatus::Running,
-                    show_summary: task.show_summary,
-                    show_command: task.show_command,
-                    show_rerun: task.show_rerun,
-                    completion_rx,
-                });
-                shell = match remote_client {
-                    Some(remote_client) => {
-                        let path_style = remote_client.read(cx).path_style();
-                        if let Some(venv_directory) = &python_venv_directory
-                            && let Ok(str) =
-                                shlex::try_quote(venv_directory.to_string_lossy().as_ref())
-                        {
-                            let path =
-                                RemotePathBuf::new(PathBuf::from(str.to_string()), path_style)
-                                    .to_string();
-                            env.insert("PATH".into(), format!("{}:$PATH ", path));
-                        }
+        let shell = match &remote_client {
+            Some(remote_client) => remote_client
+                .read(cx)
+                .shell()
+                .unwrap_or_else(get_default_system_shell),
+            None => match &settings.shell {
+                Shell::Program(program) => program.clone(),
+                Shell::WithArguments {
+                    program,
+                    args: _,
+                    title_override: _,
+                } => program.clone(),
+                Shell::System => get_system_shell(),
+            },
+        };
 
-                        create_remote_shell(
-                            task.command.as_ref().map(|command| (command, &task.args)),
+        let lang_registry = self.languages.clone();
+        let fs = self.fs.clone();
+        cx.spawn(async move |project, cx| {
+            let activation_script = maybe!(async {
+                let toolchain = toolchain?.await?;
+                let language = lang_registry
+                    .language_for_name(&toolchain.language_name.0)
+                    .await
+                    .ok();
+                let lister = language?.toolchain_lister();
+                lister?.activation_script(&toolchain, fs.as_ref()).await
+            })
+            .await;
+            project.update(cx, move |this, cx| {
+                let shell = {
+                    match remote_client {
+                        Some(remote_client) => create_remote_shell(
+                            None,
                             &mut env,
                             path,
                             remote_client,
+                            activation_script.clone(),
                             cx,
-                        )?
+                        )?,
+                        None => match activation_script.clone() {
+                            Some(activation_script) => Shell::WithArguments {
+                                program: get_default_system_shell(),
+                                args: vec![
+                                    "-c".to_owned(),
+                                    format!("{activation_script}; exec {shell} -l",),
+                                ],
+                                title_override: Some(shell.into()),
+                            },
+                            None => settings.shell,
+                        },
                     }
-                    None => {
-                        if let Some(venv_path) = &python_venv_directory {
-                            add_environment_path(&mut env, &venv_path.join(PYTHON_VENV_BIN_DIR))
-                                .log_err();
+                };
+                TerminalBuilder::new(
+                    local_path.map(|path| path.to_path_buf()),
+                    None,
+                    shell,
+                    env,
+                    settings.cursor_shape.unwrap_or_default(),
+                    settings.alternate_scroll,
+                    settings.max_scroll_history_lines,
+                    is_via_remote,
+                    cx.entity_id().as_u64(),
+                    None,
+                    cx,
+                    activation_script,
+                )
+                .map(|builder| {
+                    let terminal_handle = cx.new(|cx| builder.subscribe(cx));
+
+                    this.terminals
+                        .local_handles
+                        .push(terminal_handle.downgrade());
+
+                    let id = terminal_handle.entity_id();
+                    cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
+                        let handles = &mut project.terminals.local_handles;
+
+                        if let Some(index) = handles
+                            .iter()
+                            .position(|terminal| terminal.entity_id() == id)
+                        {
+                            handles.remove(index);
+                            cx.notify();
                         }
+                    })
+                    .detach();
 
-                        if let Some(program) = task.command {
-                            Shell::WithArguments {
-                                program,
-                                args: task.args,
-                                title_override: None,
-                            }
-                        } else {
-                            Shell::System
-                        }
-                    }
-                };
-            }
-        };
-        TerminalBuilder::new(
-            local_path.map(|path| path.to_path_buf()),
-            python_venv_directory,
-            spawn_task,
-            shell,
-            env,
-            settings.cursor_shape.unwrap_or_default(),
-            settings.alternate_scroll,
-            settings.max_scroll_history_lines,
-            is_via_remote,
-            cx.entity_id().as_u64(),
-            completion_tx,
-            cx,
-        )
-        .map(|builder| {
+                    terminal_handle
+                })
+            })?
+        })
+    }
+
+    pub fn clone_terminal(
+        &mut self,
+        terminal: &Entity<Terminal>,
+        cx: &mut Context<'_, Project>,
+        cwd: impl FnOnce() -> Option<PathBuf>,
+    ) -> Result<Entity<Terminal>> {
+        terminal.read(cx).clone_builder(cx, cwd).map(|builder| {
             let terminal_handle = cx.new(|cx| builder.subscribe(cx));
 
             self.terminals
@@ -329,211 +397,72 @@ impl Project {
             })
             .detach();
 
-            self.activate_python_virtual_environment(
-                python_venv_activate_command,
-                &terminal_handle,
-                cx,
-            );
-
             terminal_handle
         })
     }
 
-    fn python_venv_directory(
-        &self,
-        abs_path: Arc<Path>,
-        venv_settings: VenvSettings,
-        cx: &Context<Project>,
-    ) -> Task<Option<PathBuf>> {
-        cx.spawn(async move |this, cx| {
-            if let Some((worktree, relative_path)) = this
-                .update(cx, |this, cx| this.find_worktree(&abs_path, cx))
-                .ok()?
-            {
-                let toolchain = this
-                    .update(cx, |this, cx| {
-                        this.active_toolchain(
-                            ProjectPath {
-                                worktree_id: worktree.read(cx).id(),
-                                path: relative_path.into(),
-                            },
-                            LanguageName::new("Python"),
-                            cx,
-                        )
-                    })
-                    .ok()?
-                    .await;
-
-                if let Some(toolchain) = toolchain {
-                    let toolchain_path = Path::new(toolchain.path.as_ref());
-                    return Some(toolchain_path.parent()?.parent()?.to_path_buf());
-                }
-            }
-            let venv_settings = venv_settings.as_option()?;
-            this.update(cx, move |this, cx| {
-                if let Some(path) = this.find_venv_in_worktree(&abs_path, &venv_settings, cx) {
-                    return Some(path);
-                }
-                this.find_venv_on_filesystem(&abs_path, &venv_settings, cx)
-            })
-            .ok()
-            .flatten()
-        })
-    }
-
-    fn find_venv_in_worktree(
-        &self,
-        abs_path: &Path,
-        venv_settings: &terminal_settings::VenvSettingsContent,
-        cx: &App,
-    ) -> Option<PathBuf> {
-        venv_settings
-            .directories
-            .iter()
-            .map(|name| abs_path.join(name))
-            .find(|venv_path| {
-                let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR);
-                self.find_worktree(&bin_path, cx)
-                    .and_then(|(worktree, relative_path)| {
-                        worktree.read(cx).entry_for_path(&relative_path)
-                    })
-                    .is_some_and(|entry| entry.is_dir())
-            })
-    }
-
-    fn find_venv_on_filesystem(
-        &self,
-        abs_path: &Path,
-        venv_settings: &terminal_settings::VenvSettingsContent,
-        cx: &App,
-    ) -> Option<PathBuf> {
-        let (worktree, _) = self.find_worktree(abs_path, cx)?;
-        let fs = worktree.read(cx).as_local()?.fs();
-        venv_settings
-            .directories
-            .iter()
-            .map(|name| abs_path.join(name))
-            .find(|venv_path| {
-                let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR);
-                // One-time synchronous check is acceptable for terminal/task initialization
-                smol::block_on(fs.metadata(&bin_path))
-                    .ok()
-                    .flatten()
-                    .is_some_and(|meta| meta.is_dir)
-            })
-    }
-
-    fn activate_script_kind(shell: Option<&str>) -> ActivateScript {
-        let shell_env = std::env::var("SHELL").ok();
-        let shell_path = shell.or_else(|| shell_env.as_deref());
-        let shell = std::path::Path::new(shell_path.unwrap_or(""))
-            .file_name()
-            .and_then(|name| name.to_str())
-            .unwrap_or("");
-        match shell {
-            "fish" => ActivateScript::Fish,
-            "tcsh" => ActivateScript::Csh,
-            "nu" => ActivateScript::Nushell,
-            "powershell" | "pwsh" => ActivateScript::PowerShell,
-            _ => ActivateScript::Default,
+    pub fn terminal_settings<'a>(
+        &'a self,
+        path: &'a Option<PathBuf>,
+        cx: &'a App,
+    ) -> &'a TerminalSettings {
+        let mut settings_location = None;
+        if let Some(path) = path.as_ref()
+            && let Some((worktree, _)) = self.find_worktree(path, cx)
+        {
+            settings_location = Some(SettingsLocation {
+                worktree_id: worktree.read(cx).id(),
+                path,
+            });
         }
+        TerminalSettings::get(settings_location, cx)
     }
 
-    fn python_activate_command(
-        &self,
-        venv_base_directory: &Path,
-        venv_settings: &VenvSettings,
-        shell: &Shell,
-        cx: &mut App,
-    ) -> Task<Option<String>> {
-        let Some(venv_settings) = venv_settings.as_option() else {
-            return Task::ready(None);
-        };
-        let activate_keyword = match venv_settings.activate_script {
-            terminal_settings::ActivateScript::Default => match std::env::consts::OS {
-                "windows" => ".",
-                _ => ".",
-            },
-            terminal_settings::ActivateScript::Nushell => "overlay use",
-            terminal_settings::ActivateScript::PowerShell => ".",
-            terminal_settings::ActivateScript::Pyenv => "pyenv",
-            _ => "source",
-        };
-        let script_kind =
-            if venv_settings.activate_script == terminal_settings::ActivateScript::Default {
-                match shell {
-                    Shell::Program(program) => Self::activate_script_kind(Some(program)),
-                    Shell::WithArguments {
-                        program,
-                        args: _,
-                        title_override: _,
-                    } => Self::activate_script_kind(Some(program)),
-                    Shell::System => Self::activate_script_kind(None),
-                }
-            } else {
-                venv_settings.activate_script
-            };
-
-        let activate_script_name = match script_kind {
-            terminal_settings::ActivateScript::Default
-            | terminal_settings::ActivateScript::Pyenv => "activate",
-            terminal_settings::ActivateScript::Csh => "activate.csh",
-            terminal_settings::ActivateScript::Fish => "activate.fish",
-            terminal_settings::ActivateScript::Nushell => "activate.nu",
-            terminal_settings::ActivateScript::PowerShell => "activate.ps1",
-        };
+    pub fn exec_in_shell(&self, command: String, cx: &App) -> Result<std::process::Command> {
+        let path = self.first_project_directory(cx);
+        let remote_client = self.remote_client.as_ref();
+        let settings = self.terminal_settings(&path, cx).clone();
+        let remote_shell = remote_client
+            .as_ref()
+            .and_then(|remote_client| remote_client.read(cx).shell());
+        let builder = ShellBuilder::new(remote_shell.as_deref(), &settings.shell).non_interactive();
+        let (command, args) = builder.build(Some(command), &Vec::new());
 
-        let line_ending = match std::env::consts::OS {
-            "windows" => "\r",
-            _ => "\n",
-        };
+        let mut env = self
+            .environment
+            .read(cx)
+            .get_cli_environment()
+            .unwrap_or_default();
+        env.extend(settings.env);
 
-        if venv_settings.venv_name.is_empty() {
-            let path = venv_base_directory
-                .join(PYTHON_VENV_BIN_DIR)
-                .join(activate_script_name)
-                .to_string_lossy()
-                .to_string();
-
-            let is_valid_path = self.resolve_abs_path(path.as_ref(), cx);
-            cx.background_spawn(async move {
-                let quoted = shlex::try_quote(&path).ok()?;
-                if is_valid_path.await.is_some_and(|meta| meta.is_file()) {
-                    Some(format!(
-                        "{} {} ; clear{}",
-                        activate_keyword, quoted, line_ending
-                    ))
-                } else {
-                    None
+        match remote_client {
+            Some(remote_client) => {
+                let command_template = remote_client.read(cx).build_command(
+                    Some(command),
+                    &args,
+                    &env,
+                    None,
+                    // todo
+                    None,
+                    None,
+                )?;
+                let mut command = std::process::Command::new(command_template.program);
+                command.args(command_template.args);
+                command.envs(command_template.env);
+                Ok(command)
+            }
+            None => {
+                let mut command = std::process::Command::new(command);
+                command.args(args);
+                command.envs(env);
+                if let Some(path) = path {
+                    command.current_dir(path);
                 }
-            })
-        } else {
-            Task::ready(Some(format!(
-                "{activate_keyword} {activate_script_name} {name}; clear{line_ending}",
-                name = venv_settings.venv_name
-            )))
+                Ok(command)
+            }
         }
     }
 
-    fn activate_python_virtual_environment(
-        &self,
-        command: Task<Option<String>>,
-        terminal_handle: &Entity<Terminal>,
-        cx: &mut App,
-    ) {
-        terminal_handle.update(cx, |_, cx| {
-            cx.spawn(async move |this, cx| {
-                if let Some(command) = command.await {
-                    this.update(cx, |this, _| {
-                        this.input(command.into_bytes());
-                    })
-                    .ok();
-                }
-            })
-            .detach()
-        });
-    }
-
     pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {
         &self.terminals.local_handles
     }
@@ -544,6 +473,7 @@ fn create_remote_shell(
     env: &mut HashMap<String, String>,
     working_directory: Option<Arc<Path>>,
     remote_client: Entity<RemoteClient>,
+    activation_script: Option<String>,
     cx: &mut App,
 ) -> Result<Shell> {
     // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
@@ -563,6 +493,7 @@ fn create_remote_shell(
         args.as_slice(),
         env,
         working_directory.map(|path| path.display().to_string()),
+        activation_script,
         None,
     )?;
     *env = command.env;
@@ -576,57 +507,3 @@ fn create_remote_shell(
         title_override: Some(format!("{} — Terminal", host).into()),
     })
 }
-
-fn add_environment_path(env: &mut HashMap<String, String>, new_path: &Path) -> Result<()> {
-    let mut env_paths = vec![new_path.to_path_buf()];
-    if let Some(path) = env.get("PATH").or(env::var("PATH").ok().as_ref()) {
-        let mut paths = std::env::split_paths(&path).collect::<Vec<_>>();
-        env_paths.append(&mut paths);
-    }
-
-    let paths = std::env::join_paths(env_paths).context("failed to create PATH env variable")?;
-    env.insert("PATH".to_string(), paths.to_string_lossy().to_string());
-
-    Ok(())
-}
-
-#[cfg(test)]
-mod tests {
-    use collections::HashMap;
-
-    #[test]
-    fn test_add_environment_path_with_existing_path() {
-        let tmp_path = std::path::PathBuf::from("/tmp/new");
-        let mut env = HashMap::default();
-        let old_path = if cfg!(windows) {
-            "/usr/bin;/usr/local/bin"
-        } else {
-            "/usr/bin:/usr/local/bin"
-        };
-        env.insert("PATH".to_string(), old_path.to_string());
-        env.insert("OTHER".to_string(), "aaa".to_string());
-
-        super::add_environment_path(&mut env, &tmp_path).unwrap();
-        if cfg!(windows) {
-            assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new;{}", old_path));
-        } else {
-            assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new:{}", old_path));
-        }
-        assert_eq!(env.get("OTHER").unwrap(), "aaa");
-    }
-
-    #[test]
-    fn test_add_environment_path_with_empty_path() {
-        let tmp_path = std::path::PathBuf::from("/tmp/new");
-        let mut env = HashMap::default();
-        env.insert("OTHER".to_string(), "aaa".to_string());
-        let os_path = std::env::var("PATH").unwrap();
-        super::add_environment_path(&mut env, &tmp_path).unwrap();
-        if cfg!(windows) {
-            assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new;{}", os_path));
-        } else {
-            assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new:{}", os_path));
-        }
-        assert_eq!(env.get("OTHER").unwrap(), "aaa");
-    }
-}

crates/proto/build.rs 🔗

@@ -1,4 +1,5 @@
 fn main() {
+    println!("cargo:rerun-if-changed=proto");
     let mut build = prost_build::Config::new();
     build
         .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")

crates/remote/src/remote_client.rs 🔗

@@ -757,6 +757,7 @@ impl RemoteClient {
         args: &[String],
         env: &HashMap<String, String>,
         working_dir: Option<String>,
+        activation_script: Option<String>,
         port_forward: Option<(u16, String, u16)>,
     ) -> Result<CommandTemplate> {
         let Some(connection) = self
@@ -766,7 +767,14 @@ impl RemoteClient {
         else {
             return Err(anyhow!("no connection"));
         };
-        connection.build_command(program, args, env, working_dir, port_forward)
+        connection.build_command(
+            program,
+            args,
+            env,
+            working_dir,
+            activation_script,
+            port_forward,
+        )
     }
 
     pub fn upload_directory(
@@ -998,6 +1006,7 @@ pub(crate) trait RemoteConnection: Send + Sync {
         args: &[String],
         env: &HashMap<String, String>,
         working_dir: Option<String>,
+        activation_script: Option<String>,
         port_forward: Option<(u16, String, u16)>,
     ) -> Result<CommandTemplate>;
     fn connection_options(&self) -> SshConnectionOptions;
@@ -1364,6 +1373,7 @@ mod fake {
             args: &[String],
             env: &HashMap<String, String>,
             _: Option<String>,
+            _: Option<String>,
             _: Option<(u16, String, u16)>,
         ) -> Result<CommandTemplate> {
             let ssh_program = program.unwrap_or_else(|| "sh".to_string());

crates/remote/src/transport/ssh.rs 🔗

@@ -30,7 +30,10 @@ use std::{
     time::Instant,
 };
 use tempfile::TempDir;
-use util::paths::{PathStyle, RemotePathBuf};
+use util::{
+    get_default_system_shell,
+    paths::{PathStyle, RemotePathBuf},
+};
 
 pub(crate) struct SshRemoteConnection {
     socket: SshSocket,
@@ -113,6 +116,7 @@ impl RemoteConnection for SshRemoteConnection {
         input_args: &[String],
         input_env: &HashMap<String, String>,
         working_dir: Option<String>,
+        activation_script: Option<String>,
         port_forward: Option<(u16, String, u16)>,
     ) -> Result<CommandTemplate> {
         use std::fmt::Write as _;
@@ -134,6 +138,9 @@ impl RemoteConnection for SshRemoteConnection {
         } else {
             write!(&mut script, "cd; ").unwrap();
         };
+        if let Some(activation_script) = activation_script {
+            write!(&mut script, " {activation_script};").unwrap();
+        }
 
         for (k, v) in input_env.iter() {
             if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) {
@@ -155,7 +162,8 @@ impl RemoteConnection for SshRemoteConnection {
             write!(&mut script, "exec {shell} -l").unwrap();
         };
 
-        let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&script).unwrap());
+        let sys_shell = get_default_system_shell();
+        let shell_invocation = format!("{sys_shell} -c {}", shlex::try_quote(&script).unwrap());
 
         let mut args = Vec::new();
         args.extend(self.socket.ssh_args());
@@ -167,7 +175,6 @@ impl RemoteConnection for SshRemoteConnection {
 
         args.push("-t".into());
         args.push(shell_invocation);
-
         Ok(CommandTemplate {
             program: "ssh".into(),
             args,

crates/task/src/shell_builder.rs 🔗

@@ -1,3 +1,7 @@
+use std::fmt;
+
+use util::get_system_shell;
+
 use crate::Shell;
 
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
@@ -11,9 +15,22 @@ pub enum ShellKind {
     Cmd,
 }
 
+impl fmt::Display for ShellKind {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            ShellKind::Posix => write!(f, "sh"),
+            ShellKind::Csh => write!(f, "csh"),
+            ShellKind::Fish => write!(f, "fish"),
+            ShellKind::Powershell => write!(f, "powershell"),
+            ShellKind::Nushell => write!(f, "nu"),
+            ShellKind::Cmd => write!(f, "cmd"),
+        }
+    }
+}
+
 impl ShellKind {
     pub fn system() -> Self {
-        Self::new(&system_shell())
+        Self::new(&get_system_shell())
     }
 
     pub fn new(program: &str) -> Self {
@@ -22,12 +39,12 @@ impl ShellKind {
         #[cfg(not(windows))]
         let (_, program) = program.rsplit_once('/').unwrap_or(("", program));
         if program == "powershell"
-            || program == "powershell.exe"
+            || program.ends_with("powershell.exe")
             || program == "pwsh"
-            || program == "pwsh.exe"
+            || program.ends_with("pwsh.exe")
         {
             ShellKind::Powershell
-        } else if program == "cmd" || program == "cmd.exe" {
+        } else if program == "cmd" || program.ends_with("cmd.exe") {
             ShellKind::Cmd
         } else if program == "nu" {
             ShellKind::Nushell
@@ -178,18 +195,6 @@ impl ShellKind {
     }
 }
 
-fn system_shell() -> String {
-    if cfg!(target_os = "windows") {
-        // `alacritty_terminal` uses this as default on Windows. See:
-        // https://github.com/alacritty/alacritty/blob/0d4ab7bca43213d96ddfe40048fc0f922543c6f8/alacritty_terminal/src/tty/windows/mod.rs#L130
-        // We could use `util::get_windows_system_shell()` here, but we are running tasks here, so leave it to `powershell.exe`
-        // should be okay.
-        "powershell.exe".to_string()
-    } else {
-        std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
-    }
-}
-
 /// ShellBuilder is used to turn a user-requested task into a
 /// program that can be executed by the shell.
 pub struct ShellBuilder {
@@ -206,7 +211,7 @@ impl ShellBuilder {
         let (program, args) = match remote_system_shell {
             Some(program) => (program.to_string(), Vec::new()),
             None => match shell {
-                Shell::System => (system_shell(), Vec::new()),
+                Shell::System => (get_system_shell(), Vec::new()),
                 Shell::Program(shell) => (shell.clone(), Vec::new()),
                 Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
             },

crates/terminal/src/terminal.rs 🔗

@@ -344,7 +344,6 @@ pub struct TerminalBuilder {
 impl TerminalBuilder {
     pub fn new(
         working_directory: Option<PathBuf>,
-        python_venv_directory: Option<PathBuf>,
         task: Option<TaskState>,
         shell: Shell,
         mut env: HashMap<String, String>,
@@ -353,8 +352,9 @@ impl TerminalBuilder {
         max_scroll_history_lines: Option<usize>,
         is_ssh_terminal: bool,
         window_id: u64,
-        completion_tx: Sender<Option<ExitStatus>>,
+        completion_tx: Option<Sender<Option<ExitStatus>>>,
         cx: &App,
+        activation_script: Option<String>,
     ) -> Result<TerminalBuilder> {
         // If the parent environment doesn't have a locale set
         // (As is the case when launched from a .app on MacOS),
@@ -428,13 +428,10 @@ impl TerminalBuilder {
                     .clone()
                     .or_else(|| Some(home_dir().to_path_buf())),
                 drain_on_exit: true,
-                env: env.into_iter().collect(),
+                env: env.clone().into_iter().collect(),
             }
         };
 
-        // Setup Alacritty's env, which modifies the current process's environment
-        alacritty_terminal::tty::setup_env();
-
         let default_cursor_style = AlacCursorStyle::from(cursor_shape);
         let scrolling_history = if task.is_some() {
             // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling.
@@ -517,11 +514,19 @@ impl TerminalBuilder {
             hyperlink_regex_searches: RegexSearches::new(),
             vi_mode_enabled: false,
             is_ssh_terminal,
-            python_venv_directory,
             last_mouse_move_time: Instant::now(),
             last_hyperlink_search_position: None,
             #[cfg(windows)]
             shell_program,
+            activation_script,
+            template: CopyTemplate {
+                shell,
+                env,
+                cursor_shape,
+                alternate_scroll,
+                max_scroll_history_lines,
+                window_id,
+            },
         };
 
         Ok(TerminalBuilder {
@@ -683,7 +688,7 @@ pub enum SelectionPhase {
 
 pub struct Terminal {
     pty_tx: Notifier,
-    completion_tx: Sender<Option<ExitStatus>>,
+    completion_tx: Option<Sender<Option<ExitStatus>>>,
     term: Arc<FairMutex<Term<ZedListener>>>,
     term_config: Config,
     events: VecDeque<InternalEvent>,
@@ -695,7 +700,6 @@ pub struct Terminal {
     pub breadcrumb_text: String,
     pub pty_info: PtyProcessInfo,
     title_override: Option<SharedString>,
-    pub python_venv_directory: Option<PathBuf>,
     scroll_px: Pixels,
     next_link_id: usize,
     selection_phase: SelectionPhase,
@@ -707,6 +711,17 @@ pub struct Terminal {
     last_hyperlink_search_position: Option<Point<Pixels>>,
     #[cfg(windows)]
     shell_program: Option<String>,
+    template: CopyTemplate,
+    activation_script: Option<String>,
+}
+
+struct CopyTemplate {
+    shell: Shell,
+    env: HashMap<String, String>,
+    cursor_shape: CursorShape,
+    alternate_scroll: AlternateScroll,
+    max_scroll_history_lines: Option<usize>,
+    window_id: u64,
 }
 
 pub struct TaskState {
@@ -1895,7 +1910,9 @@ impl Terminal {
             }
         });
 
-        self.completion_tx.try_send(e).ok();
+        if let Some(tx) = &self.completion_tx {
+            tx.try_send(e).ok();
+        }
         let task = match &mut self.task {
             Some(task) => task,
             None => {
@@ -1950,6 +1967,28 @@ impl Terminal {
     pub fn vi_mode_enabled(&self) -> bool {
         self.vi_mode_enabled
     }
+
+    pub fn clone_builder(
+        &self,
+        cx: &App,
+        cwd: impl FnOnce() -> Option<PathBuf>,
+    ) -> Result<TerminalBuilder> {
+        let working_directory = self.working_directory().or_else(cwd);
+        TerminalBuilder::new(
+            working_directory,
+            None,
+            self.template.shell.clone(),
+            self.template.env.clone(),
+            self.template.cursor_shape,
+            self.template.alternate_scroll,
+            self.template.max_scroll_history_lines,
+            self.is_ssh_terminal,
+            self.template.window_id,
+            None,
+            cx,
+            self.activation_script.clone(),
+        )
+    }
 }
 
 // Helper function to convert a grid row to a string
@@ -2164,7 +2203,6 @@ mod tests {
         let (completion_tx, completion_rx) = smol::channel::unbounded();
         let terminal = cx.new(|cx| {
             TerminalBuilder::new(
-                None,
                 None,
                 None,
                 task::Shell::WithArguments {
@@ -2178,8 +2216,9 @@ mod tests {
                 None,
                 false,
                 0,
-                completion_tx,
+                Some(completion_tx),
                 cx,
+                None,
             )
             .unwrap()
             .subscribe(cx)

crates/terminal_view/src/persistence.rs 🔗

@@ -3,9 +3,9 @@ use async_recursion::async_recursion;
 use collections::HashSet;
 use futures::{StreamExt as _, stream::FuturesUnordered};
 use gpui::{AppContext as _, AsyncWindowContext, Axis, Entity, Task, WeakEntity};
-use project::{Project, terminals::TerminalKind};
+use project::Project;
 use serde::{Deserialize, Serialize};
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
 use ui::{App, Context, Pixels, Window};
 use util::ResultExt as _;
 
@@ -246,11 +246,9 @@ async fn deserialize_pane_group(
                             .update(cx, |workspace, cx| default_working_directory(workspace, cx))
                             .ok()
                             .flatten();
-                        let kind = TerminalKind::Shell(
-                            working_directory.as_deref().map(Path::to_path_buf),
-                        );
-                        let terminal =
-                            project.update(cx, |project, cx| project.create_terminal(kind, cx));
+                        let terminal = project.update(cx, |project, cx| {
+                            project.create_terminal_shell(working_directory, cx)
+                        });
                         Some(Some(terminal))
                     } else {
                         Some(None)

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -16,7 +16,7 @@ use gpui::{
     Task, WeakEntity, Window, actions,
 };
 use itertools::Itertools;
-use project::{Fs, Project, ProjectEntryId, terminals::TerminalKind};
+use project::{Fs, Project, ProjectEntryId};
 use search::{BufferSearchBar, buffer_search::DivRegistrar};
 use settings::Settings;
 use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId};
@@ -376,14 +376,19 @@ impl TerminalPanel {
                 }
                 self.serialize(cx);
             }
-            pane::Event::Split(direction) => {
-                let Some(new_pane) = self.new_pane_with_cloned_active_terminal(window, cx) else {
-                    return;
-                };
+            &pane::Event::Split(direction) => {
+                let fut = self.new_pane_with_cloned_active_terminal(window, cx);
                 let pane = pane.clone();
-                let direction = *direction;
-                self.center.split(&pane, &new_pane, direction).log_err();
-                window.focus(&new_pane.focus_handle(cx));
+                cx.spawn_in(window, async move |panel, cx| {
+                    let Some(new_pane) = fut.await else {
+                        return;
+                    };
+                    _ = panel.update_in(cx, |panel, window, cx| {
+                        panel.center.split(&pane, &new_pane, direction).log_err();
+                        window.focus(&new_pane.focus_handle(cx));
+                    });
+                })
+                .detach();
             }
             pane::Event::Focus => {
                 self.active_pane = pane.clone();
@@ -400,57 +405,62 @@ impl TerminalPanel {
         &mut self,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<Entity<Pane>> {
-        let workspace = self.workspace.upgrade()?;
+    ) -> Task<Option<Entity<Pane>>> {
+        let Some(workspace) = self.workspace.upgrade() else {
+            return Task::ready(None);
+        };
         let workspace = workspace.read(cx);
         let database_id = workspace.database_id();
         let weak_workspace = self.workspace.clone();
         let project = workspace.project().clone();
-        let (working_directory, python_venv_directory) = self
-            .active_pane
+        let active_pane = &self.active_pane;
+        let terminal_view = active_pane
             .read(cx)
             .active_item()
-            .and_then(|item| item.downcast::<TerminalView>())
-            .map(|terminal_view| {
-                let terminal = terminal_view.read(cx).terminal().read(cx);
-                (
-                    terminal
-                        .working_directory()
-                        .or_else(|| default_working_directory(workspace, cx)),
-                    terminal.python_venv_directory.clone(),
-                )
-            })
-            .unwrap_or((None, None));
-        let kind = TerminalKind::Shell(working_directory);
-        let terminal = project
-            .update(cx, |project, cx| {
-                project.create_terminal_with_venv(kind, python_venv_directory, cx)
-            })
-            .ok()?;
-
-        let terminal_view = Box::new(cx.new(|cx| {
-            TerminalView::new(
-                terminal.clone(),
-                weak_workspace.clone(),
-                database_id,
-                project.downgrade(),
-                window,
-                cx,
-            )
-        }));
-        let pane = new_terminal_pane(
-            weak_workspace,
-            project,
-            self.active_pane.read(cx).is_zoomed(),
-            window,
-            cx,
-        );
-        self.apply_tab_bar_buttons(&pane, cx);
-        pane.update(cx, |pane, cx| {
-            pane.add_item(terminal_view, true, true, None, window, cx);
+            .and_then(|item| item.downcast::<TerminalView>());
+        let working_directory = terminal_view.as_ref().and_then(|terminal_view| {
+            let terminal = terminal_view.read(cx).terminal().read(cx);
+            terminal
+                .working_directory()
+                .or_else(|| default_working_directory(workspace, cx))
         });
+        let is_zoomed = active_pane.read(cx).is_zoomed();
+        cx.spawn_in(window, async move |panel, cx| {
+            let terminal = project
+                .update(cx, |project, cx| match terminal_view {
+                    Some(view) => Task::ready(project.clone_terminal(
+                        &view.read(cx).terminal.clone(),
+                        cx,
+                        || working_directory,
+                    )),
+                    None => project.create_terminal_shell(working_directory, cx),
+                })
+                .ok()?
+                .await
+                .ok()?;
 
-        Some(pane)
+            panel
+                .update_in(cx, move |terminal_panel, window, cx| {
+                    let terminal_view = Box::new(cx.new(|cx| {
+                        TerminalView::new(
+                            terminal.clone(),
+                            weak_workspace.clone(),
+                            database_id,
+                            project.downgrade(),
+                            window,
+                            cx,
+                        )
+                    }));
+                    let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx);
+                    terminal_panel.apply_tab_bar_buttons(&pane, cx);
+                    pane.update(cx, |pane, cx| {
+                        pane.add_item(terminal_view, true, true, None, window, cx);
+                    });
+                    Some(pane)
+                })
+                .ok()
+                .flatten()
+        })
     }
 
     pub fn open_terminal(
@@ -465,8 +475,8 @@ impl TerminalPanel {
 
         terminal_panel
             .update(cx, |panel, cx| {
-                panel.add_terminal(
-                    TerminalKind::Shell(Some(action.working_directory.clone())),
+                panel.add_terminal_shell(
+                    Some(action.working_directory.clone()),
                     RevealStrategy::Always,
                     window,
                     cx,
@@ -571,15 +581,16 @@ impl TerminalPanel {
     ) -> Task<Result<WeakEntity<Terminal>>> {
         let reveal = spawn_task.reveal;
         let reveal_target = spawn_task.reveal_target;
-        let kind = TerminalKind::Task(spawn_task);
         match reveal_target {
             RevealTarget::Center => self
                 .workspace
                 .update(cx, |workspace, cx| {
-                    Self::add_center_terminal(workspace, kind, window, cx)
+                    Self::add_center_terminal(workspace, window, cx, |project, cx| {
+                        project.create_terminal_task(spawn_task, cx)
+                    })
                 })
                 .unwrap_or_else(|e| Task::ready(Err(e))),
-            RevealTarget::Dock => self.add_terminal(kind, reveal, window, cx),
+            RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx),
         }
     }
 
@@ -594,11 +605,14 @@ impl TerminalPanel {
             return;
         };
 
-        let kind = TerminalKind::Shell(default_working_directory(workspace, cx));
-
         terminal_panel
             .update(cx, |this, cx| {
-                this.add_terminal(kind, RevealStrategy::Always, window, cx)
+                this.add_terminal_shell(
+                    default_working_directory(workspace, cx),
+                    RevealStrategy::Always,
+                    window,
+                    cx,
+                )
             })
             .detach_and_log_err(cx);
     }
@@ -660,9 +674,13 @@ impl TerminalPanel {
 
     pub fn add_center_terminal(
         workspace: &mut Workspace,
-        kind: TerminalKind,
         window: &mut Window,
         cx: &mut Context<Workspace>,
+        create_terminal: impl FnOnce(
+            &mut Project,
+            &mut Context<Project>,
+        ) -> Task<Result<Entity<Terminal>>>
+        + 'static,
     ) -> Task<Result<WeakEntity<Terminal>>> {
         if !is_enabled_in_workspace(workspace, cx) {
             return Task::ready(Err(anyhow!(
@@ -671,9 +689,7 @@ impl TerminalPanel {
         }
         let project = workspace.project().downgrade();
         cx.spawn_in(window, async move |workspace, cx| {
-            let terminal = project
-                .update(cx, |project, cx| project.create_terminal(kind, cx))?
-                .await?;
+            let terminal = project.update(cx, create_terminal)?.await?;
 
             workspace.update_in(cx, |workspace, window, cx| {
                 let terminal_view = cx.new(|cx| {
@@ -692,9 +708,9 @@ impl TerminalPanel {
         })
     }
 
-    pub fn add_terminal(
+    pub fn add_terminal_task(
         &mut self,
-        kind: TerminalKind,
+        task: SpawnInTerminal,
         reveal_strategy: RevealStrategy,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -710,7 +726,66 @@ impl TerminalPanel {
             })?;
             let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
             let terminal = project
-                .update(cx, |project, cx| project.create_terminal(kind, cx))?
+                .update(cx, |project, cx| project.create_terminal_task(task, cx))?
+                .await?;
+            let result = workspace.update_in(cx, |workspace, window, cx| {
+                let terminal_view = Box::new(cx.new(|cx| {
+                    TerminalView::new(
+                        terminal.clone(),
+                        workspace.weak_handle(),
+                        workspace.database_id(),
+                        workspace.project().downgrade(),
+                        window,
+                        cx,
+                    )
+                }));
+
+                match reveal_strategy {
+                    RevealStrategy::Always => {
+                        workspace.focus_panel::<Self>(window, cx);
+                    }
+                    RevealStrategy::NoFocus => {
+                        workspace.open_panel::<Self>(window, cx);
+                    }
+                    RevealStrategy::Never => {}
+                }
+
+                pane.update(cx, |pane, cx| {
+                    let focus = pane.has_focus(window, cx)
+                        || matches!(reveal_strategy, RevealStrategy::Always);
+                    pane.add_item(terminal_view, true, focus, None, window, cx);
+                });
+
+                Ok(terminal.downgrade())
+            })?;
+            terminal_panel.update(cx, |terminal_panel, cx| {
+                terminal_panel.pending_terminals_to_add =
+                    terminal_panel.pending_terminals_to_add.saturating_sub(1);
+                terminal_panel.serialize(cx)
+            })?;
+            result
+        })
+    }
+
+    pub fn add_terminal_shell(
+        &mut self,
+        cwd: Option<PathBuf>,
+        reveal_strategy: RevealStrategy,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<WeakEntity<Terminal>>> {
+        let workspace = self.workspace.clone();
+        cx.spawn_in(window, async move |terminal_panel, cx| {
+            if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? {
+                anyhow::bail!("terminal not yet supported for remote projects");
+            }
+            let pane = terminal_panel.update(cx, |terminal_panel, _| {
+                terminal_panel.pending_terminals_to_add += 1;
+                terminal_panel.active_pane.clone()
+            })?;
+            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
+            let terminal = project
+                .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))?
                 .await?;
             let result = workspace.update_in(cx, |workspace, window, cx| {
                 let terminal_view = Box::new(cx.new(|cx| {
@@ -819,7 +894,7 @@ impl TerminalPanel {
             })??;
             let new_terminal = project
                 .update(cx, |project, cx| {
-                    project.create_terminal(TerminalKind::Task(spawn_task), cx)
+                    project.create_terminal_task(spawn_task, cx)
                 })?
                 .await?;
             terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| {
@@ -1248,18 +1323,29 @@ impl Render for TerminalPanel {
                         let panes = terminal_panel.center.panes();
                         if let Some(&pane) = panes.get(action.0) {
                             window.focus(&pane.read(cx).focus_handle(cx));
-                        } else if let Some(new_pane) =
-                            terminal_panel.new_pane_with_cloned_active_terminal(window, cx)
-                        {
-                            terminal_panel
-                                .center
-                                .split(
-                                    &terminal_panel.active_pane,
-                                    &new_pane,
-                                    SplitDirection::Right,
-                                )
-                                .log_err();
-                            window.focus(&new_pane.focus_handle(cx));
+                        } else {
+                            let future =
+                                terminal_panel.new_pane_with_cloned_active_terminal(window, cx);
+                            cx.spawn_in(window, async move |terminal_panel, cx| {
+                                if let Some(new_pane) = future.await {
+                                    _ = terminal_panel.update_in(
+                                        cx,
+                                        |terminal_panel, window, cx| {
+                                            terminal_panel
+                                                .center
+                                                .split(
+                                                    &terminal_panel.active_pane,
+                                                    &new_pane,
+                                                    SplitDirection::Right,
+                                                )
+                                                .log_err();
+                                            let new_pane = new_pane.read(cx);
+                                            window.focus(&new_pane.focus_handle(cx));
+                                        },
+                                    );
+                                }
+                            })
+                            .detach();
                         }
                     }),
                 )
@@ -1395,13 +1481,14 @@ impl Panel for TerminalPanel {
             return;
         }
         cx.defer_in(window, |this, window, cx| {
-            let Ok(kind) = this.workspace.update(cx, |workspace, cx| {
-                TerminalKind::Shell(default_working_directory(workspace, cx))
-            }) else {
+            let Ok(kind) = this
+                .workspace
+                .update(cx, |workspace, cx| default_working_directory(workspace, cx))
+            else {
                 return;
             };
 
-            this.add_terminal(kind, RevealStrategy::Always, window, cx)
+            this.add_terminal_shell(kind, RevealStrategy::Always, window, cx)
                 .detach_and_log_err(cx)
         })
     }

crates/terminal_view/src/terminal_path_like_target.rs 🔗

@@ -364,7 +364,7 @@ fn possibly_open_target(
 mod tests {
     use super::*;
     use gpui::TestAppContext;
-    use project::{Project, terminals::TerminalKind};
+    use project::Project;
     use serde_json::json;
     use std::path::{Path, PathBuf};
     use terminal::{HoveredWord, alacritty_terminal::index::Point as AlacPoint};
@@ -405,8 +405,8 @@ mod tests {
             app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 
         let terminal = project
-            .update(cx, |project, cx| {
-                project.create_terminal(TerminalKind::Shell(None), cx)
+            .update(cx, |project: &mut Project, cx| {
+                project.create_terminal_shell(None, cx)
             })
             .await
             .expect("Failed to create a terminal");

crates/terminal_view/src/terminal_view.rs 🔗

@@ -15,7 +15,7 @@ use gpui::{
     deferred, div,
 };
 use persistence::TERMINAL_DB;
-use project::{Project, search::SearchQuery, terminals::TerminalKind};
+use project::{Project, search::SearchQuery};
 use schemars::JsonSchema;
 use task::TaskId;
 use terminal::{
@@ -204,12 +204,9 @@ impl TerminalView {
         cx: &mut Context<Workspace>,
     ) {
         let working_directory = default_working_directory(workspace, cx);
-        TerminalPanel::add_center_terminal(
-            workspace,
-            TerminalKind::Shell(working_directory),
-            window,
-            cx,
-        )
+        TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| {
+            project.create_terminal_shell(working_directory, cx)
+        })
         .detach_and_log_err(cx);
     }
 
@@ -1333,16 +1330,10 @@ impl Item for TerminalView {
         let terminal = self
             .project
             .update(cx, |project, cx| {
-                let terminal = self.terminal().read(cx);
-                let working_directory = terminal
-                    .working_directory()
-                    .or_else(|| Some(project.active_project_directory(cx)?.to_path_buf()));
-                let python_venv_directory = terminal.python_venv_directory.clone();
-                project.create_terminal_with_venv(
-                    TerminalKind::Shell(working_directory),
-                    python_venv_directory,
-                    cx,
-                )
+                let cwd = project
+                    .active_project_directory(cx)
+                    .map(|it| it.to_path_buf());
+                project.clone_terminal(self.terminal(), cx, || cwd)
             })
             .ok()?
             .log_err()?;
@@ -1498,9 +1489,7 @@ impl SerializableItem for TerminalView {
                 .flatten();
 
             let terminal = project
-                .update(cx, |project, cx| {
-                    project.create_terminal(TerminalKind::Shell(cwd), cx)
-                })?
+                .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))?
                 .await?;
             cx.update(|window, cx| {
                 cx.new(|cx| {

crates/util/src/util.rs 🔗

@@ -1057,6 +1057,18 @@ pub fn get_system_shell() -> String {
     }
 }
 
+pub fn get_default_system_shell() -> String {
+    #[cfg(target_os = "windows")]
+    {
+        get_windows_system_shell()
+    }
+
+    #[cfg(not(target_os = "windows"))]
+    {
+        "/bin/sh".to_string()
+    }
+}
+
 #[derive(Debug)]
 pub enum ConnectionResult<O> {
     Timeout,

crates/workspace/src/persistence.rs 🔗

@@ -824,7 +824,6 @@ impl WorkspaceDb {
                 conn.exec_bound(
                     sql!(
                         DELETE FROM breakpoints WHERE workspace_id = ?1;
-                        DELETE FROM toolchains WHERE workspace_id = ?1;
                     )
                 )?(workspace.id).context("Clearing old breakpoints")?;
 
@@ -1097,7 +1096,6 @@ impl WorkspaceDb {
 
     query! {
         pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
-            DELETE FROM toolchains WHERE workspace_id = ?1;
             DELETE FROM workspaces
             WHERE workspace_id IS ?
         }
@@ -1424,24 +1422,24 @@ impl WorkspaceDb {
         &self,
         workspace_id: WorkspaceId,
         worktree_id: WorktreeId,
-        relative_path: String,
+        relative_worktree_path: String,
         language_name: LanguageName,
     ) -> Result<Option<Toolchain>> {
         self.write(move |this| {
             let mut select = this
                 .select_bound(sql!(
-                    SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_path = ?
+                    SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_worktree_path = ?
                 ))
-                .context("Preparing insertion")?;
+                .context("select toolchain")?;
 
             let toolchain: Vec<(String, String, String)> =
-                select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_path))?;
+                select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_worktree_path))?;
 
             Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain {
                 name: name.into(),
                 path: path.into(),
                 language_name,
-                as_json: serde_json::Value::from_str(&raw_json).ok()?
+                as_json: serde_json::Value::from_str(&raw_json).ok()?,
             })))
         })
         .await
@@ -1456,7 +1454,7 @@ impl WorkspaceDb {
                 .select_bound(sql!(
                     SELECT name, path, worktree_id, relative_worktree_path, language_name, raw_json FROM toolchains WHERE workspace_id = ?
                 ))
-                .context("Preparing insertion")?;
+                .context("select toolchains")?;
 
             let toolchain: Vec<(String, String, u64, String, String, String)> =
                 select(workspace_id)?;
@@ -1465,7 +1463,7 @@ impl WorkspaceDb {
                 name: name.into(),
                 path: path.into(),
                 language_name: LanguageName::new(&language_name),
-                as_json: serde_json::Value::from_str(&raw_json).ok()?
+                as_json: serde_json::Value::from_str(&raw_json).ok()?,
             }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect())
         })
         .await