magi-code 0.63.3

Repository-aware CLI coding agent for terminal work
Documentation
use super::transcript::sanitize_preview;
use crate::{
    output::{
        ActivityEvent, ActivityId, ActivityKind, ActivityMetadata, ActivityStatus,
        is_credential_like_key, redact_sensitive_text,
    },
    rendering::{DisplayLine, DisplayRole, DisplaySpan, render},
    tui::{PADDED_INLINE_SEPARATOR, normalize_inline_separators},
};

pub(crate) const MAX_ACTIVITY_PREVIEW_BYTES: usize = 16 * 1024;
pub(crate) const MAX_ACTIVITY_PREVIEW_LINES: usize = 200;

#[derive(Debug, Default, Clone)]
pub(crate) struct ReasoningSummaryGroup {
    items: Vec<(ActivityId, Vec<String>)>,
}

impl ReasoningSummaryGroup {
    pub(crate) fn reset(&mut self) {
        self.items.clear();
    }
    pub(crate) fn append_identity_event(
        &mut self,
        id: ActivityId,
        parent_id: Option<ActivityId>,
        text: &str,
    ) -> Option<ActivityEvent> {
        let lines = text
            .lines()
            .map(str::trim)
            .filter(|line| !line.is_empty())
            .map(str::to_string)
            .collect::<Vec<_>>();
        if lines.is_empty() {
            return None;
        }
        let entry = self.items.iter_mut().find(|(entry_id, _)| *entry_id == id);
        let lines = if let Some((_, existing)) = entry {
            *existing = lines;
            existing.clone()
        } else {
            self.items.push((id.clone(), lines));
            self.items
                .last()
                .map(|(_, lines)| lines.clone())
                .unwrap_or_default()
        };
        let mut metadata = ActivityMetadata::new("reasoning summary");
        metadata.detail = Some(numbered_reasoning_lines(&lines));
        Some(ActivityEvent::Started {
            id,
            parent_id,
            kind: ActivityKind::Assistant,
            status: ActivityStatus::Success,
            metadata,
        })
    }
}

pub(crate) fn numbered_reasoning_lines(lines: &[String]) -> String {
    lines
        .iter()
        .enumerate()
        .map(|(index, line)| format!("{}. {line}", index + 1))
        .collect::<Vec<_>>()
        .join("\n")
}

#[derive(Debug, Clone)]
pub(crate) struct ActivityNode {
    pub(crate) id: ActivityId,
    pub(crate) parent_id: Option<ActivityId>,
    pub(crate) kind: ActivityKind,
    pub(crate) status: ActivityStatus,
    pub(crate) metadata: ActivityMetadata,
    pub(crate) started_at: chrono::DateTime<chrono::Local>,
    pub(crate) preview: String,
    pub(crate) preview_truncated: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ActivityDetailLines {
    pub(crate) summary: Vec<DisplayLine>,
    pub(crate) body: Vec<DisplayLine>,
}

pub(crate) fn activity_detail_lines(node: &ActivityNode) -> ActivityDetailLines {
    let mut summary = vec![
        field_line(
            "status",
            format!(
                "{} {}",
                activity_status_mark(node.status),
                activity_status_label(node.status)
            ),
        ),
        field_line("id", node.id.as_str()),
        field_line("type", activity_kind_label(&node.kind)),
        field_line(
            "label",
            normalize_inline_separators(&redact_sensitive_text(&node.metadata.label)),
        ),
    ];
    let mut body = Vec::new();

    if let Some(detail) = &node.metadata.detail {
        let redacted_detail = redact_sensitive_text(detail);
        let detail_lines = redacted_detail.lines().collect::<Vec<_>>();
        if detail_lines.len() > 1 && node.metadata.label.starts_with("reasoning summaries ×") {
            summary.push(field_line(
                "summary",
                format!("{} lines", detail_lines.len()),
            ));
            if !body.is_empty() {
                body.push(DisplayLine::plain(""));
            }
            body.push(section_line("Detail"));
            body.extend(detail_lines.into_iter().map(DisplayLine::plain));
        } else {
            summary.push(field_line(
                "summary",
                normalize_inline_separators(&redacted_detail),
            ));
        }
    }

    if !node.metadata.fields.is_empty() {
        body.push(section_line("Metadata"));
        for (key, value) in &node.metadata.fields {
            body.push(field_line(
                display_metadata_key(key),
                redact_activity_metadata_value(key, value),
            ));
        }
    }

    if !node.preview.is_empty() {
        if !body.is_empty() {
            body.push(DisplayLine::plain(""));
        }
        body.push(section_line("Sanitized output preview"));
        body.extend(render_activity_preview_lines(&node.preview));
    }

    ActivityDetailLines { summary, body }
}

fn section_line<T: Into<String>>(text: T) -> DisplayLine {
    DisplayLine::from_span(text, DisplayRole::Heading)
}

fn field_line(label: impl AsRef<str>, value: impl AsRef<str>) -> DisplayLine {
    DisplayLine {
        spans: vec![
            DisplaySpan::new(label.as_ref(), DisplayRole::ListMarker),
            DisplaySpan::new(": ", DisplayRole::Punctuation),
            DisplaySpan::new(value.as_ref(), DisplayRole::Plain),
        ],
    }
}

fn render_activity_preview_lines(preview: &str) -> Vec<DisplayLine> {
    let mut out = Vec::new();
    let mut plain = Vec::new();
    let mut diff = Vec::new();
    let mut in_diff = false;

    for line in preview.split('\n') {
        if line == "DIFF:" || line == "Diff preview" {
            flush_plain(&mut out, &mut plain);
            in_diff = true;
            out.push(section_line("Diff preview"));
            continue;
        }
        if in_diff {
            diff.push(line.to_string());
        } else {
            plain.push(line.to_string());
        }
    }
    flush_plain(&mut out, &mut plain);
    if !diff.is_empty() {
        while diff.last().is_some_and(|line| line.is_empty()) {
            diff.pop();
        }
        out.extend(render(
            &diff.join("\n"),
            crate::rendering::RenderInputKind::Diff,
        ));
    }
    out
}

fn flush_plain(out: &mut Vec<DisplayLine>, lines: &mut Vec<String>) {
    if lines.is_empty() {
        return;
    }
    while lines.last().is_some_and(|line| line.is_empty()) {
        lines.pop();
    }
    out.extend(lines.drain(..).map(DisplayLine::plain));
}

pub(crate) fn activity_status_mark(status: ActivityStatus) -> &'static str {
    match status {
        ActivityStatus::Queued => "",
        ActivityStatus::Running | ActivityStatus::Writing => "",
        ActivityStatus::Success => "",
        ActivityStatus::Failed => "",
        ActivityStatus::Canceled => "",
    }
}

