magi-code 0.96.1

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

pub(super) fn grep_card(
    state: &MissionControlState,
    id: TranscriptCardId,
    link: Option<TranscriptActivityLink>,
) -> TranscriptCard {
    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 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, "timed_out");
    let truncated = grep_bool(detail, "truncated");
    let matches = grep_usize(detail, "matches_returned");
    let title = match status {
        TranscriptCardStatus::Success if truncated => matches.map_or_else(
            || "Grep • results returned".to_string(),
            |count| {
                format!(
                    "Grep • {count} {} returned",
                    plural(count, "match", "matches")
                )
            },
        ),
        TranscriptCardStatus::Success => matches.map_or_else(
            || "Grep".to_string(),
            |count| format!("Grep • {count} {}", plural(count, "match", "matches")),
        ),
        TranscriptCardStatus::Failed if timed_out => "Grep • timed out".to_string(),
        TranscriptCardStatus::Canceled => "Grep • canceled".to_string(),
        _ => "Grep".to_string(),
    };
    let mut lines = Vec::new();
    if let Some(patterns) = grep_patterns(detail) {
        lines.push(grep_quoted_patterns(&patterns));
    }
    let scope_index = lines.len();
    let scope = grep_param(detail, "path").unwrap_or(".");
    lines.push(format!("in {}", sanitized_single_line(scope)));
    let preview_start = lines.len();
    let mut preview_end = preview_start;
    match status {
        TranscriptCardStatus::Running => {}
        TranscriptCardStatus::Success => {
            if let Some(preview) = detail.map(|detail| grep_preview(detail.output.as_ref()))
                && !preview.is_empty()
            {
                lines.extend(preview.lines().map(str::to_string));
                preview_end = lines.len();
            }
            if truncated {
                lines.extend(grep_truncation_messages(detail));
            }
        }
        TranscriptCardStatus::Failed if timed_out => lines.push("search timed out".to_string()),
        TranscriptCardStatus::Failed => lines.push(
            detail
                .and_then(|detail| first_non_empty_line(detail.output.as_ref()))
                .or_else(|| node.and_then(|node| first_non_empty_line(&node.preview)))
                .unwrap_or_else(|| "grep failed".to_string()),
        ),
        TranscriptCardStatus::Canceled => lines.push("canceled".to_string()),
        TranscriptCardStatus::None => {}
    }
    let body_lines = lines
        .iter()
        .enumerate()
        .map(|(index, line)| {
            if index == scope_index {
                split_role_line(line, 3, DisplayRole::Plain, DisplayRole::InlineCode)
            } else if index < scope_index {
                DisplayLine::from_span(line, DisplayRole::String)
            } else if (preview_start..preview_end).contains(&index) {
                grep_match_line(line)
            } else {
                DisplayLine::plain(line)
            }
        })
        .collect();
    TranscriptCard {
        id,
        role: TranscriptCardRole::Grep,
        status,
        title,
        metadata: Vec::new(),
        body: lines.join("\n"),
        body_lines,
        children: Vec::new(),
        activity_rows: Vec::new(),
        subagent_card: None,
    }
}

fn grep_patterns(detail: Option<&crate::output::ToolActivityDetail>) -> Option<Vec<&str>> {
    let detail = detail?;
    if let Some(patterns) = detail
        .params
        .get(crate::tools::contract::arg_key::PATTERNS)
        .and_then(serde_json::Value::as_array)
    {
        let patterns = patterns
            .iter()
            .filter_map(serde_json::Value::as_str)
            .filter(|pattern| !pattern.is_empty())
            .collect::<Vec<_>>();
        if !patterns.is_empty() {
            return Some(patterns);
        }
    }
    detail
        .params
        .get(crate::tools::contract::arg_key::PATTERN)
        .and_then(serde_json::Value::as_str)
        .filter(|pattern| !pattern.is_empty())
        .map(|pattern| vec![pattern])
}

fn grep_param<'a>(
    detail: Option<&'a crate::output::ToolActivityDetail>,
    key: &str,
) -> Option<&'a str> {
    detail?
        .params
        .get(key)
        .and_then(serde_json::Value::as_str)
        .filter(|value| !value.is_empty())
}

pub(super) fn grep_bool(detail: Option<&crate::output::ToolActivityDetail>, key: &str) -> bool {
    detail
        .and_then(|detail| detail.metadata.get(key))
        .and_then(serde_json::Value::as_bool)
        .unwrap_or(false)
}

pub(super) fn grep_usize(
    detail: Option<&crate::output::ToolActivityDetail>,
    key: &str,
) -> Option<usize> {
    detail?
        .metadata
        .get(key)?
        .as_u64()
        .and_then(|value| usize::try_from(value).ok())
}

fn grep_quoted_pattern(text: &str) -> String {
    serde_json::to_string(&transcript::sanitize_preview(text))
        .unwrap_or_else(|_| "\"grep pattern\"".to_string())
}

fn grep_quoted_patterns(patterns: &[&str]) -> String {
    if patterns.len() == 1 {
        return grep_quoted_pattern(patterns[0]);
    }
    let quoted = patterns
        .iter()
        .map(|pattern| grep_quoted_pattern(pattern))
        .collect::<Vec<_>>();
    format!("[{}]", quoted.join(", "))
}

