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:
parent
82b0881dcb
commit
9eacac62a9
11 changed files with 108 additions and 86 deletions
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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,75 +74,77 @@ 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)
|
||||||
{
|
{
|
||||||
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
|
Some(path) if !path.is_empty() => {
|
||||||
return Task::ready(Err(anyhow!(
|
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
|
||||||
"Could not find path {} in project",
|
return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
|
||||||
path.display()
|
};
|
||||||
)));
|
|
||||||
};
|
|
||||||
let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
let buffer =
|
||||||
let mut output = String::new();
|
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||||
let buffer = buffer.await?;
|
|
||||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
|
||||||
|
|
||||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
cx.spawn(async move |cx| {
|
||||||
let entry = &group.entries[group.primary_ix];
|
let mut output = String::new();
|
||||||
let range = entry.range.to_point(&snapshot);
|
let buffer = buffer.await?;
|
||||||
let severity = match entry.diagnostic.severity {
|
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||||
DiagnosticSeverity::ERROR => "error",
|
|
||||||
DiagnosticSeverity::WARNING => "warning",
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
writeln!(
|
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||||
output,
|
let entry = &group.entries[group.primary_ix];
|
||||||
"{} at line {}: {}",
|
let range = entry.range.to_point(&snapshot);
|
||||||
severity,
|
let severity = match entry.diagnostic.severity {
|
||||||
range.start.row + 1,
|
DiagnosticSeverity::ERROR => "error",
|
||||||
entry.diagnostic.message
|
DiagnosticSeverity::WARNING => "warning",
|
||||||
)?;
|
_ => continue,
|
||||||
}
|
};
|
||||||
|
|
||||||
if output.is_empty() {
|
writeln!(
|
||||||
Ok("File doesn't have errors or warnings!".to_string())
|
output,
|
||||||
} else {
|
"{} at line {}: {}",
|
||||||
Ok(output)
|
severity,
|
||||||
}
|
range.start.row + 1,
|
||||||
})
|
entry.diagnostic.message
|
||||||
} 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 output.is_empty() {
|
||||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
Ok("File doesn't have errors or warnings!".to_string())
|
||||||
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
|
} else {
|
||||||
else {
|
Ok(output)
|
||||||
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
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
_ => {
|
||||||
|
let project = project.read(cx);
|
||||||
|
let mut output = String::new();
|
||||||
|
let mut has_diagnostics = false;
|
||||||
|
|
||||||
if has_diagnostics {
|
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
|
||||||
Task::ready(Ok(output))
|
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||||
} else {
|
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
|
||||||
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
|
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 has_diagnostics {
|
||||||
|
Task::ready(Ok(output))
|
||||||
|
} else {
|
||||||
|
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`")
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue