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 let cmd = MarkdownString::escape(&input.command);
49 if input.command.contains('\n') {
50 format!("```bash\n{cmd}\n```")
51 } else {
52 format!("`{cmd}`")
53 }
54 }
55 Err(_) => "Run bash command".to_string(),
56 }
57 }
58
59 fn run(
60 self: Arc<Self>,
61 input: serde_json::Value,
62 _messages: &[LanguageModelRequestMessage],
63 project: Entity<Project>,
64 _action_log: Entity<ActionLog>,
65 cx: &mut App,
66 ) -> Task<Result<String>> {
67 let input: BashToolInput = match serde_json::from_value(input) {
68 Ok(input) => input,
69 Err(err) => return Task::ready(Err(anyhow!(err))),
70 };
71
72 let Some(worktree) = project.read(cx).worktree_for_root_name(&input.cd, cx) else {
73 return Task::ready(Err(anyhow!("Working directory not found in the project")));
74 };
75 let working_directory = worktree.read(cx).abs_path();
76
77 cx.spawn(async move |_| {
78 // Add 2>&1 to merge stderr into stdout for proper interleaving.
79 let command = format!("({}) 2>&1", input.command);
80
81 let output = new_smol_command("bash")
82 .arg("-c")
83 .arg(&command)
84 .current_dir(working_directory)
85 .output()
86 .await
87 .context("Failed to execute bash command")?;
88
89 let output_string = String::from_utf8_lossy(&output.stdout).to_string();
90
91 if output.status.success() {
92 if output_string.is_empty() {
93 Ok("Command executed successfully.".to_string())
94 } else {
95 Ok(output_string)
96 }
97 } else {
98 Ok(format!(
99 "Command failed with exit code {}\n{}",
100 output.status.code().unwrap_or(-1),
101 &output_string
102 ))
103 }
104 })
105 }
106}