bash_tool.rs

 1use anyhow::{anyhow, Context as _, Result};
 2use assistant_tool::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 util::command::new_smol_command;
10
11#[derive(Debug, Serialize, Deserialize, JsonSchema)]
12pub struct BashToolInput {
13    /// The bash command to execute as a one-liner.
14    ///
15    /// WARNING: you must not `cd` into the working directory, as that's already
16    /// taken care of automatically. Doing so will cause the command to fail!
17    command: String,
18    /// Working directory for the command. This must be one of the root directories of the project.
19    working_directory: String,
20}
21
22pub struct BashTool;
23
24impl Tool for BashTool {
25    fn name(&self) -> String {
26        "bash".to_string()
27    }
28
29    fn description(&self) -> String {
30        include_str!("./bash_tool/description.md").to_string()
31    }
32
33    fn input_schema(&self) -> serde_json::Value {
34        let schema = schemars::schema_for!(BashToolInput);
35        serde_json::to_value(&schema).unwrap()
36    }
37
38    fn run(
39        self: Arc<Self>,
40        input: serde_json::Value,
41        _messages: &[LanguageModelRequestMessage],
42        project: Entity<Project>,
43        cx: &mut App,
44    ) -> Task<Result<String>> {
45        let input: BashToolInput = match serde_json::from_value(input) {
46            Ok(input) => input,
47            Err(err) => return Task::ready(Err(anyhow!(err))),
48        };
49
50        let Some(worktree) = project
51            .read(cx)
52            .worktree_for_root_name(&input.working_directory, cx)
53        else {
54            return Task::ready(Err(anyhow!("Working directory not found in the project")));
55        };
56        let working_directory = worktree.read(cx).abs_path();
57
58        cx.spawn(|_| async move {
59            // Add 2>&1 to merge stderr into stdout for proper interleaving.
60            let command = format!("{} 2>&1", input.command);
61
62            let output = new_smol_command("bash")
63                .arg("-c")
64                .arg(&command)
65                .current_dir(working_directory)
66                .output()
67                .await
68                .context("Failed to execute bash command")?;
69
70            let output_string = String::from_utf8_lossy(&output.stdout).to_string();
71
72            if output.status.success() {
73                if output_string.is_empty() {
74                    Ok("Command executed successfully.".to_string())
75                } else {
76                    Ok(output_string)
77                }
78            } else {
79                Ok(format!(
80                    "Command failed with exit code {}\n{}",
81                    output.status.code().unwrap_or(-1),
82                    &output_string
83                ))
84            }
85        })
86    }
87}