Escape markdown in tools' ui_text (#27502)

Escape markdown in tools' `ui_text`

<img width="628" alt="Screenshot 2025-03-26 at 10 43 23 AM"
src="https://github.com/user-attachments/assets/bb694821-aae7-4ccf-a35a-a3317b0222d5"
/>


Release Notes:

- N/A
This commit is contained in:
Richard Feldman 2025-03-26 11:27:02 -04:00 committed by GitHub
parent 82b0881dcb
commit 9eacac62a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 108 additions and 86 deletions

View file

@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use ui::IconName; use ui::IconName;
use util::command::new_smol_command; use util::command::new_smol_command;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BashToolInput { pub struct BashToolInput {
@ -43,7 +44,14 @@ impl Tool for BashTool {
fn ui_text(&self, input: &serde_json::Value) -> String { fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<BashToolInput>(input.clone()) { match serde_json::from_value::<BashToolInput>(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(), Err(_) => "Run bash command".to_string(),
} }
} }

View file

@ -7,6 +7,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use ui::IconName; use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CopyPathToolInput { pub struct CopyPathToolInput {
@ -60,8 +61,8 @@ impl Tool for CopyPathTool {
fn ui_text(&self, input: &serde_json::Value) -> String { fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CopyPathToolInput>(input.clone()) { match serde_json::from_value::<CopyPathToolInput>(input.clone()) {
Ok(input) => { Ok(input) => {
let src = input.source_path.as_str(); let src = MarkdownString::escape(&input.source_path);
let dest = input.destination_path.as_str(); let dest = MarkdownString::escape(&input.destination_path);
format!("Copy `{src}` to `{dest}`") format!("Copy `{src}` to `{dest}`")
} }
Err(_) => "Copy path".to_string(), Err(_) => "Copy path".to_string(),

View file

@ -7,6 +7,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use ui::IconName; use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateFileToolInput { pub struct CreateFileToolInput {
@ -57,7 +58,7 @@ impl Tool for CreateFileTool {
fn ui_text(&self, input: &serde_json::Value) -> String { fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CreateFileToolInput>(input.clone()) { match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
Ok(input) => { Ok(input) => {
let path = input.path.as_str(); let path = MarkdownString::escape(&input.path);
format!("Create file `{path}`") format!("Create file `{path}`")
} }
Err(_) => "Create file".to_string(), Err(_) => "Create file".to_string(),

View file

@ -6,12 +6,9 @@ use language_model::LanguageModelRequestMessage;
use project::Project; use project::Project;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{fmt::Write, path::Path, sync::Arc};
fmt::Write,
path::{Path, PathBuf},
sync::Arc,
};
use ui::IconName; use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsToolInput { 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`. /// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
/// </example> /// </example>
pub path: Option<PathBuf>, pub path: Option<String>,
} }
pub struct DiagnosticsTool; pub struct DiagnosticsTool;
@ -58,9 +55,12 @@ impl Tool for DiagnosticsTool {
fn ui_text(&self, input: &serde_json::Value) -> String { fn ui_text(&self, input: &serde_json::Value) -> String {
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone()) if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
.ok() .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 { } else {
"Check project diagnostics".to_string() "Check project diagnostics".to_string()
} }
@ -74,17 +74,17 @@ impl Tool for DiagnosticsTool {
_action_log: Entity<ActionLog>, _action_log: Entity<ActionLog>,
cx: &mut App, cx: &mut App,
) -> Task<Result<String>> { ) -> Task<Result<String>> {
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input) match serde_json::from_value::<DiagnosticsToolInput>(input)
.ok() .ok()
.and_then(|input| input.path) .and_then(|input| input.path)
{ {
Some(path) if !path.is_empty() => {
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else { let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!( return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
"Could not find path {} in project",
path.display()
)));
}; };
let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
let buffer =
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
let mut output = String::new(); let mut output = String::new();
@ -115,7 +115,8 @@ impl Tool for DiagnosticsTool {
Ok(output) Ok(output)
} }
}) })
} else { }
_ => {
let project = project.read(cx); let project = project.read(cx);
let mut output = String::new(); let mut output = String::new();
let mut has_diagnostics = false; let mut has_diagnostics = false;
@ -146,4 +147,5 @@ impl Tool for DiagnosticsTool {
} }
} }
} }
}
} }

View file

@ -13,6 +13,7 @@ use project::Project;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ui::IconName; use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType { enum ContentType {
@ -133,7 +134,7 @@ impl Tool for FetchTool {
fn ui_text(&self, input: &serde_json::Value) -> String { fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<FetchToolInput>(input.clone()) { match serde_json::from_value::<FetchToolInput>(input.clone()) {
Ok(input) => format!("Fetch `{}`", input.url), Ok(input) => format!("Fetch {}", MarkdownString::escape(&input.url)),
Err(_) => "Fetch URL".to_string(), Err(_) => "Fetch URL".to_string(),
} }
} }

View file

@ -33,10 +33,10 @@ pub struct FindReplaceFileToolInput {
/// </example> /// </example>
pub path: PathBuf, 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.
/// ///
/// <example>Fix API endpoint URLs</example> /// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year</example> /// <example>Update copyright year in `page_footer`</example>
pub display_description: String, pub display_description: String,
/// The unique string to find in the file. This string cannot be empty; /// The unique string to find in the file. This string cannot be empty;

View file

@ -7,6 +7,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fmt::Write, path::Path, sync::Arc}; use std::{fmt::Write, path::Path, sync::Arc};
use ui::IconName; use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ListDirectoryToolInput { pub struct ListDirectoryToolInput {
@ -61,7 +62,10 @@ impl Tool for ListDirectoryTool {
fn ui_text(&self, input: &serde_json::Value) -> String { fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ListDirectoryToolInput>(input.clone()) { match serde_json::from_value::<ListDirectoryToolInput>(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(), Err(_) => "List directory".to_string(),
} }
} }

View file

@ -7,6 +7,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{path::Path, sync::Arc}; use std::{path::Path, sync::Arc};
use ui::IconName; use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct MovePathToolInput { pub struct MovePathToolInput {
@ -60,16 +61,17 @@ impl Tool for MovePathTool {
fn ui_text(&self, input: &serde_json::Value) -> String { fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<MovePathToolInput>(input.clone()) { match serde_json::from_value::<MovePathToolInput>(input.clone()) {
Ok(input) => { Ok(input) => {
let src = input.source_path.as_str(); let src = MarkdownString::escape(&input.source_path);
let dest = input.destination_path.as_str(); let dest = MarkdownString::escape(&input.destination_path);
let src_path = Path::new(src); let src_path = Path::new(&input.source_path);
let dest_path = Path::new(dest); let dest_path = Path::new(&input.destination_path);
match dest_path match dest_path
.file_name() .file_name()
.and_then(|os_str| os_str.to_os_string().into_string().ok()) .and_then(|os_str| os_str.to_os_string().into_string().ok())
{ {
Some(filename) if src_path.parent() == dest_path.parent() => { Some(filename) if src_path.parent() == dest_path.parent() => {
let filename = MarkdownString::escape(&filename);
format!("Rename `{src}` to `{filename}`") format!("Rename `{src}` to `{filename}`")
} }
_ => { _ => {

View file

@ -10,6 +10,7 @@ use project::Project;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ui::IconName; use ui::IconName;
use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ReadFileToolInput { pub struct ReadFileToolInput {
@ -64,7 +65,10 @@ impl Tool for ReadFileTool {
fn ui_text(&self, input: &serde_json::Value) -> String { fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<ReadFileToolInput>(input.clone()) { match serde_json::from_value::<ReadFileToolInput>(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(), Err(_) => "Read file".to_string(),
} }
} }

View file

@ -12,6 +12,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{cmp, fmt::Write, sync::Arc}; use std::{cmp, fmt::Write, sync::Arc};
use ui::IconName; use ui::IconName;
use util::markdown::MarkdownString;
use util::paths::PathMatcher; use util::paths::PathMatcher;
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
@ -63,14 +64,12 @@ impl Tool for RegexSearchTool {
match serde_json::from_value::<RegexSearchToolInput>(input.clone()) { match serde_json::from_value::<RegexSearchToolInput>(input.clone()) {
Ok(input) => { Ok(input) => {
let page = input.page(); let page = input.page();
let regex = MarkdownString::escape(&input.regex);
if page > 1 { if page > 1 {
format!( format!("Get page {page} of search results for regex “`{regex}`”")
"Get page {page} of search results for regex “`{}`”",
input.regex
)
} else { } else {
format!("Search files for regex “`{}`”", input.regex) format!("Search files for regex “`{regex}`”")
} }
} }
Err(_) => "Search with regex".to_string(), Err(_) => "Search with regex".to_string(),

View file

@ -19,7 +19,7 @@ impl MarkdownString {
/// * `$` for inline math /// * `$` for inline math
/// * `~` for strikethrough /// * `~` 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: /// the other characters involved are escaped:
/// ///
/// * `!`, `]`, `(`, and `)` are used in link syntax, but `[` is escaped so these are parsed as /// * `!`, `]`, `(`, and `)` are used in link syntax, but `[` is escaped so these are parsed as