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}