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