use super::*;
pub(super) fn ast_grep_card(input: &ToolCardInput<'_>) -> CardDraft {
use crate::tools::contract::{arg_key, metadata_key};
let detail = input.detail;
let body = input.body;
let mut status = input_status(input);
let timed_out = grep_bool(detail, metadata_key::TIMED_OUT);
if status == CardStatus::Failed
&& detail.is_some_and(|detail| detail.output.trim() == "prompt canceled")
{
status = CardStatus::Canceled;
}
let param = |key: &str| {
detail
.and_then(|detail| detail.params.get(key))
.and_then(serde_json::Value::as_str)
.filter(|value| !value.trim().is_empty())
};
let pattern = param(arg_key::PATTERN);
let path = param(arg_key::PATH)
.or_else(|| {
detail
.and_then(|detail| detail.metadata.get(metadata_key::PATH))
.and_then(serde_json::Value::as_str)
.filter(|value| !value.trim().is_empty())
})
.unwrap_or(".");
let language = param(arg_key::LANGUAGE).or_else(|| {
detail
.and_then(|detail| detail.metadata.get(metadata_key::LANGUAGE))
.and_then(serde_json::Value::as_str)
});
let rewrite = param(arg_key::REWRITE);
let limit = detail
.and_then(|detail| detail.params.get(arg_key::LIMIT))
.and_then(serde_json::Value::as_u64)
.and_then(|n| usize::try_from(n).ok());
let outline = param("operation") == Some("outline");
let matches = grep_usize(
detail,
if outline {
"symbols_returned"
} else {
"matches_returned"
},
);
let name = if outline {
"AST Outline"
} else if rewrite.is_some() {
"AST Rewrite Preview"
} else {
"AST Grep"
};
let unit = if outline {
("entry", "entries")
} else {
("output line", "output lines")
};
let truncated = grep_bool(detail, metadata_key::TRUNCATED);
let output_truncated = grep_bool(detail, metadata_key::STDOUT_TRUNCATED)
|| grep_bool(detail, metadata_key::STDERR_TRUNCATED);
let title = match status {
CardStatus::Success if truncated => matches.map_or_else(
|| name.into(),
|n| format!("{name} • {n} {} returned", plural(n, unit.0, unit.1)),
),
CardStatus::Success => matches.map_or_else(
|| name.into(),
|n| format!("{name} • {n} {}", plural(n, unit.0, unit.1)),
),
CardStatus::Failed if timed_out => format!("{name} • timed out"),
CardStatus::Canceled => format!("{name} • canceled"),
_ => name.into(),
};
let mut lines = Vec::new();
if let Some(pattern) = pattern {
lines.push(ast_grep_bounded(&ast_grep_quoted(pattern)));
}
lines.push(ast_grep_bounded(&format!(
"in {}",
sanitized_single_line(path)
)));
if let Some(language) = language {
lines.push(ast_grep_bounded(&format!(
"language {}",
sanitized_single_line(language)
)));
}
if let Some(rewrite) = rewrite {
lines.push(ast_grep_bounded(&format!(
"rewrite {}",
ast_grep_quoted(rewrite)
)));
}
if let Some(limit) = limit {
lines.push(format!("limit {limit}"));
}
match status {
CardStatus::Success => {
let output = detail.map(|detail| detail.output.as_ref()).unwrap_or("");
let rows = output
.lines()
.filter(|line| !line.trim().is_empty())
.map(ast_grep_bounded)
.collect::<Vec<_>>();
lines.extend(rows.iter().take(TOOL_PREVIEW_LINES).cloned());
if rows.len() > TOOL_PREVIEW_LINES {
lines.push(format!("… preview limited to {TOOL_PREVIEW_LINES} lines"));
}
if output_truncated {
lines.push("… output truncated".into());
} else if truncated {
lines.push("… search incomplete".into());
}
}
CardStatus::Failed if timed_out => lines.push("search timed out".into()),
CardStatus::Failed => lines.push(
detail
.and_then(|d| d.metadata.get(metadata_key::CLEANUP_WARNING))
.and_then(serde_json::Value::as_str)
.and_then(ast_grep_first_line)
.or_else(|| detail.and_then(|d| ast_grep_first_line(d.output.as_ref())))
.or_else(|| ast_grep_first_line(body))
.unwrap_or_else(|| "ast-grep failed".into()),
),
CardStatus::Canceled => lines.push("Canceled before completion".into()),
_ => {}
}
CardDraft {
role: ToolCardRole::AstGrep,
status,
title,
metadata: Vec::new(),
body: lines.join("\n"),
body_lines: Vec::new(),
activity_rows: Vec::new(),
}
}
fn ast_grep_quoted(text: &str) -> String {
serde_json::to_string(&redact_sensitive_text(&sanitize_display_text(text)))
.unwrap_or_else(|_| "\"ast-grep value\"".into())
}
fn ast_grep_bounded(text: &str) -> String {
truncate_preview_line(
&redact_sensitive_text(&sanitize_display_text(text)),
PREVIEW_LINE_MAX_CHARS,
)
}
fn ast_grep_first_line(text: &str) -> Option<String> {
text.lines()
.map(str::trim)
.find(|line| !line.is_empty())
.map(ast_grep_bounded)
}