magi-code 0.102.0

Repository-aware CLI coding agent for terminal work
Documentation
//! Fixed public Bash notices, shared by terminal and wire display. No source rows escape.
use super::{ToolActivityDetail, protection::public_decision};

pub(crate) struct BashNotice {
    pub(crate) label: &'static str,
    pub(crate) value: Option<String>,
}

pub(crate) struct BashNotices {
    pub(crate) rows: Vec<BashNotice>,
    pub(crate) limited_entries: usize,
    pub(crate) unsupported_entries: usize,
    pub(crate) unsupported_notices: usize,
}

pub(crate) fn bash_notices(detail: Option<&ToolActivityDetail>) -> BashNotices {
    let mut rows = [
        ("Humanize protection", "humanize_protection"),
        ("Bash protection", "bash_protection"),
    ]
    .into_iter()
    .map(|(label, key)| BashNotice {
        label,
        value: public_decision(detail, key).map(|(_, label)| label.to_owned()),
    })
    .collect::<Vec<_>>();
    let status = detail.and_then(|detail| detail.metadata["bash_file_tracking"]["status"].as_str());
    let tracking = match status {
        Some("completed") => Some("completed"),
        Some("command_limit") => Some("skipped (command limit)"),
        Some("canceled") => Some("skipped (canceled)"),
        Some("busy") => Some("skipped (worker busy)"),
        Some("unavailable") => Some("skipped (worker unavailable)"),
        Some("timed_out") => Some("timed out"),
        Some("error") => Some("assessment failed"),
        Some("redacted") => Some("skipped (sensitive command)"),
        Some("unsupported") => Some("skipped (unsupported command)"),
        Some("no_candidates") => Some("skipped (no literal paths)"),
        Some("command_failed") => Some("skipped (command did not succeed)"),
        _ => None,
    };
    rows.push(BashNotice {
        label: "File tracking",
        value: tracking.map(str::to_owned),
    });
    // Unknown status suppresses activity, matching the original public selector.
    let unsupported_notices = usize::from(status.is_some() && tracking.is_none());
    let activity = detail
        .filter(|_| unsupported_notices == 0)
        .and_then(|detail| detail.metadata["inferred_file_activity"].as_array());
    let mut files = std::collections::BTreeMap::<&str, u8>::new();
    let mut unsupported_entries = 0;
    for item in activity.into_iter().flatten().take(32) {
        let path = item["path"].as_str().filter(|path| !path.is_empty());
        let bits = match item["operation"].as_str() {
            Some("read") => 1,
            Some("write") => 2,
            Some("edit") => 4,
            Some("read_write") => 3,
            _ => 0,
        };
        if let Some(path) = path.filter(|_| bits != 0) {
            *files.entry(path).or_default() |= bits;
        } else {
            unsupported_entries += 1;
        }
    }
    let limited_entries = activity.map_or(0, |entries| entries.len().saturating_sub(32));
    rows.push(BashNotice {
        label: "Inferred file activity",
        value: activity.map(|entries| {
            let count = |bit| files.values().filter(|bits| **bits & bit != 0).count();
            format!(
                "{} files inferred in supported selection • R {} / W {} / E {} • selected first {} of {} entries; {} unsupported • {} • not verified effects",
                files.len(), count(1), count(2), count(4), entries.len().min(32), entries.len(), unsupported_entries,
                if limited_entries > 0 || unsupported_entries > 0 { "full totals unknown" } else { "complete selection" },
            )
        }),
    });
    BashNotices {
        rows,
        limited_entries,
        unsupported_entries,
        unsupported_notices,
    }
}

/// Independent execution facts. Counts describe only normalized, retained streams.
pub(super) fn bash_execution_notices(detail: Option<&ToolActivityDetail>) -> Vec<BashNotice> {
    let Some(detail) = detail.filter(|detail| {
        matches!(
            detail.status,
            super::ActivityStatus::Success
                | super::ActivityStatus::Failed
                | super::ActivityStatus::Canceled
        )
    }) else {
        return Vec::new();
    };
    let metadata = &detail.metadata;
    let timeout = metadata["timed_out"].as_bool().map(|timed_out| {
        if timed_out {
            detail.params["timeout"].as_u64().map_or_else(
                || "timed out; timeout duration unavailable".to_owned(),
                |seconds| format!("timed out after {seconds}s"),
            )
        } else {
            "not timed out".to_owned()
        }
    });
    let truncation = ["stdout_truncated", "stderr_truncated"].map(|key| metadata[key].as_bool());
    let truncation = truncation.iter().any(Option::is_some).then(|| {
        ["stdout", "stderr"]
            .into_iter()
            .zip(truncation)
            .map(|(stream, truncated)| {
                format!(
                    "{stream} {}",
                    match truncated {
                        Some(true) => "truncated",
                        Some(false) => "not truncated",
                        None => "truncation unavailable",
                    }
                )
            })
            .collect::<Vec<_>>()
            .join("; ")
    });
    let cleanup = metadata["cleanup_warning"]
        .as_str()
        .and_then(super::first_non_empty_line);
    let streams = ["stdout", "stderr"].map(|key| metadata[key].as_str());
    let output_lines = streams.iter().any(Option::is_some).then(|| {
        let count: usize = streams
            .iter()
            .flatten()
            .map(|stream| stream.lines().count())
            .sum();
        let scope = match streams {
            [Some(_), Some(_)] => "stdout + stderr",
            [Some(_), None] => "stdout; stderr unavailable",
            _ => "stderr; stdout unavailable",
        };
        let unit = if count == 1 { "line" } else { "lines" };
        format!("{count} {unit} in retained sanitized {scope}; full output total unknown")
    });
    let failure = (detail.status == super::ActivityStatus::Failed)
        .then(|| {
            metadata["stderr"]
                .as_str()
                .and_then(super::first_non_empty_line)
                .or_else(|| {
                    // Only non-stream errors (for example, a denied command). Never inspect
                    // formatted process output to invent a failure from successful stdout.
                    streams
                        .iter()
                        .all(Option::is_none)
                        .then(|| super::first_non_empty_line(&detail.output))
                        .flatten()
                })
        })
        .flatten()
        .filter(|failure| {
            cleanup.as_ref() != Some(failure)
                && timeout.as_ref() != Some(failure)
                && truncation.as_ref() != Some(failure)
        });
    [
        ("Command timeout", timeout),
        ("Output truncation", truncation),
        ("Cleanup warning", cleanup),
        ("Failure explanation", failure),
        ("Output lines", output_lines),
    ]
    .into_iter()
    .map(|(label, value)| BashNotice { label, value })
    .collect()
}