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 command: String,
15 /// Working directory for the command. This must be one of the root directories of the project.
16 working_directory: 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 run(
36 self: Arc<Self>,
37 input: serde_json::Value,
38 _messages: &[LanguageModelRequestMessage],
39 project: Entity<Project>,
40 cx: &mut App,
41 ) -> Task<Result<String>> {
42 let input: BashToolInput = match serde_json::from_value(input) {
43 Ok(input) => input,
44 Err(err) => return Task::ready(Err(anyhow!(err))),
45 };
46
47 let Some(worktree) = project
48 .read(cx)
49 .worktree_for_root_name(&input.working_directory, cx)
50 else {
51 return Task::ready(Err(anyhow!("Working directory not found in the project")));
52 };
53 let working_directory = worktree.read(cx).abs_path();
54
55 cx.spawn(|_| async move {
56 // Add 2>&1 to merge stderr into stdout for proper interleaving.
57 let command = format!("{} 2>&1", input.command);
58
59 let output = new_smol_command("bash")
60 .arg("-c")
61 .arg(&command)
62 .current_dir(working_directory)
63 .output()
64 .await
65 .context("Failed to execute bash command")?;
66
67 let output_string = String::from_utf8_lossy(&output.stdout).to_string();
68
69 if output.status.success() {
70 if output_string.is_empty() {
71 Ok("Command executed successfully.".to_string())
72 } else {
73 Ok(output_string)
74 }
75 } else {
76 Ok(format!(
77 "Command failed with exit code {}\n{}",
78 output.status.code().unwrap_or(-1),
79 &output_string
80 ))
81 }
82 })
83 }
84}