magi-code 0.96.1

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

#[derive(Debug, Clone)]
pub(super) struct ReadActivityTarget {
    pub(super) target: String,
    selector_start: Option<usize>,
    has_canonical_metadata: bool,
}

pub(super) fn compact_read_activity_detail(
    detail: &crate::output::ToolActivityDetail,
) -> Option<String> {
    if let Some((offset, targets)) = canonical_read_activity_targets(&detail.metadata) {
        return compact_read_activity_targets(&targets, Some(offset));
    }
    if matches!(
        detail.status,
        ActivityStatus::Queued | ActivityStatus::Running | ActivityStatus::Writing
    ) {
        return None;
    }
    compact_read_activity_request(&detail.params)
}

fn canonical_read_activity_targets(value: &Value) -> Option<(usize, Vec<ReadActivityTarget>)> {
    let root = value.as_object()?;
    let offset = positive_read_activity_number(root.get("offset"))?;
    let entries = match root.get("results") {
        Some(results) => results.as_array()?.iter().collect::<Vec<_>>(),
        None => vec![value],
    };
    let targets = entries
        .into_iter()
        .filter_map(|entry| {
            let object = entry.as_object()?;
            let requested_target = first_nonempty_read_activity_string(object, &["requested_path"]);
            let metadata_target = first_nonempty_read_activity_string(object, &["source", "path"]);
            let failed = object.get("success").and_then(Value::as_bool) == Some(false);
            let target = if failed {
                requested_target.or(metadata_target)?
            } else {
                metadata_target.or(requested_target)?
            };
            let fallback_raw = match object.get("raw") {
                None => Some(false),
                Some(Value::Bool(raw)) => Some(*raw),
                Some(_) => None,
            };
            let selector_metadata = fallback_raw.and_then(|fallback_raw| {
                read_activity_selector_metadata(object.get("selector"), fallback_raw)
            });
            Some(ReadActivityTarget {
                target: target.to_string(),
                selector_start: selector_metadata.and_then(|metadata| metadata.start),
                has_canonical_metadata: !failed
                    && metadata_target.is_some()
                    && selector_metadata.is_some(),
            })
        })
        .collect::<Vec<_>>();
    (!targets.is_empty()).then_some((offset, targets))
}

fn first_nonempty_read_activity_string<'a>(
    object: &'a serde_json::Map<String, Value>,
    keys: &[&str],
) -> Option<&'a str> {
    keys.iter().find_map(|key| {
        object
            .get(*key)
            .and_then(Value::as_str)
            .filter(|value| !value.trim().is_empty())
    })
}

fn positive_read_activity_number(value: Option<&Value>) -> Option<usize> {
    value?
        .as_u64()
        .and_then(|value| usize::try_from(value).ok())
        .filter(|value| *value > 0)
}

#[derive(Debug, Clone, Copy)]
pub(super) struct ReadActivitySelector {
    start: Option<usize>,
}

fn read_activity_selector_metadata(
    selector: Option<&Value>,
    fallback_raw: bool,
) -> Option<ReadActivitySelector> {
    let selector = selector?;
    if selector.is_null() {
        return Some(ReadActivitySelector {
            start: fallback_raw.then_some(1),
        });
    }
    let selector = selector.as_object()?;
    let raw = match selector.get("raw") {
        None => fallback_raw,
        Some(Value::Bool(raw)) => *raw,
        Some(_) => return None,
    };
    let lines = match selector.get("lines") {
        None | Some(Value::Null) => {
            return Some(ReadActivitySelector {
                start: raw.then_some(1),
            });
        }
        Some(Value::Object(lines)) => lines,
        Some(_) => return None,
    };
    let start = match lines.get("type").and_then(Value::as_str)? {
        "single" => positive_read_activity_number(lines.get("line"))?,
        "range" => {
            let start = positive_read_activity_number(lines.get("start"))?;
            let end = positive_read_activity_number(lines.get("end"))?;
            (end >= start).then_some(start)?
        }
        "from_start" => {
            positive_read_activity_number(lines.get("end"))?;
            1
        }
        "from_line" => positive_read_activity_number(lines.get("start"))?,
        "count" => {
            let start = positive_read_activity_number(lines.get("start"))?;
            positive_read_activity_number(lines.get("count"))?;
            start
        }
        _ => return None,
    };
    Some(ReadActivitySelector { start: Some(start) })
}

fn compact_read_activity_request(params: &Value) -> Option<String> {
    let params = params.as_object()?;
    let targets = match params.get("paths") {
        Some(paths) if !paths.is_null() => {
            if params.get("path").is_some_and(|path| !path.is_null()) {
                return None;
            }
            paths
                .as_array()?
                .iter()
                .map(Value::as_str)
                .collect::<Option<Vec<_>>>()?
        }
        Some(Value::Null) | None => vec![params.get("path")?.as_str()?],
        Some(_) => return None,
    };
    if targets.is_empty() {
        return None;
    }
    let targets = targets
        .into_iter()
        .map(|target| ReadActivityTarget {
            target: target.to_string(),
            selector_start: None,
            has_canonical_metadata: false,
        })
        .collect::<Vec<_>>();
    compact_read_activity_targets(&targets, None)
}