pub(super) const GREP_PREVIEW_LINES: usize = 10;

fn grep_preview(output: &str) -> String {
    let sanitized = transcript::sanitize_preview(output.trim_end_matches('\n'));
    let mut lines = sanitized.lines();
    let mut preview = lines
        .by_ref()
        .take(GREP_PREVIEW_LINES)
        .map(|line| truncate_preview_line(line, PREVIEW_LINE_MAX_DISPLAY_WIDTH))
        .collect::<Vec<_>>();
    if lines.next().is_some() {
        preview.push(format!("… preview limited to {GREP_PREVIEW_LINES} lines"));
    }
    preview.join("\n")
}

fn grep_truncation_messages(detail: Option<&crate::output::ToolActivityDetail>) -> Vec<String> {
    let mut messages = Vec::new();
    if let Some(reasons) = detail
        .and_then(|detail| detail.metadata.get("truncation_reasons"))
        .and_then(serde_json::Value::as_array)
    {
        for reason in reasons.iter().filter_map(serde_json::Value::as_str) {
            let message = match reason {
                "matches" => "… more matches may exist",
                "file_bytes" => "… search incomplete: file size limit",
                "scan_bytes" => "… search incomplete: scan byte limit",
                "file_limit" => "… search incomplete: file limit",
                "read_errors" => "… search incomplete: unreadable files skipped",
                "walk_errors" => "… search incomplete: workspace walk errors or omissions",
                "per_file" => "… search limited to 5 matches per file",
                "deadline" => "… search timed out",
                "output_bytes" => "… output limited to 65,536 bytes",
                _ => continue,
            };
            if !messages.iter().any(|existing| existing == message) {
                messages.push(message.to_string());
            }
        }
    }
    if !messages.is_empty() {
        return messages;
    }

    if grep_bool(detail, "match_limit_reached") {
        messages.push("… more matches may exist".to_string());
    }
    if grep_bool(detail, "byte_limit_reached") {
        messages.push("… search incomplete: file size limit".to_string());
    }
    if grep_bool(detail, "scan_byte_limit_reached") {
        messages.push("… search incomplete: scan byte limit".to_string());
    }
    if grep_bool(detail, "file_limit_reached") {
        messages.push("… search incomplete: file limit".to_string());
    }
    if grep_usize(detail, "read_errors_skipped").is_some_and(|count| count > 0) {
        messages.push("… search incomplete: unreadable files skipped".to_string());
    }
    if grep_usize(detail, "walk_errors").is_some_and(|count| count > 0)
        || grep_usize(detail, "walk_entries_omitted").is_some_and(|count| count > 0)
    {
        messages.push("… search incomplete: workspace walk errors or omissions".to_string());
    }
    if grep_bool(detail, "per_file_limit_reached") {
        messages.push("… search limited to 5 matches per file".to_string());
    }
    if grep_bool(detail, "output_byte_limit_reached")
        || grep_usize(detail, "context_lines_omitted").is_some_and(|count| count > 0)
    {
        messages.push("… output limited to 65,536 bytes".to_string());
    }
    if !messages.is_empty() {
        return messages;
    }

    vec![match detail
        .and_then(|detail| detail.metadata.get("limit_reason"))
        .and_then(serde_json::Value::as_str)
    {
        Some("matches") => "… more matches may exist".to_string(),
        Some("file_bytes") => "… search incomplete: file size limit".to_string(),
        Some("scan_bytes") => "… search incomplete: scan byte limit".to_string(),
        Some("file_limit") => "… search incomplete: file limit".to_string(),
        Some("read_errors") => "… search incomplete: unreadable files skipped".to_string(),
        Some("walk_errors") => {
            "… search incomplete: workspace walk errors or omissions".to_string()
        }
        Some("per_file") => "… search limited to 5 matches per file".to_string(),
        Some("deadline") => "… search timed out".to_string(),
        Some("output_bytes") => "… output limited to 65,536 bytes".to_string(),
        _ => "… search incomplete".to_string(),
    }]
}

fn grep_match_line(line: &str) -> DisplayLine {
    // Only recognize a path:number: prefix; source text stays literal, including Markdown.
    for (path_end, _) in line.match_indices(':') {
        let after_path = &line[path_end + 1..];
        let Some((number, _)) = after_path.split_once(':') else {
            continue;
        };
        if path_end == 0 || number.is_empty() || !number.bytes().all(|byte| byte.is_ascii_digit()) {
            continue;
        }
        let source_start = path_end + number.len() + 2;
        return tool_display_line(vec![
            DisplaySpan::new(&line[..path_end], DisplayRole::InlineCode),
            DisplaySpan::new(":", DisplayRole::Punctuation),
            DisplaySpan::new(number, DisplayRole::Number),
            DisplaySpan::new(":", DisplayRole::Punctuation),
            DisplaySpan::new(&line[source_start..], DisplayRole::Plain),
        ]);
    }
    DisplayLine::plain(line)
}