Add Bash tool (#26597)

Richard Feldman created

<img width="636" alt="Screenshot 2025-03-12 at 4 24 18 PM"
src="https://github.com/user-attachments/assets/6f317031-f495-4a5a-8260-79a56b10d628"
/>

<img width="634" alt="Screenshot 2025-03-12 at 4 24 36 PM"
src="https://github.com/user-attachments/assets/27283432-4f94-49f3-9d61-a0a9c737de40"
/>


Release Notes:

- N/A

Change summary

crates/assistant_tools/src/assistant_tools.rs       |  3 
crates/assistant_tools/src/bash_tool.rs             | 70 ++++++++++++++
crates/assistant_tools/src/bash_tool/description.md |  1 
3 files changed, 74 insertions(+)

Detailed changes

crates/assistant_tools/src/assistant_tools.rs 🔗

@@ -1,3 +1,4 @@
+mod bash_tool;
 mod delete_path_tool;
 mod edit_files_tool;
 mod list_directory_tool;
@@ -8,6 +9,7 @@ mod regex_search;
 use assistant_tool::ToolRegistry;
 use gpui::App;
 
+use crate::bash_tool::BashTool;
 use crate::delete_path_tool::DeletePathTool;
 use crate::edit_files_tool::EditFilesTool;
 use crate::list_directory_tool::ListDirectoryTool;
@@ -25,4 +27,5 @@ pub fn init(cx: &mut App) {
     registry.register_tool(EditFilesTool);
     registry.register_tool(RegexSearchTool);
     registry.register_tool(DeletePathTool);
+    registry.register_tool(BashTool);
 }

crates/assistant_tools/src/bash_tool.rs 🔗

@@ -0,0 +1,70 @@
+use anyhow::{anyhow, Result};
+use assistant_tool::Tool;
+use gpui::{App, Entity, Task};
+use language_model::LanguageModelRequestMessage;
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct BashToolInput {
+    /// The bash command to execute as a one-liner.
+    command: String,
+}
+
+pub struct BashTool;
+
+impl Tool for BashTool {
+    fn name(&self) -> String {
+        "bash".to_string()
+    }
+
+    fn description(&self) -> String {
+        include_str!("./bash_tool/description.md").to_string()
+    }
+
+    fn input_schema(&self) -> serde_json::Value {
+        let schema = schemars::schema_for!(BashToolInput);
+        serde_json::to_value(&schema).unwrap()
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: serde_json::Value,
+        _messages: &[LanguageModelRequestMessage],
+        _project: Entity<Project>,
+        cx: &mut App,
+    ) -> Task<Result<String>> {
+        let input: BashToolInput = match serde_json::from_value(input) {
+            Ok(input) => input,
+            Err(err) => return Task::ready(Err(anyhow!(err))),
+        };
+
+        cx.spawn(|_| async move {
+            // Add 2>&1 to merge stderr into stdout for proper interleaving
+            let command = format!("{} 2>&1", input.command);
+
+            // Spawn a blocking task to execute the command
+            let output = futures::executor::block_on(async {
+                std::process::Command::new("bash")
+                    .arg("-c")
+                    .arg(&command)
+                    .output()
+                    .map_err(|err| anyhow!("Failed to execute bash command: {}", err))
+            })?;
+
+            let output_string = String::from_utf8_lossy(&output.stdout).to_string();
+
+            if output.status.success() {
+                Ok(output_string)
+            } else {
+                Ok(format!(
+                    "Command failed with exit code {}\n{}",
+                    output.status.code().unwrap_or(-1),
+                    &output_string
+                ))
+            }
+        })
+    }
+}

crates/assistant_tools/src/bash_tool/description.md 🔗

@@ -0,0 +1 @@
+Executes a bash one-liner and returns the combined output. This tool spawns a bash process, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned. Use this tool when you need to run shell commands to get information about the system or process files.