magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
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)
}