magi-code 0.96.5

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

pub(super) fn bash_card(input: &ToolCardInput<'_>) -> CardDraft {
    let detail = input.detail;
    let mut status = input_status(input);
    if status == CardStatus::Failed && tool_was_canceled(detail) {
        status = CardStatus::Canceled;
    }
    let timed_out = detail
        .and_then(|detail| detail.metadata.get("timed_out"))
        .and_then(serde_json::Value::as_bool)
        .unwrap_or(false);
    let exit_code = detail
        .and_then(|detail| detail.metadata.get("exit_code"))
        .and_then(serde_json::Value::as_i64);
    let command = input
        .bash_command
        .map(sanitize_display_text)
        .unwrap_or_default();
    let title = bash_title(status, timed_out, exit_code);
    let intent = detail
        .and_then(|detail| detail.params.get(crate::tools::contract::arg_key::INTENT))
        .and_then(serde_json::Value::as_str)
        .map(str::trim)
        .map(sanitized_single_line)
        .filter(|intent| !intent.is_empty());
    let mut body_rows = intent.iter().cloned().collect::<Vec<_>>();
    if !command.is_empty() {
        body_rows.push(command);
    }
    let mut display_lines = Vec::new();
    let status_row = match status {
        CardStatus::Running | CardStatus::None => None,
        CardStatus::Success => bash_output_line_count(detail)
            .map(|lines| format!("{lines} {}", if lines == 1 { "line" } else { "lines" })),
        CardStatus::Failed if timed_out => Some(
            detail
                .and_then(|detail| detail.params.get("timeout"))
                .and_then(serde_json::Value::as_u64)
                .map_or_else(
                    || "timed out".to_string(),
                    |timeout| format!("timed out after {timeout}s"),
                ),
        ),
        CardStatus::Failed => Some(
            bash_failure_reason(detail)
                .or_else(|| first_non_empty_line(input.preview))
                .unwrap_or_else(|| "command failed".to_string()),
        ),
        CardStatus::Canceled => Some("canceled".to_string()),
    };
    if let Some(status_row) = status_row {
        display_lines.push(TextLine::plain(&status_row));
        body_rows.push(status_row);
    }
    CardDraft {
        role: ToolCardRole::Bash,
        status,
        title,
        metadata: bash_jev_metadata(detail),
        body: body_rows.join("\n"),
        body_lines: display_lines,
        activity_rows: Vec::new(),
    }
}

fn bash_title(status: CardStatus, timed_out: bool, exit_code: Option<i64>) -> String {
    if timed_out {
        return "Bash • timed out".to_string();
    }
    if matches!(status, CardStatus::Success | CardStatus::Failed)
        && let Some(exit_code) = exit_code
    {
        format!("Bash • exit {exit_code}")
    } else if status == CardStatus::Canceled {
        "Bash • canceled".to_string()
    } else {
        "Bash".to_string()
    }
}

fn bash_output_line_count(detail: Option<&ToolActivityDetail>) -> Option<usize> {
    let metadata = &detail?.metadata;
    let streams = ["stdout", "stderr"].map(|key| metadata[key].as_str());
    if streams.iter().all(Option::is_none) {
        return None;
    }
    Some(
        streams
            .into_iter()
            .flatten()
            .map(|stream| stream.lines().count())
            .sum(),
    )
}

fn bash_failure_reason(detail: Option<&crate::output::ToolActivityDetail>) -> Option<String> {
    let detail = detail?;
    detail
        .metadata
        .get("cleanup_warning")
        .and_then(serde_json::Value::as_str)
        .and_then(first_non_empty_line)
        .or_else(|| {
            (detail
                .metadata
                .get("stdout_truncated")
                .and_then(serde_json::Value::as_bool)
                == Some(true)
                || detail
                    .metadata
                    .get("stderr_truncated")
                    .and_then(serde_json::Value::as_bool)
                    == Some(true))
            .then(|| "output truncated".to_string())
        })
        .or_else(|| {
            detail
                .metadata
                .get("stderr")
                .and_then(serde_json::Value::as_str)
                .and_then(first_non_empty_line)
        })
        .or_else(|| bash_error_from_output(detail.output.as_ref()))
}

fn bash_error_from_output(output: &str) -> Option<String> {
    output
        .lines()
        .map(str::trim)
        .find(|line| {
            !line.is_empty()
                && !matches!(*line, "stdout:" | "stderr:")
                && !line.starts_with("[output truncated:")
        })
        .map(|line| normalize_inline_separators(&redact_sensitive_text(line)))
}