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