use super::*;
pub(super) fn ast_grep_card(
state: &MissionControlState,
id: TranscriptCardId,
link: Option<TranscriptActivityLink>,
body: &str,
) -> TranscriptCard {
use crate::tools::contract::{arg_key, metadata_key};
let node = link
.as_ref()
.and_then(|link| state.nodes.get(&link.activity_id));
let detail = node.and_then(|node| node.tool_detail.as_ref());
let mut status = node
.map(|node| transcript_status_from_activity(node.status))
.or_else(|| detail.map(|detail| transcript_status_from_activity(detail.status)))
.unwrap_or_else(|| transcript_tool_status(state, id.entry_index));
let timed_out = grep_bool(detail, metadata_key::TIMED_OUT);
if status == TranscriptCardStatus::Failed
&& detail.is_some_and(|detail| detail.output.trim() == "prompt canceled")
{
status = TranscriptCardStatus::Canceled;
}
if timed_out && status != TranscriptCardStatus::Canceled {
status = TranscriptCardStatus::Failed;
}
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 matches = grep_usize(detail, "matches_returned");
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 {
TranscriptCardStatus::Success if truncated => matches.map_or_else(
|| "AST Grep".into(),
|n| format!("AST Grep • {n} {} returned", plural(n, "match", "matches")),
),
TranscriptCardStatus::Success => matches.map_or_else(
|| "AST Grep".into(),
|n| format!("AST Grep • {n} {}", plural(n, "match", "matches")),
),
TranscriptCardStatus::Failed if timed_out => "AST Grep • timed out".into(),
TranscriptCardStatus::Canceled => "AST Grep • canceled".into(),
_ => "AST Grep".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 {
TranscriptCardStatus::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());
}
}
TranscriptCardStatus::Failed if timed_out => lines.push("search timed out".into()),
TranscriptCardStatus::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()),
),
TranscriptCardStatus::Canceled => lines.push("Canceled before completion".into()),
_ => {}
}
TranscriptCard {
id,
role: TranscriptCardRole::AstGrep,
status,
title,
metadata: Vec::new(),
body: lines.join("\n"),
body_lines: Vec::new(),
children: Vec::new(),
activity_rows: Vec::new(),
subagent_card: None,
}
}
fn ast_grep_quoted(text: &str) -> String {
serde_json::to_string(&redact_sensitive_text(&transcript::sanitize_preview(text)))
.unwrap_or_else(|_| "\"ast-grep value\"".into())
}
fn ast_grep_bounded(text: &str) -> String {
truncate_preview_line(
&redact_sensitive_text(&transcript::sanitize_preview(text)),
PREVIEW_LINE_MAX_DISPLAY_WIDTH,
)
}
fn ast_grep_first_line(text: &str) -> Option<String> {
text.lines()
.map(str::trim)
.find(|line| !line.is_empty())
.map(ast_grep_bounded)
}