magi-code 0.101.1

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

fn hash_edit_running_ops(input: &str) -> Vec<String> {
    crate::tools::hash_edit_operation_summaries(input)
        .into_iter()
        .map(|operation| match operation {
            crate::tools::HashEditOperationSummary::Update { path } => {
                format!("Update {}", sanitized_single_line(&path))
            }
            crate::tools::HashEditOperationSummary::Move { path, destination } => format!(
                "Move {} → {}",
                sanitized_single_line(&path),
                sanitized_single_line(&destination)
            ),
            crate::tools::HashEditOperationSummary::Delete { path } => {
                format!("Delete {}", sanitized_single_line(&path))
            }
        })
        .collect()
}

pub(super) fn hash_edit_card(input: &ToolCardInput<'_>) -> CardDraft {
    let detail = input.detail;
    let metadata = detail.map(|detail| &detail.metadata);
    let error_kind = hash_edit_metadata_str(metadata, "error_kind");
    let outcome = hash_edit_metadata_str(metadata, "outcome");
    let mut status = input_status(input);
    if status == CardStatus::Failed && error_kind == Some("canceled") {
        status = CardStatus::Canceled;
    }
    let files = metadata
        .and_then(|metadata| metadata.get("files"))
        .and_then(serde_json::Value::as_array);
    let added = hash_edit_metadata_usize(metadata, "added");
    let removed = hash_edit_metadata_usize(metadata, "removed");
    let stale = outcome == Some("stale_tag") || error_kind == Some("stale_tag");
    let partial = outcome == Some("partial");
    let mut diff_preview_start = None;
    let (title, lines) = if status == CardStatus::Running {
        let operations = detail
            .and_then(|detail| detail.params.get("input"))
            .and_then(serde_json::Value::as_str)
            .map(hash_edit_running_ops)
            .unwrap_or_default();
        ("Hash Edit".to_string(), hash_edit_bounded_rows(operations))
    } else if status == CardStatus::Canceled && !partial {
        (
            "Hash Edit • canceled".to_string(),
            vec!["Canceled before completion".to_string()],
        )
    } else if stale {
        let path = hash_edit_metadata_str(metadata, "path")
            .map(sanitized_single_line)
            .unwrap_or_default();
        let expected = hash_edit_metadata_str(metadata, "expected_tag")
            .map(sanitized_single_line)
            .unwrap_or_default();
        let current = hash_edit_metadata_str(metadata, "current_tag")
            .map(sanitized_single_line)
            .unwrap_or_default();
        (
            "Hash Edit • stale tag".to_string(),
            vec![
                path,
                format!("Expected #{expected} · current #{current}"),
                "Re-read file, then retry.".to_string(),
            ],
        )
    } else if partial {
        let committed = hash_edit_metadata_usize(metadata, "committed");
        let changed = hash_edit_metadata_usize(metadata, "changed");
        let sections = hash_edit_metadata_usize(metadata, "sections");
        let rows = files
            .map(|files| {
                files
                    .iter()
                    .filter_map(hash_edit_partial_row)
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();
        let mut rows = hash_edit_bounded_rows(rows);
        rows.push(changed.map_or_else(
            || "Completed operations are not rolled back.".into(),
            |changed| {
                format!(
                    "Changes to {changed} {} remain on disk.",
                    plural(changed, "file", "files")
                )
            },
        ));
        if let Some(diff) = detail.and_then(|detail| detail.applied_diff.as_deref()) {
            diff_preview_start = Some(rows.len());
            rows.extend(hash_edit_diff_preview(
                diff,
                metadata.is_some_and(|value| value["diff_truncated"] == true),
            ));
        }
        let title = committed.zip(sections).map_or_else(
            || "Hash Edit • partial".into(),
            |(committed, sections)| format!("Hash Edit • partial {committed}/{sections}"),
        );
        (title, rows)
    } else if status == CardStatus::Failed {
        let mut rows = Vec::new();
        if let Some(path) = hash_edit_failure_path(detail, files) {
            rows.push(path);
        }
        rows.push(
            detail
                .and_then(|detail| first_non_empty_line(detail.output.as_ref()))
                .unwrap_or_else(|| "hash_edit failed".to_string()),
        );
        if metadata
            .and_then(|value| value.get("humanize_protection_denied"))
            .and_then(serde_json::Value::as_bool)
            == Some(true)
        {
            rows.extend(attempted_change_preview(
                detail.and_then(|detail| detail.params.get("input")),
            ));
        }
        ("Hash Edit".to_string(), rows)
    } else if status == CardStatus::Success {
        let operations = files
            .map(|files| {
                files
                    .iter()
                    .filter_map(hash_edit_success_row)
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();
        let mut rows = hash_edit_bounded_rows(operations);
        rows.extend(
            files
                .into_iter()
                .flatten()
                .filter_map(|file| file["warnings"].as_array())
                .flatten()
                .filter_map(serde_json::Value::as_str)
                .take(3)
                .map(|warning| format!("Warning: {}", sanitized_single_line(warning))),
        );
        let title = hash_edit_success_title(files, added, removed);
        if added
            .zip(removed)
            .is_some_and(|(added, removed)| added.saturating_add(removed) > 0)
            && let Some(diff) = detail.and_then(|detail| detail.applied_diff.as_deref())
        {
            diff_preview_start = Some(rows.len());
            rows.extend(hash_edit_diff_preview(
                diff,
                metadata
                    .and_then(|metadata| metadata.get("diff_truncated"))
                    .and_then(serde_json::Value::as_bool)
                    .unwrap_or(false),
            ));
        }
        (title, rows)
    } else {
        ("Hash Edit".to_string(), Vec::new())
    };
    let body_lines = lines
        .iter()
        .enumerate()
        .map(|(index, line)| {
            let role = if diff_preview_start.is_some_and(|start| index >= start) {
                match line.as_bytes().first() {
                    Some(b'+') => TextMeaning::Added,
                    Some(b'-') => TextMeaning::Removed,
                    _ => TextMeaning::Plain,
                }
            } else {
                TextMeaning::Plain
            };
            TextLine::from_span(line, role)
        })
        .collect();
    CardDraft {
        role: ToolCardRole::HashEdit,
        status,
        title,
        metadata: humanize_protection_metadata(detail),
        body: lines.join("\n"),
        body_lines,
        activity_rows: Vec::new(),
    }
}

fn hash_edit_metadata_str<'a>(
    metadata: Option<&'a serde_json::Value>,
    key: &str,
) -> Option<&'a str> {
    metadata?.get(key)?.as_str()
}

fn hash_edit_metadata_usize(metadata: Option<&serde_json::Value>, key: &str) -> Option<usize> {
    metadata?
        .get(key)?
        .as_u64()
        .and_then(|value| usize::try_from(value).ok())
}

fn hash_edit_bounded_rows(mut rows: Vec<String>) -> Vec<String> {
    if rows.len() <= TOOL_PREVIEW_LINES {
        return rows;
    }
    let remaining = rows.len() - TOOL_PREVIEW_LINES;
    rows.truncate(TOOL_PREVIEW_LINES);
    rows.push(format!("… {remaining} more files"));
    rows
}

fn hash_edit_success_title(
    files: Option<&Vec<serde_json::Value>>,
    added: Option<usize>,
    removed: Option<usize>,
) -> String {
    let counts = added
        .zip(removed)
        .map(|(added, removed)| format!("+{added} −{removed}"));
    let total = files.map_or(0, Vec::len);
    if total > 1 {
        let all_noop = files.is_some_and(|files| {
            files.iter().all(|file| {
                file.get("operation").and_then(serde_json::Value::as_str) == Some("noop")
            })
        });
        return if all_noop {
            format!("Hash Edit • {total} files • no changes")
        } else {
            counts.as_ref().map_or_else(
                || format!("Hash Edit • {total} files"),
                |counts| format!("Hash Edit • {total} files • {counts}"),
            )
        };
    }
    match files
        .and_then(|files| files.first())
        .and_then(|file| file.get("operation"))
        .and_then(serde_json::Value::as_str)
    {
        Some("move")
            if counts.is_some()
                && added
                    .zip(removed)
                    .is_some_and(|(added, removed)| added.saturating_add(removed) > 0) =>
        {
            format!("Hash Edit • moved • {}", counts.unwrap_or_default())
        }
        Some("move") => "Hash Edit • moved".to_string(),
        Some("delete") => removed.map_or_else(
            || "Hash Edit • deleted".into(),
            |removed| format!("Hash Edit • deleted • −{removed}"),
        ),
        Some("noop") => "Hash Edit • no changes".to_string(),
        Some("update") => counts.map_or_else(
            || "Hash Edit".into(),
            |counts| format!("Hash Edit • {counts}"),
        ),
        _ => "Hash Edit".to_string(),
    }
}

fn hash_edit_partial_row(value: &serde_json::Value) -> Option<String> {
    let path = value
        .get("path")
        .and_then(serde_json::Value::as_str)
        .map(sanitized_single_line)?;
    let status = value
        .get("status")
        .and_then(serde_json::Value::as_str)
        .unwrap_or("");
    match status {
        "committed" => hash_edit_success_row(value).map(|row| format!("✓ {row}")),
        "failed"
        | "committed_but_undurable"
        | "committed_with_error"
        | "destination_written_source_retained"
        | "partially_applied" => Some(format!(
            "✗ {path} · {}",
            value
                .get("error")
                .and_then(serde_json::Value::as_str)
                .and_then(first_non_empty_line)
                .unwrap_or_else(|| "error".to_string())
        )),
        _ => Some(format!("○ {path} · not written")),
    }
}

fn hash_edit_success_row(value: &serde_json::Value) -> Option<String> {
    let path = value
        .get("path")
        .and_then(serde_json::Value::as_str)
        .map(sanitized_single_line)?;
    match value.get("operation").and_then(serde_json::Value::as_str) {
        Some("move") => Some(format!(
            "Moved {path} → {}",
            value
                .get("destination")
                .and_then(serde_json::Value::as_str)
                .map(sanitized_single_line)
                .unwrap_or_default()
        )),
        Some("delete") => Some(format!("Deleted {path}")),
        Some("noop") => Some(format!("Unchanged {path}")),
        Some("update") => Some(format!("Updated {path}")),
        _ => None,
    }
}

fn hash_edit_failure_path(
    detail: Option<&crate::output::ToolActivityDetail>,
    files: Option<&Vec<serde_json::Value>>,
) -> Option<String> {
    files
        .and_then(|files| files.first())
        .and_then(|file| file.get("path"))
        .and_then(serde_json::Value::as_str)
        .map(sanitized_single_line)
        .or_else(|| {
            detail
                .and_then(|detail| detail.metadata.get("path"))
                .and_then(serde_json::Value::as_str)
                .map(sanitized_single_line)
        })
        .or_else(|| {
            detail
                .and_then(|detail| detail.params.get("input"))
                .and_then(serde_json::Value::as_str)
                .map(hash_edit_running_ops)
                .and_then(|rows| rows.first().cloned())
                .and_then(|row| row.split_once(' ').map(|(_, path)| path.to_string()))
        })
}

pub(super) const HASH_EDIT_DIFF_PREVIEW_LINES: usize = 10;

fn hash_edit_diff_preview(diff: &str, diff_truncated: bool) -> Vec<String> {
    let mut changed = Vec::new();
    let mut header_rows_remaining = 0usize;
    for line in diff.lines() {
        if line.starts_with("diff --git ") {
            header_rows_remaining = 2;
            continue;
        }
        if header_rows_remaining > 0 {
            header_rows_remaining -= 1;
            continue;
        }
        if line.starts_with("--- a/")
            || line == "--- /dev/null"
            || line.starts_with("+++ b/")
            || line == "+++ /dev/null"
        {
            continue;
        }
        if !(line.starts_with('+') || line.starts_with('-')) {
            continue;
        }
        let safe = sanitize_display_text(line);
        changed.push(truncate_preview_line(&safe, PREVIEW_LINE_MAX_CHARS));
    }
    let limited = diff_truncated || changed.len() > HASH_EDIT_DIFF_PREVIEW_LINES;
    changed.truncate(HASH_EDIT_DIFF_PREVIEW_LINES);
    if limited {
        changed.push(format!(
            "… diff preview limited to {HASH_EDIT_DIFF_PREVIEW_LINES} changed lines"
        ));
    }
    changed
}