Allow Bash tool to Just Work with more `cd` inputs (#27501)

Richard Feldman created

Release Notes:

- N/A

Change summary

crates/assistant_tools/src/bash_tool.rs | 43 ++++++++++++++++++++++++--
1 file changed, 39 insertions(+), 4 deletions(-)

Detailed changes

crates/assistant_tools/src/bash_tool.rs 🔗

@@ -5,6 +5,7 @@ use language_model::LanguageModelRequestMessage;
 use project::Project;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+use std::path::Path;
 use std::sync::Arc;
 use ui::IconName;
 use util::command::new_smol_command;
@@ -68,10 +69,44 @@ impl Tool for BashTool {
             Err(err) => return Task::ready(Err(anyhow!(err))),
         };
 
-        let Some(worktree) = project.read(cx).worktree_for_root_name(&input.cd, cx) else {
-            return Task::ready(Err(anyhow!("Working directory not found in the project")));
+        let project = project.read(cx);
+        let input_path = Path::new(&input.cd);
+        let working_dir = if input.cd == "." {
+            // Accept "." as meaning "the one worktree" if we only have one worktree.
+            let mut worktrees = project.worktrees(cx);
+
+            let only_worktree = match worktrees.next() {
+                Some(worktree) => worktree,
+                None => return Task::ready(Err(anyhow!("No worktrees found in the project"))),
+            };
+
+            if worktrees.next().is_some() {
+                return Task::ready(Err(anyhow!("'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.")));
+            }
+
+            only_worktree.read(cx).abs_path()
+        } else if input_path.is_absolute() {
+            // Absolute paths are allowed, but only if they're in one of the project's worktrees.
+            if !project
+                .worktrees(cx)
+                .any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
+            {
+                return Task::ready(Err(anyhow!(
+                    "The absolute path must be within one of the project's worktrees"
+                )));
+            }
+
+            input_path.into()
+        } else {
+            let Some(worktree) = project.worktree_for_root_name(&input.cd, cx) else {
+                return Task::ready(Err(anyhow!(
+                    "`cd` directory {} not found in the project",
+                    &input.cd
+                )));
+            };
+
+            worktree.read(cx).abs_path()
         };
-        let working_directory = worktree.read(cx).abs_path();
 
         cx.spawn(async move |_| {
             // Add 2>&1 to merge stderr into stdout for proper interleaving.
@@ -80,7 +115,7 @@ impl Tool for BashTool {
             let output = new_smol_command("bash")
                 .arg("-c")
                 .arg(&command)
-                .current_dir(working_directory)
+                .current_dir(working_dir)
                 .output()
                 .await
                 .context("Failed to execute bash command")?;