magi-code 0.101.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

pub(super) fn ast_grep_card(input: &ToolCardInput<'_>) -> CardDraft {
    use crate::tool_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)
}