fn compact_read_activity_targets(
    targets: &[ReadActivityTarget],
    offset: Option<usize>,
) -> Option<String> {
    let sanitized_targets = targets
        .iter()
        .map(|target| sanitize_read_activity_target(&target.target))
        .collect::<Vec<_>>();
    let summaries = sanitized_targets
        .iter()
        .enumerate()
        .filter_map(|(index, target)| {
            if target.is_empty() {
                return None;
            }
            let display_target = if is_read_activity_resource(target) {
                target.clone()
            } else {
                shortest_unique_activity_path_suffix(index, &sanitized_targets, true)
            };
            if display_target.is_empty() {
                return None;
            }
            let start = if targets[index].has_canonical_metadata {
                targets[index].selector_start.or_else(|| {
                    if is_read_activity_resource(target) {
                        None
                    } else {
                        offset
                    }
                })
            } else {
                None
            };
            Some(match start {
                Some(start) => format!("{display_target}:{start}"),
                None => display_target,
            })
        })
        .collect::<Vec<_>>();
    let mut summary = summaries
        .iter()
        .take(2)
        .cloned()
        .collect::<Vec<_>>()
        .join(", ");
    if summary.is_empty() {
        return None;
    }
    if targets.len() > 2 {
        summary.push_str(" …");
    }
    Some(summary)
}

fn is_read_activity_resource(target: &str) -> bool {
    target.contains("://")
}

pub(super) fn shortest_unique_activity_path_suffix(
    index: usize,
    targets: &[String],
    preserve_resources: bool,
) -> String {
    let target = &targets[index];
    if preserve_resources && is_read_activity_resource(target) {
        return target.clone();
    }
    let segments = activity_path_segments(target);
    for length in 1..=segments.len() {
        let candidate = segments[segments.len() - length..].join("/");
        let suffix = &segments[segments.len() - length..];
        let collides = targets.iter().enumerate().any(|(other_index, other)| {
            if other_index == index
                || other.is_empty()
                || (preserve_resources && is_read_activity_resource(other))
            {
                return false;
            }
            let other_segments = activity_path_segments(other);
            other_segments.len() >= length
                && other_segments[other_segments.len() - length..] == *suffix
        });
        if !collides {
            return candidate;
        }
    }
    segments.join("/")
}
fn activity_path_segments(target: &str) -> Vec<&str> {
    target
        .trim_end_matches(['/', '\\'])
        .split(['/', '\\'])
        .filter(|segment| !segment.is_empty())
        .collect()
}

pub(crate) fn sanitize_read_activity_label(label: &str) -> String {
    let label = crate::output::sanitize_display_text(label);
    label.strip_prefix("read ").map_or_else(
        || sanitize_read_activity_target(&label),
        |target| format!("read {}", sanitize_read_activity_target(target)),
    )
}

pub(super) fn sanitize_read_activity_target(target: &str) -> String {
    let sanitized = crate::output::sanitize_display_text(target)
        .chars()
        .filter(|character| !character.is_control())
        .collect::<String>();
    let trimmed = sanitized.trim();
    if is_http_activity_url(trimmed) {
        if let Ok(mut url) = reqwest::Url::parse(trimmed) {
            let _ = url.set_username("");
            let _ = url.set_password(None);
            url.set_query(None);
            url.set_fragment(None);
            return normalize_inline_separators(url.as_ref());
        }
        return normalize_inline_separators(&strip_http_activity_url_parts(trimmed));
    }
    normalize_inline_separators(&sanitized)
}

fn is_http_activity_url(target: &str) -> bool {
    target
        .get(.."http://".len())
        .is_some_and(|scheme| scheme.eq_ignore_ascii_case("http://"))
        || target
            .get(.."https://".len())
            .is_some_and(|scheme| scheme.eq_ignore_ascii_case("https://"))
}

fn strip_http_activity_url_parts(target: &str) -> String {
    let mut safe = target
        .split(['?', '#'])
        .next()
        .unwrap_or(target)
        .to_string();
    let Some(scheme_end) = safe.find("://") else {
        return safe;
    };
    let authority_start = scheme_end + 3;
    let authority_end = safe[authority_start..]
        .find('/')
        .map_or(safe.len(), |end| authority_start + end);
    if let Some(userinfo_end) = safe[authority_start..authority_end].rfind('@') {
        safe.replace_range(authority_start..authority_start + userinfo_end + 1, "");
    }
    safe
}