acp_thread: If available, use git bash over powershell in terminal tool (#39466)

Lukas Wirth created

Release Notes:

- When git bash is installed, agents will now use that over powershell
when invoking terminal commands

Change summary

Cargo.lock                                  |  1 
crates/acp_thread/src/acp_thread.rs         | 17 ++++++++--
crates/assistant_tools/src/terminal_tool.rs | 19 ++++++++---
crates/prompt_store/src/prompts.rs          |  6 ++-
crates/util/Cargo.toml                      |  1 
crates/util/src/shell.rs                    | 36 ++++++++++++++++++----
crates/util/src/util.rs                     |  4 +
7 files changed, 65 insertions(+), 19 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -20340,6 +20340,7 @@ dependencies = [
  "tendril",
  "unicase",
  "walkdir",
+ "which 6.0.3",
  "workspace-hack",
  "zed-collections",
  "zed-util-macros",

crates/acp_thread/src/acp_thread.rs 🔗

@@ -12,7 +12,7 @@ use language::language_settings::FormatOnSave;
 pub use mention::*;
 use project::lsp_store::{FormatTrigger, LspFormatTarget};
 use serde::{Deserialize, Serialize};
-use settings::Settings as _;
+use settings::{Settings as _, SettingsLocation};
 use task::{Shell, ShellBuilder};
 pub use terminal::*;
 
@@ -35,7 +35,7 @@ use std::rc::Rc;
 use std::time::{Duration, Instant};
 use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
 use ui::App;
-use util::{ResultExt, get_default_system_shell};
+use util::{ResultExt, get_default_system_shell_preferring_bash};
 use uuid::Uuid;
 
 #[derive(Debug)]
@@ -2086,7 +2086,16 @@ impl AcpThread {
     ) -> Task<Result<Entity<Terminal>>> {
         let env = match &cwd {
             Some(dir) => self.project.update(cx, |project, cx| {
-                let shell = TerminalSettings::get_global(cx).shell.clone();
+                let worktree = project.find_worktree(dir.as_path(), cx);
+                let shell = TerminalSettings::get(
+                    worktree.as_ref().map(|(worktree, path)| SettingsLocation {
+                        worktree_id: worktree.read(cx).id(),
+                        path: &path,
+                    }),
+                    cx,
+                )
+                .shell
+                .clone();
                 project.directory_environment(&shell, dir.as_path().into(), cx)
             }),
             None => Task::ready(None).shared(),
@@ -2115,7 +2124,7 @@ impl AcpThread {
                             .remote_client()
                             .and_then(|r| r.read(cx).default_system_shell())
                     })?
-                    .unwrap_or_else(|| get_default_system_shell());
+                    .unwrap_or_else(|| get_default_system_shell_preferring_bash());
                 let (task_command, task_args) = ShellBuilder::new(&Shell::Program(shell))
                     .redirect_stdin_to_dev_null()
                     .build(Some(command.clone()), &args);

crates/assistant_tools/src/terminal_tool.rs 🔗

@@ -18,7 +18,7 @@ use portable_pty::{CommandBuilder, PtySize, native_pty_system};
 use project::Project;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::{Settings, SettingsLocation};
 use std::{
     env,
     path::{Path, PathBuf},
@@ -32,8 +32,8 @@ use terminal_view::TerminalView;
 use theme::ThemeSettings;
 use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
 use util::{
-    ResultExt, get_default_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
-    time::duration_alt_display,
+    ResultExt, get_default_system_shell_preferring_bash, markdown::MarkdownInlineCode,
+    size::format_file_size, time::duration_alt_display,
 };
 use workspace::Workspace;
 
@@ -122,7 +122,16 @@ impl Tool for TerminalTool {
         let cwd = working_dir.clone();
         let env = match &cwd {
             Some(dir) => project.update(cx, |project, cx| {
-                let shell = TerminalSettings::get_global(cx).shell.clone();
+                let worktree = project.find_worktree(dir.as_path(), cx);
+                let shell = TerminalSettings::get(
+                    worktree.as_ref().map(|(worktree, path)| SettingsLocation {
+                        worktree_id: worktree.read(cx).id(),
+                        path: &path,
+                    }),
+                    cx,
+                )
+                .shell
+                .clone();
                 project.directory_environment(&shell, dir.as_path().into(), cx)
             }),
             None => Task::ready(None).shared(),
@@ -133,7 +142,7 @@ impl Tool for TerminalTool {
                     .remote_client()
                     .and_then(|r| r.read(cx).default_system_shell())
             })
-            .unwrap_or_else(|| get_default_system_shell());
+            .unwrap_or_else(|| get_default_system_shell_preferring_bash());
 
         let env = cx.spawn(async move |_| {
             let mut env = env.await.unwrap_or_default();

crates/prompt_store/src/prompts.rs 🔗

@@ -14,7 +14,9 @@ use std::{
     time::Duration,
 };
 use text::LineEnding;
-use util::{ResultExt, get_default_system_shell, rel_path::RelPath};
+use util::{
+    ResultExt, get_default_system_shell_preferring_bash, rel_path::RelPath, shell::ShellKind,
+};
 
 use crate::UserPromptId;
 
@@ -43,7 +45,7 @@ impl ProjectContext {
             user_rules: default_user_rules,
             os: std::env::consts::OS.to_string(),
             arch: std::env::consts::ARCH.to_string(),
-            shell: get_default_system_shell(),
+            shell: ShellKind::new(&get_default_system_shell_preferring_bash()).to_string(),
         }
     }
 }

crates/util/Cargo.toml 🔗

@@ -44,6 +44,7 @@ tempfile.workspace = true
 unicase.workspace = true
 util_macros = { workspace = true, optional = true }
 walkdir.workspace = true
+which.workspace = true
 workspace-hack.workspace = true
 
 [target.'cfg(unix)'.dependencies]

crates/util/src/shell.rs 🔗

@@ -29,6 +29,30 @@ pub fn get_default_system_shell() -> String {
     }
 }
 
+/// Get the default system shell, preferring git-bash on Windows.
+pub fn get_default_system_shell_preferring_bash() -> String {
+    if cfg!(windows) {
+        get_windows_git_bash().unwrap_or_else(|| get_windows_system_shell())
+    } else {
+        "/bin/sh".to_string()
+    }
+}
+
+pub fn get_windows_git_bash() -> Option<String> {
+    static GIT_BASH: LazyLock<Option<String>> = LazyLock::new(|| {
+        // /path/to/git/cmd/git.exe/../../bin/bash.exe
+        let git = which::which("git").ok()?;
+        let git_bash = git.parent()?.parent()?.join("bin").join("bash.exe");
+        if git_bash.is_file() {
+            Some(git_bash.to_string_lossy().to_string())
+        } else {
+            None
+        }
+    });
+
+    (*GIT_BASH).clone()
+}
+
 pub fn get_windows_system_shell() -> String {
     use std::path::PathBuf;
 
@@ -152,20 +176,16 @@ impl ShellKind {
 
     pub fn new(program: impl AsRef<Path>) -> Self {
         let program = program.as_ref();
-        let Some(program) = program.file_name().and_then(|s| s.to_str()) else {
+        let Some(program) = program.file_stem().and_then(|s| s.to_str()) else {
             return if cfg!(windows) {
                 ShellKind::PowerShell
             } else {
                 ShellKind::Posix
             };
         };
-        if program == "powershell"
-            || program.ends_with("powershell.exe")
-            || program == "pwsh"
-            || program.ends_with("pwsh.exe")
-        {
+        if program == "powershell" || program == "pwsh" {
             ShellKind::PowerShell
-        } else if program == "cmd" || program.ends_with("cmd.exe") {
+        } else if program == "cmd" {
             ShellKind::Cmd
         } else if program == "nu" {
             ShellKind::Nushell
@@ -177,6 +197,8 @@ impl ShellKind {
             ShellKind::Tcsh
         } else if program == "rc" {
             ShellKind::Rc
+        } else if program == "sh" || program == "bash" {
+            ShellKind::Posix
         } else {
             if cfg!(windows) {
                 ShellKind::PowerShell

crates/util/src/util.rs 🔗

@@ -999,7 +999,9 @@ pub fn default<D: Default>() -> D {
     Default::default()
 }
 
-pub use self::shell::{get_default_system_shell, get_system_shell};
+pub use self::shell::{
+    get_default_system_shell, get_default_system_shell_preferring_bash, get_system_shell,
+};
 
 #[derive(Debug)]
 pub enum ConnectionResult<O> {