magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use crate::tui::{
    state::MissionControlState,
    usage_format::{USAGE_SEPARATOR, compact_tokens, usage_metric_spans},
};
use ratatui::{
    Frame,
    layout::Rect,
    text::{Line, Span},
    widgets::Paragraph,
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;

pub(super) fn draw_prompt_footer(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
    if area.width <= 2 || area.height < 2 {
        return;
    }
    let available = usize::from(area.width - 2);
    let full_left = footer_text(state, true);
    let right = super::super::header::version_title(state);
    // Independent rectangles prevent opposing border labels from colliding.
    let right_width = right.width().min(available / 2);
    let left_width = available.saturating_sub(right_width + usize::from(right_width > 0));
    let left = if full_left.width() <= left_width {
        full_left
    } else {
        footer_text(state, false)
    };
    let y = area.bottom() - 1;
    frame.render_widget(
        Paragraph::new(truncate_line(left, left_width)),
        Rect::new(area.x + 1, y, left_width as u16, 1),
    );
    frame.render_widget(
        Paragraph::new(truncate_line(right, right_width)).right_aligned(),
        Rect::new(
            area.right() - 1 - right_width as u16,
            y,
            right_width as u16,
            1,
        ),
    );
}

fn footer_text(state: &MissionControlState, show_bar: bool) -> Line<'static> {
    let auto = state.compaction_settings.auto_for_provider(
        &state.provider,
        state
            .context_usage
            .map_or(usize::MAX, |usage| usage.max_tokens),
    );
    let policy = state
        .context_usage
        .and_then(|usage| crate::agent::runner::auto_compaction_policy(&auto, usage.max_tokens));
    let auto_label =
        auto_compaction_label(&auto, state.context_usage.map(|usage| usage.max_tokens));
    let usage = state.context_usage;
    let context = usage.map_or_else(
        || "—%".to_string(),
        |usage| context_usage_label(usage.current_tokens, usage.max_tokens),
    );
    let muted = state.theme.muted(state.is_prompt_focused());
    let session = usage_metric_spans(
        state.session_usage_totals(),
        state.session_cache_percent(),
        muted,
        state.theme,
    );
    let elapsed = state
        .session_active_time
        .elapsed(std::time::Instant::now())
        .as_secs();
    let mut spans = vec![Span::styled(
        format!(" Session: {}m{:02}s • ", elapsed / 60, elapsed % 60),
        muted,
    )];
    for (index, field) in session.into_iter().enumerate() {
        if index > 0 {
            spans.push(Span::styled(USAGE_SEPARATOR, muted));
        }
        spans.push(field);
    }
    spans.push(Span::styled(" • Current Ctx: ", muted));
    spans.push(Span::styled(context, muted.fg(state.theme.text_status())));
    spans.push(Span::styled(" ", muted));
    if show_bar {
        let bar = context_bar(
            usage.map_or(0, |usage| usage.current_tokens),
            usage.map_or(0, |usage| usage.max_tokens),
            policy.as_ref().map(|(tokens, _)| *tokens),
        );
        spans.push(Span::styled(bar, muted.fg(state.theme.text_accent())));
        spans.push(Span::styled(" ", muted));
    }
    let total = usage.map_or_else(
        || "—".to_string(),
        |usage| compact_tokens(usage.max_tokens as u64),
    );
    spans.push(Span::styled(total, muted.fg(state.theme.text_status())));
    spans.push(Span::styled(" • ", muted));
    spans.push(Span::styled(
        auto_label,
        muted.fg(state.theme.text_command()),
    ));
    spans.push(Span::styled(" ", muted));
    Line::from(spans)
}

fn auto_compaction_label(
    auto: &crate::config::AutoCompactionSettings,
    max_tokens: Option<usize>,
) -> String {
    if !auto.enabled {
        return "Auto off".to_string();
    }
    let threshold = match (auto.threshold_percent, auto.threshold_tokens, max_tokens) {
        (Some(percent), Some(tokens), Some(max_tokens)) => {
            let percent_tokens = (max_tokens as u128 * u128::from(percent)).div_ceil(100);
            if u128::from(tokens) < percent_tokens {
                format!("{} tokens", compact_tokens(tokens))
            } else {
                format!("{percent}%")
            }
        }
        (Some(percent), None, _) => format!("{percent}%"),
        (None, Some(tokens), _) => format!("{} tokens", compact_tokens(tokens)),
        _ => match auto.threshold_display() {
            Some(threshold) => threshold,
            None => return "Auto off".to_string(),
        },
    };
    format!("Compact at {threshold}")
}

fn context_usage_label(current_tokens: usize, max_tokens: usize) -> String {
    let percent = if max_tokens == 0 {
        "—".to_string()
    } else {
        ((current_tokens as u128 * 100 / max_tokens as u128).min(999)).to_string()
    };
    format!("{percent}%")
}

fn context_bar(current_tokens: usize, max_tokens: usize, threshold: Option<usize>) -> String {
    const CELLS: usize = 10;
    let filled = if max_tokens == 0 {
        0
    } else {
        ((current_tokens as u128 * CELLS as u128 / max_tokens as u128).min(CELLS as u128)) as usize
    };
    let marker = threshold.filter(|_| max_tokens > 0).map(|tokens| {
        ((tokens as u128 * CELLS as u128 / max_tokens as u128).min((CELLS - 1) as u128)) as usize
    });
    let mut bar = String::from("[");
    for index in 0..CELLS {
        bar.push(if marker == Some(index) {
            '|'
        } else if index < filled {
            '#'
        } else {
            '.'
        });
    }
    bar.push(']');
    bar
}

pub(super) fn truncate_line(line: Line<'static>, width: usize) -> Line<'static> {
    if line.width() <= width {
        return line;
    }
    if width == 0 {
        return Line::default();
    }
    let mut remaining = width - 1;
    let mut spans = Vec::new();
    'spans: for span in line.spans {
        let mut text = String::new();
        for grapheme in span.content.graphemes(true) {
            let cells = grapheme.width();
            if cells > remaining {
                spans.push(Span::styled(text, span.style));
                break 'spans;
            }
            text.push_str(grapheme);
            remaining -= cells;
        }
        spans.push(Span::styled(text, span.style));
    }
    spans.push(Span::raw("…"));
    Line::from(spans).style(line.style)
}