bash_tool.rs

  1use anyhow::{anyhow, Context as _, Result};
  2use assistant_tool::{ActionLog, Tool};
  3use gpui::{App, Entity, Task};
  4use language_model::LanguageModelRequestMessage;
  5use project::Project;
  6use schemars::JsonSchema;
  7use serde::{Deserialize, Serialize};
  8use std::sync::Arc;
  9use ui::IconName;
 10use util::command::new_smol_command;
 11use util::markdown::MarkdownString;
 12
 13#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 14pub struct BashToolInput {
 15    /// The bash command to execute as a one-liner.
 16    command: String,
 17    /// Working directory for the command. This must be one of the root directories of the project.
 18    cd: String,
 19}
 20
 21pub struct BashTool;
 22
 23impl Tool for BashTool {
 24    fn name(&self) -> String {
 25        "bash".to_string()
 26    }
 27
 28    fn needs_confirmation(&self) -> bool {
 29        true
 30    }
 31
 32    fn description(&self) -> String {
 33        include_str!("./bash_tool/description.md").to_string()
 34    }
 35
 36    fn icon(&self) -> IconName {
 37        IconName::Terminal
 38    }
 39
 40    fn input_schema(&self) -> serde_json::Value {
 41        let schema = schemars::schema_for!(BashToolInput);
 42        serde_json::to_value(&schema).unwrap()
 43    }
 44
 45    fn ui_text(&self, input: &serde_json::Value) -> String {
 46        match serde_json::from_value::<BashToolInput>(input.clone()) {
 47            Ok(input) => {
 48                if input.command.contains('\n') {
 49                    MarkdownString::code_block("bash", &input.command).0
 50                } else {
 51                    MarkdownString::inline_code(&input.command).0
 52                }
 53            }
 54            Err(_) => "Run bash command".to_string(),
 55        }
 56    }
 57
 58    fn run(
 59        self: Arc<Self>,
 60        input: serde_json::Value,
 61        _messages: &[LanguageModelRequestMessage],
 62        project: Entity<Project>,
 63        _action_log: Entity<ActionLog>,
 64        cx: &mut App,
 65    ) -> Task<Result<String>> {
 66        let input: BashToolInput = match serde_json::from_value(input) {
 67            Ok(input) => input,
 68            Err(err) => return Task::ready(Err(anyhow!(err))),
 69        };
 70
 71        let Some(worktree) = project.read(cx).worktree_for_root_name(&input.cd, cx) else {
 72            return Task::ready(Err(anyhow!("Working directory not found in the project")));
 73        };
 74        let working_directory = worktree.read(cx).abs_path();
 75
 76        cx.spawn(async move |_| {
 77            // Add 2>&1 to merge stderr into stdout for proper interleaving.
 78            let command = format!("({}) 2>&1", input.command);
 79
 80            let output = new_smol_command("bash")
 81                .arg("-c")
 82                .arg(&command)
 83                .current_dir(working_directory)
 84                .output()
 85                .await
 86                .context("Failed to execute bash command")?;
 87
 88            let output_string = String::from_utf8_lossy(&output.stdout).to_string();
 89
 90            if output.status.success() {
 91                if output_string.is_empty() {
 92                    Ok("Command executed successfully.".to_string())
 93                } else {
 94                    Ok(output_string)
 95                }
 96            } else {
 97                Ok(format!(
 98                    "Command failed with exit code {}\n{}",
 99                    output.status.code().unwrap_or(-1),
100                    &output_string
101                ))
102            }
103        })
104    }
105}