diff --git a/crates/assistant_tools/src/bash_tool.rs b/crates/assistant_tools/src/bash_tool.rs index 19cf5c198cb39ab0158b0605e1c94df355c60a6f..5045eb6ee36fe4f85079c47557556f8a301f2452 100644 --- a/crates/assistant_tools/src/bash_tool.rs +++ b/crates/assistant_tools/src/bash_tool.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; 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 { @@ -43,7 +44,14 @@ impl Tool for BashTool { fn ui_text(&self, input: &serde_json::Value) -> String { match serde_json::from_value::(input.clone()) { - Ok(input) => format!("`{}`", input.command), + Ok(input) => { + let cmd = MarkdownString::escape(&input.command); + if input.command.contains('\n') { + format!("```bash\n{cmd}\n```") + } else { + format!("`{cmd}`") + } + } Err(_) => "Run bash command".to_string(), } } diff --git a/crates/assistant_tools/src/copy_path_tool.rs b/crates/assistant_tools/src/copy_path_tool.rs index d4cb85421b641f270c8bcb6a6036f224c458f849..337cd177c14e3e82be2794cb4059372f339b8e1d 100644 --- a/crates/assistant_tools/src/copy_path_tool.rs +++ b/crates/assistant_tools/src/copy_path_tool.rs @@ -7,6 +7,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; use ui::IconName; +use util::markdown::MarkdownString; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct CopyPathToolInput { @@ -60,8 +61,8 @@ impl Tool for CopyPathTool { fn ui_text(&self, input: &serde_json::Value) -> String { match serde_json::from_value::(input.clone()) { Ok(input) => { - let src = input.source_path.as_str(); - let dest = input.destination_path.as_str(); + let src = MarkdownString::escape(&input.source_path); + let dest = MarkdownString::escape(&input.destination_path); format!("Copy `{src}` to `{dest}`") } Err(_) => "Copy path".to_string(), diff --git a/crates/assistant_tools/src/create_file_tool.rs b/crates/assistant_tools/src/create_file_tool.rs index ccbb74803bbd39b6cc68bb892c1b3532078a124c..5f0c67745fb8d794bdec1d24ddc0e0b9c968f747 100644 --- a/crates/assistant_tools/src/create_file_tool.rs +++ b/crates/assistant_tools/src/create_file_tool.rs @@ -7,6 +7,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; use ui::IconName; +use util::markdown::MarkdownString; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct CreateFileToolInput { @@ -57,7 +58,7 @@ impl Tool for CreateFileTool { fn ui_text(&self, input: &serde_json::Value) -> String { match serde_json::from_value::(input.clone()) { Ok(input) => { - let path = input.path.as_str(); + let path = MarkdownString::escape(&input.path); format!("Create file `{path}`") } Err(_) => "Create file".to_string(), diff --git a/crates/assistant_tools/src/diagnostics_tool.rs b/crates/assistant_tools/src/diagnostics_tool.rs index a3638a431fb996c04598f17190e20a34b441fc01..6b51ec15869ec5bfb140839b1961f3390a59bf19 100644 --- a/crates/assistant_tools/src/diagnostics_tool.rs +++ b/crates/assistant_tools/src/diagnostics_tool.rs @@ -6,12 +6,9 @@ use language_model::LanguageModelRequestMessage; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::{ - fmt::Write, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{fmt::Write, path::Path, sync::Arc}; use ui::IconName; +use util::markdown::MarkdownString; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct DiagnosticsToolInput { @@ -28,7 +25,7 @@ pub struct DiagnosticsToolInput { /// /// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`. /// - pub path: Option, + pub path: Option, } pub struct DiagnosticsTool; @@ -58,9 +55,12 @@ impl Tool for DiagnosticsTool { fn ui_text(&self, input: &serde_json::Value) -> String { if let Some(path) = serde_json::from_value::(input.clone()) .ok() - .and_then(|input| input.path) + .and_then(|input| match input.path { + Some(path) if !path.is_empty() => Some(MarkdownString::escape(&path)), + _ => None, + }) { - format!("Check diagnostics for “`{}`”", path.display()) + format!("Check diagnostics for `{path}`") } else { "Check project diagnostics".to_string() } @@ -74,76 +74,78 @@ impl Tool for DiagnosticsTool { _action_log: Entity, cx: &mut App, ) -> Task> { - if let Some(path) = serde_json::from_value::(input) + match serde_json::from_value::(input) .ok() .and_then(|input| input.path) { - let Some(project_path) = project.read(cx).find_project_path(&path, cx) else { - return Task::ready(Err(anyhow!( - "Could not find path {} in project", - path.display() - ))); - }; - let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx)); - - cx.spawn(async move |cx| { + Some(path) if !path.is_empty() => { + let Some(project_path) = project.read(cx).find_project_path(&path, cx) else { + return Task::ready(Err(anyhow!("Could not find path {path} in project",))); + }; + + let buffer = + project.update(cx, |project, cx| project.open_buffer(project_path, cx)); + + cx.spawn(async move |cx| { + let mut output = String::new(); + let buffer = buffer.await?; + let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + + for (_, group) in snapshot.diagnostic_groups(None) { + let entry = &group.entries[group.primary_ix]; + let range = entry.range.to_point(&snapshot); + let severity = match entry.diagnostic.severity { + DiagnosticSeverity::ERROR => "error", + DiagnosticSeverity::WARNING => "warning", + _ => continue, + }; + + writeln!( + output, + "{} at line {}: {}", + severity, + range.start.row + 1, + entry.diagnostic.message + )?; + } + + if output.is_empty() { + Ok("File doesn't have errors or warnings!".to_string()) + } else { + Ok(output) + } + }) + } + _ => { + let project = project.read(cx); let mut output = String::new(); - let buffer = buffer.await?; - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - - for (_, group) in snapshot.diagnostic_groups(None) { - let entry = &group.entries[group.primary_ix]; - let range = entry.range.to_point(&snapshot); - let severity = match entry.diagnostic.severity { - DiagnosticSeverity::ERROR => "error", - DiagnosticSeverity::WARNING => "warning", - _ => continue, - }; - - writeln!( - output, - "{} at line {}: {}", - severity, - range.start.row + 1, - entry.diagnostic.message - )?; + let mut has_diagnostics = false; + + for (project_path, _, summary) in project.diagnostic_summaries(true, cx) { + if summary.error_count > 0 || summary.warning_count > 0 { + let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx) + else { + continue; + }; + + has_diagnostics = true; + output.push_str(&format!( + "{}: {} error(s), {} warning(s)\n", + Path::new(worktree.read(cx).root_name()) + .join(project_path.path) + .display(), + summary.error_count, + summary.warning_count + )); + } } - if output.is_empty() { - Ok("File doesn't have errors or warnings!".to_string()) + if has_diagnostics { + Task::ready(Ok(output)) } else { - Ok(output) - } - }) - } else { - let project = project.read(cx); - let mut output = String::new(); - let mut has_diagnostics = false; - - for (project_path, _, summary) in project.diagnostic_summaries(true, cx) { - if summary.error_count > 0 || summary.warning_count > 0 { - let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx) - else { - continue; - }; - - has_diagnostics = true; - output.push_str(&format!( - "{}: {} error(s), {} warning(s)\n", - Path::new(worktree.read(cx).root_name()) - .join(project_path.path) - .display(), - summary.error_count, - summary.warning_count - )); + Task::ready(Ok("No errors or warnings found in the project.".to_string())) } } - - if has_diagnostics { - Task::ready(Ok(output)) - } else { - Task::ready(Ok("No errors or warnings found in the project.".to_string())) - } } } } diff --git a/crates/assistant_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs index a9895f0fbef9f1efd37f1683689c3fdfb506db3d..aca6b0b2c7607529430d4023d983ee8fa7b58db3 100644 --- a/crates/assistant_tools/src/fetch_tool.rs +++ b/crates/assistant_tools/src/fetch_tool.rs @@ -13,6 +13,7 @@ use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ui::IconName; +use util::markdown::MarkdownString; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] enum ContentType { @@ -133,7 +134,7 @@ impl Tool for FetchTool { fn ui_text(&self, input: &serde_json::Value) -> String { match serde_json::from_value::(input.clone()) { - Ok(input) => format!("Fetch `{}`", input.url), + Ok(input) => format!("Fetch {}", MarkdownString::escape(&input.url)), Err(_) => "Fetch URL".to_string(), } } diff --git a/crates/assistant_tools/src/find_replace_file_tool.rs b/crates/assistant_tools/src/find_replace_file_tool.rs index d13fbff4db88f5f10922daf7c493d2a69946e7a1..73a516f0b3ba05c992a3495a6918582b305712a2 100644 --- a/crates/assistant_tools/src/find_replace_file_tool.rs +++ b/crates/assistant_tools/src/find_replace_file_tool.rs @@ -33,10 +33,10 @@ pub struct FindReplaceFileToolInput { /// pub path: PathBuf, - /// A user-friendly description of what's being replaced. This will be shown in the UI. + /// A user-friendly markdown description of what's being replaced. This will be shown in the UI. /// /// Fix API endpoint URLs - /// Update copyright year + /// Update copyright year in `page_footer` pub display_description: String, /// The unique string to find in the file. This string cannot be empty; diff --git a/crates/assistant_tools/src/list_directory_tool.rs b/crates/assistant_tools/src/list_directory_tool.rs index 9ceba3b129c9e45cc1674a205aed84b89536a2ff..c7ed123e66d3266436c7b8c9980c3625a2705d4c 100644 --- a/crates/assistant_tools/src/list_directory_tool.rs +++ b/crates/assistant_tools/src/list_directory_tool.rs @@ -7,6 +7,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{fmt::Write, path::Path, sync::Arc}; use ui::IconName; +use util::markdown::MarkdownString; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ListDirectoryToolInput { @@ -61,7 +62,10 @@ impl Tool for ListDirectoryTool { fn ui_text(&self, input: &serde_json::Value) -> String { match serde_json::from_value::(input.clone()) { - Ok(input) => format!("List the `{}` directory's contents", input.path), + Ok(input) => { + let path = MarkdownString::escape(&input.path); + format!("List the `{path}` directory's contents") + } Err(_) => "List directory".to_string(), } } diff --git a/crates/assistant_tools/src/move_path_tool.rs b/crates/assistant_tools/src/move_path_tool.rs index f54bfecf783ad8719dde1f2bb27dbfeb550fd034..9da7baded546187f09f475499705c94ffab0229b 100644 --- a/crates/assistant_tools/src/move_path_tool.rs +++ b/crates/assistant_tools/src/move_path_tool.rs @@ -7,6 +7,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{path::Path, sync::Arc}; use ui::IconName; +use util::markdown::MarkdownString; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct MovePathToolInput { @@ -60,16 +61,17 @@ impl Tool for MovePathTool { fn ui_text(&self, input: &serde_json::Value) -> String { match serde_json::from_value::(input.clone()) { Ok(input) => { - let src = input.source_path.as_str(); - let dest = input.destination_path.as_str(); - let src_path = Path::new(src); - let dest_path = Path::new(dest); + let src = MarkdownString::escape(&input.source_path); + let dest = MarkdownString::escape(&input.destination_path); + let src_path = Path::new(&input.source_path); + let dest_path = Path::new(&input.destination_path); match dest_path .file_name() .and_then(|os_str| os_str.to_os_string().into_string().ok()) { Some(filename) if src_path.parent() == dest_path.parent() => { + let filename = MarkdownString::escape(&filename); format!("Rename `{src}` to `{filename}`") } _ => { diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index 4022f326cdc6ffaa9f451aedddbf448d9897e106..e195bc11063cc951a2f3f195e9d84d62f2440f02 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -10,6 +10,7 @@ use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ui::IconName; +use util::markdown::MarkdownString; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ReadFileToolInput { @@ -64,7 +65,10 @@ impl Tool for ReadFileTool { fn ui_text(&self, input: &serde_json::Value) -> String { match serde_json::from_value::(input.clone()) { - Ok(input) => format!("Read file `{}`", input.path.display()), + Ok(input) => { + let path = MarkdownString::escape(&input.path.display().to_string()); + format!("Read file `{path}`") + } Err(_) => "Read file".to_string(), } } diff --git a/crates/assistant_tools/src/regex_search_tool.rs b/crates/assistant_tools/src/regex_search_tool.rs index 3bd6d26fa3e5582ac18d2e303a38c41b91c6e1bd..db2fd1967f25c607b6addda520fbf89401a85568 100644 --- a/crates/assistant_tools/src/regex_search_tool.rs +++ b/crates/assistant_tools/src/regex_search_tool.rs @@ -12,6 +12,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{cmp, fmt::Write, sync::Arc}; use ui::IconName; +use util::markdown::MarkdownString; use util::paths::PathMatcher; #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -63,14 +64,12 @@ impl Tool for RegexSearchTool { match serde_json::from_value::(input.clone()) { Ok(input) => { let page = input.page(); + let regex = MarkdownString::escape(&input.regex); if page > 1 { - format!( - "Get page {page} of search results for regex “`{}`”", - input.regex - ) + format!("Get page {page} of search results for regex “`{regex}`”") } else { - format!("Search files for regex “`{}`”", input.regex) + format!("Search files for regex “`{regex}`”") } } Err(_) => "Search with regex".to_string(), diff --git a/crates/util/src/markdown.rs b/crates/util/src/markdown.rs index 333887a9b12d82900c0a8f5ca5947d26fc45c3ca..aa3c51d41e0462540e1d16fc1f24bdb0b7aa1357 100644 --- a/crates/util/src/markdown.rs +++ b/crates/util/src/markdown.rs @@ -19,7 +19,7 @@ impl MarkdownString { /// * `$` for inline math /// * `~` for strikethrough /// - /// Escape of some character is unnecessary because while they are involved in markdown syntax, + /// Escape of some characters is unnecessary, because while they are involved in markdown syntax, /// the other characters involved are escaped: /// /// * `!`, `]`, `(`, and `)` are used in link syntax, but `[` is escaped so these are parsed as