Antonio ScandurraWIPbdb17681 year agoHistory
235 lines · 8.3 KB · Rust
RawDownload
use crate::schema::json_schema_for;use anyhow::{Context as _, Result, anyhow};use assistant_tool::{ActionLog, Tool};use futures::io::BufReader;use futures::{AsyncBufReadExt, AsyncReadExt, FutureExt};use gpui::{App, Entity, Task};use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};use project::Project;use schemars::JsonSchema;use serde::{Deserialize, Serialize};use std::path::Path;use std::sync::Arc;use ui::IconName;use util::command::new_smol_command;use util::markdown::MarkdownString; #[derive(Debug, Serialize, Deserialize, JsonSchema)]pub struct BashToolInput {    /// The bash one-liner command to execute.    command: String,    /// Working directory for the command. This must be one of the root directories of the project.    cd: String,} pub struct BashTool; impl Tool for BashTool {    fn name(&self) -> String {        "bash".to_string()    }     fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {        true    }     fn description(&self) -> String {        include_str!("./bash_tool/description.md").to_string()    }     fn icon(&self) -> IconName {        IconName::Terminal    }     fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {        json_schema_for::<BashToolInput>(format)    }     fn ui_text(&self, input: &serde_json::Value) -> String {        match serde_json::from_value::<BashToolInput>(input.clone()) {            Ok(input) => {                let mut lines = input.command.lines();                let first_line = lines.next().unwrap_or_default();                let remaining_line_count = lines.count();                match remaining_line_count {                    0 => MarkdownString::inline_code(&first_line).0,                    1 => {                        MarkdownString::inline_code(&format!(                            "{} - {} more line",                            first_line, remaining_line_count                        ))                        .0                    }                    n => {                        MarkdownString::inline_code(&format!("{} - {} more lines", first_line, n)).0                    }                }            }            Err(_) => "Run bash command".to_string(),        }    }     fn run(        self: Arc<Self>,        input: serde_json::Value,        _messages: &[LanguageModelRequestMessage],        project: Entity<Project>,        _action_log: Entity<ActionLog>,        cx: &mut App,    ) -> Task<Result<String>> {        let input: BashToolInput = match serde_json::from_value(input) {            Ok(input) => input,            Err(err) => return Task::ready(Err(anyhow!(err))),        };         let project = project.read(cx);        let input_path = Path::new(&input.cd);        let working_dir = if input.cd == "." {            // Accept "." as meaning "the one worktree" if we only have one worktree.            let mut worktrees = project.worktrees(cx);             let only_worktree = match worktrees.next() {                Some(worktree) => worktree,                None => return Task::ready(Err(anyhow!("No worktrees found in the project"))),            };             if worktrees.next().is_some() {                return Task::ready(Err(anyhow!(                    "'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly."                )));            }             only_worktree.read(cx).abs_path()        } else if input_path.is_absolute() {            // Absolute paths are allowed, but only if they're in one of the project's worktrees.            if !project                .worktrees(cx)                .any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))            {                return Task::ready(Err(anyhow!(                    "The absolute path must be within one of the project's worktrees"                )));            }             input_path.into()        } else {            let Some(worktree) = project.worktree_for_root_name(&input.cd, cx) else {                return Task::ready(Err(anyhow!(                    "`cd` directory {} not found in the project",                    &input.cd                )));            };             worktree.read(cx).abs_path()        };         cx.spawn(async move |cx| {            // Add 2>&1 to merge stderr into stdout for proper interleaving.            let command = format!("({}) 2>&1", input.command);             let mut cmd = new_smol_command("bash")                .arg("-c")                .arg(&command)                .current_dir(working_dir)                .stdout(std::process::Stdio::piped())                .spawn()                .context("Failed to execute bash command")?;             // Capture stdout with a limit            let stdout = cmd.stdout.take().unwrap();            let mut reader = BufReader::new(stdout);             const MESSAGE_1: &str = "Command output too long. The first ";            const MESSAGE_2: &str = " bytes:\n\n";            const ERR_MESSAGE_1: &str = "Command failed with exit code ";            const ERR_MESSAGE_2: &str = "\n\n";             const STDOUT_LIMIT: usize = 8192;             const LIMIT: usize = STDOUT_LIMIT                - (MESSAGE_1.len()                    + (STDOUT_LIMIT.ilog10() as usize + 1) // byte count                    + MESSAGE_2.len()                    + ERR_MESSAGE_1.len()                    + 3 // status code                    + ERR_MESSAGE_2.len());             // Read one more byte to determine whether the output was truncated            let mut buffer = vec![0; LIMIT + 1];            let bytes_read = reader.read(&mut buffer).await?;             let mut timer = cx                .background_executor()                .timer(std::time::Duration::from_secs(10))                .fuse();             // Repeatedly fill the output reader's buffer without copying it.            loop {                let mut skipped_bytes = reader.fill_buf().fuse();                 futures::select! {                    skipped_bytes = skipped_bytes => {                        let skipped_bytes = skipped_bytes?;                        if skipped_bytes.is_empty() {                            break;                        }                        let skipped_bytes_len = skipped_bytes.len();                        reader.consume_unpin(skipped_bytes_len);                         timer = cx                            .background_executor()                            .timer(std::time::Duration::from_secs(10))                            .fuse();                    }                    _ = timer => {                        return Err(anyhow!("Command timed out. Output so far:\n{}", String::from_utf8_lossy(&buffer[..bytes_read])));                    }                }            }             let output_bytes = &buffer[..bytes_read];             // Let the process continue running            let status = cmd.status().await.context("Failed to get command status")?;             let output_string = if bytes_read > LIMIT {                // Valid to find `\n` in UTF-8 since 0-127 ASCII characters are not used in                // multi-byte characters.                let last_line_ix = output_bytes.iter().rposition(|b| *b == b'\n');                let output_string = String::from_utf8_lossy(                    &output_bytes[..last_line_ix.unwrap_or(output_bytes.len())],                );                 format!(                    "{}{}{}{}",                    MESSAGE_1,                    output_string.len(),                    MESSAGE_2,                    output_string                )            } else {                String::from_utf8_lossy(&output_bytes).into()            };             let output_with_status = if status.success() {                if output_string.is_empty() {                    "Command executed successfully.".to_string()                } else {                    output_string.to_string()                }            } else {                format!(                    "{}{}{}{}",                    ERR_MESSAGE_1,                    status.code().unwrap_or(-1),                    ERR_MESSAGE_2,                    output_string,                )            };             debug_assert!(output_with_status.len() <= STDOUT_LIMIT);             Ok(output_with_status)        })    }}