magi-code 0.97.5

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;
use crate::output::{ActivityKind, NormalizedUsageAggregate};

/// Caller supplies only permitted summaries/activity text, never raw live reasoning.
pub(crate) struct TaskFacts<'a> {
    pub(crate) label: &'a str,
    pub(crate) intent: Option<&'a str>,
    pub(crate) status: Option<ActivityStatus>,
    pub(crate) fields: &'a [(String, String)],
    pub(crate) failure: Option<&'a str>,
    pub(crate) usage: Option<NormalizedUsageAggregate>,
    pub(crate) live_tail: &'a [String],
}
pub(crate) struct TaskSummary {
    pub(crate) label: String,
    pub(crate) intent: String,
    pub(crate) status: CardStatus,
    pub(crate) body: String,
    pub(crate) identity: Option<String>,
    pub(crate) agent: Option<String>,
    pub(crate) model_line: Option<String>,
    pub(crate) usage: Option<NormalizedUsageAggregate>,
    pub(crate) live_tail: Vec<String>,
}
pub(crate) fn project_task(facts: &TaskFacts<'_>) -> TaskSummary {
    let field = |key: &str| {
        facts
            .fields
            .iter()
            .find(|(name, _)| name == key)
            .map(|(_, value)| compact_line(value))
            .filter(|text| !text.trim().is_empty())
    };
    let label = compact_line(facts.label);
    let model_line = field("provider")
        .zip(field("model"))
        .map(|(provider, model)| {
            field("reasoning").map_or_else(
                || format!("{provider}/{model}"),
                |reasoning| format!("{provider}/{model}({reasoning})"),
            )
        });
    TaskSummary {
        intent: facts
            .intent
            .map(compact_line)
            .unwrap_or_else(|| label.clone()),
        label,
        status: match facts.status {
            Some(ActivityStatus::Queued) | None => CardStatus::None,
            Some(status) => card_status_from_activity(status),
        },
        body: facts
            .failure
            .and_then(first_non_empty_line)
            .map(|line| compact_line(&line))
            .unwrap_or_default(),
        identity: field("identity"),
        agent: field("agent"),
        model_line: model_line.map(|text| compact_line(&text)),
        usage: facts.usage,
        live_tail: facts
            .live_tail
            .iter()
            .rev()
            .take(32)
            .rev()
            .map(|text| compact_line(text))
            .collect(),
    }
}

pub(crate) fn task_counts(statuses: impl IntoIterator<Item = CardStatus>) -> (usize, usize, usize) {
    statuses
        .into_iter()
        .fold((0, 0, 0), |(done, failed, running), status| match status {
            CardStatus::Success | CardStatus::Canceled => (done + 1, failed, running),
            CardStatus::Failed => (done, failed + 1, running),
            CardStatus::Running => (done, failed, running + 1),
            CardStatus::None => (done, failed, running),
        })
}

pub(crate) struct TaskActivityFacts<'a> {
    pub(crate) kind: &'a ActivityKind,
    pub(crate) status: ActivityStatus,
    pub(crate) label: &'a str,
    pub(crate) permitted_preview: &'a str,
    pub(crate) tool_detail: Option<&'a ToolActivityDetail>,
    pub(crate) reasoning_summary: Option<(Option<usize>, Option<&'a str>)>,
}

pub(crate) fn project_task_activity(facts: &TaskActivityFacts<'_>) -> Vec<String> {
    if let Some((count, latest)) = facts.reasoning_summary {
        return match latest.filter(|text| !text.trim().is_empty()) {
            Some(latest) => vec![match count {
                Some(count) if count > 1 => {
                    format!("reasoning summaries ×{count} • {}", compact_line(latest))
                }
                _ => format!("reasoning • {}", compact_line(latest)),
            }],
            None => vec![compact_line(facts.label)],
        };
    }
    if *facts.kind == ActivityKind::Tool {
        let (name, label) = facts.tool_detail.map_or(("tool", facts.label), |detail| {
            (detail.tool_name.as_ref(), detail.label.as_ref())
        });
        let mark = match facts.status {
            ActivityStatus::Queued => "○",
            ActivityStatus::Running | ActivityStatus::Writing => "●",
            ActivityStatus::Success => "✓",
            ActivityStatus::Failed => "✗",
            ActivityStatus::Canceled => "⊘",
            ActivityStatus::Interrupted => "?",
        };
        let target = (ToolCapability::from_dispatch_name(name).is_some()
            || name == crate::code_mode::TOOL_NAME)
            .then(|| compact_line(strip_tool_prefix(label, name)))
            .filter(|label| !label.is_empty() && !label.eq_ignore_ascii_case(name));
        return vec![target.map_or_else(
            || format!("{} • {mark}", compact_line(name)),
            |target| format!("{} • {target} • {mark}", compact_line(name)),
        )];
    }
    // Unclassified reasoning activity is not an approved summary source.
    if facts.label.trim_start().starts_with("reasoning") {
        return Vec::new();
    }
    let (prefix, source) = match facts.kind {
        ActivityKind::Assistant | ActivityKind::SubagentTask => ("agent", facts.permitted_preview),
        ActivityKind::Diagnostic => (
            "diagnostic",
            if facts.permitted_preview.is_empty() {
                facts.label
            } else {
                facts.permitted_preview
            },
        ),
        ActivityKind::Hook => ("hook", facts.label),
        ActivityKind::ProviderContextInjection => ("provider context injection", facts.label),
        ActivityKind::SubagentBatch => ("subagent batch", facts.label),
        ActivityKind::Compaction => ("compaction", facts.label),
        ActivityKind::Tool => unreachable!(),
    };
    source
        .lines()
        .rev()
        .filter(|line| !line.trim().is_empty())
        .take(32)
        .map(|line| {
            format!(
                "{prefix} • {}",
                compact_line(strip_tool_prefix(line, prefix))
            )
        })
        .collect()
}

fn strip_tool_prefix<'a>(label: &'a str, tool: &str) -> &'a str {
    let Some(prefix) = label.get(..tool.len()) else {
        return label;
    };
    let Some(rest) = label.get(tool.len()..) else {
        return label;
    };
    if prefix.eq_ignore_ascii_case(tool)
        && rest
            .chars()
            .next()
            .is_some_and(|ch| ch.is_whitespace() || matches!(ch, '•' | '·'))
    {
        rest.trim_start_matches([' ', '\t', '•', '·'])
    } else {
        label
    }
}
fn compact_line(text: &str) -> String {
    truncate_preview_line(&sanitized_single_line(text.trim()), PREVIEW_LINE_MAX_CHARS)
}