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