fn activity_status_label(status: ActivityStatus) -> &'static str {
    match status {
        ActivityStatus::Queued => "QUEUED",
        ActivityStatus::Running => "RUNNING",
        ActivityStatus::Writing => "WRITING",
        ActivityStatus::Success => "SUCCESS",
        ActivityStatus::Failed => "FAILED",
        ActivityStatus::Canceled => "CANCELED",
    }
}

fn activity_kind_label(kind: &ActivityKind) -> &'static str {
    match kind {
        ActivityKind::Tool => "tool",
        ActivityKind::Hook => "hook",
        ActivityKind::ProviderContextInjection => "provider context injection",
        ActivityKind::SubagentBatch => "subagent batch",
        ActivityKind::SubagentTask => "subagent task",
        ActivityKind::Assistant => "assistant",
        ActivityKind::Diagnostic => "diagnostic",
        ActivityKind::Compaction => "compaction",
    }
}

pub(crate) fn compact_activity_metadata(fields: &[(String, String)], max_fields: usize) -> String {
    fields
        .iter()
        .take(max_fields)
        .map(|(key, value)| compact_activity_metadata_part(key, value))
        .collect::<Vec<_>>()
        .join(PADDED_INLINE_SEPARATOR)
}

fn compact_activity_metadata_part(key: &str, value: &str) -> String {
    let value = normalize_inline_separators(&redact_activity_metadata_value(key, value));
    match key {
        "identity" => format!("identity: {value}"),
        _ => format!("{} {value}", display_metadata_key(key)),
    }
}

fn display_metadata_key(key: &str) -> String {
    match key {
        "exit_code" => "exit".to_string(),
        "output_lines" => "lines".to_string(),
        "edit_count" => "edits".to_string(),
        "matches_returned" => "matches".to_string(),
        other => other.replace('_', " "),
    }
}

fn redact_activity_metadata_value(key: &str, value: &str) -> String {
    if is_credential_like_key(key) {
        "<redacted>".to_string()
    } else {
        redact_sensitive_text(value)
    }
}

pub(super) fn append_preview(node: &mut ActivityNode, preview: &str) {
    if node.preview_truncated {
        return;
    }
    let mut combined = String::with_capacity(node.preview.len().saturating_add(preview.len()));
    combined.push_str(&node.preview);
    combined.push_str(&sanitize_preview(preview));
    let (bounded, truncated) = bound_preview(&combined);
    node.preview = bounded;
    node.preview_truncated |= truncated;
}

fn bound_preview(text: &str) -> (String, bool) {
    let mut truncated = false;
    let mut by_lines = String::new();
    for (index, line) in text.split_inclusive('\n').enumerate() {
        if index >= MAX_ACTIVITY_PREVIEW_LINES {
            truncated = true;
            break;
        }
        by_lines.push_str(line);
    }
    if !truncated && text.lines().count() > MAX_ACTIVITY_PREVIEW_LINES {
        truncated = true;
    }

    if by_lines.len() > MAX_ACTIVITY_PREVIEW_BYTES {
        truncated = true;
    }
    let mut out = String::new();
    for ch in by_lines.chars() {
        if out.len().saturating_add(ch.len_utf8()) > MAX_ACTIVITY_PREVIEW_BYTES {
            truncated = true;
            break;
        }
        out.push(ch);
    }
    (out, truncated)
}

pub(super) fn final_preview_detail(tool_name: &str) -> String {
    match tool_name {
        "bash" | "shell" => "bash preview: final output only".to_string(),
        _ => "final tool output preview".to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const OLD_ACTIVITY_PREVIEW_TRUNCATED_MARKER: &str = "[… activity preview truncated …]";

    #[test]
    fn bound_preview_limits_bytes_without_visible_marker() {
        let (bounded, truncated) = bound_preview(&"x".repeat(MAX_ACTIVITY_PREVIEW_BYTES + 1));

        assert!(truncated);
        assert!(bounded.len() <= MAX_ACTIVITY_PREVIEW_BYTES);
        assert!(!bounded.contains(OLD_ACTIVITY_PREVIEW_TRUNCATED_MARKER));
    }

    #[test]
    fn bound_preview_limits_lines_without_visible_marker() {
        let (bounded, truncated) = bound_preview(&"line\n".repeat(MAX_ACTIVITY_PREVIEW_LINES + 20));

        assert!(truncated);
        assert!(bounded.lines().count() <= MAX_ACTIVITY_PREVIEW_LINES);
        assert!(!bounded.contains(OLD_ACTIVITY_PREVIEW_TRUNCATED_MARKER));
    }
}