magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use crate::tui::{state::MissionControlState, theme::MissionControlTheme};
use ratatui::{
    Frame,
    layout::Rect,
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::{Clear, Paragraph},
};

const LOGO: [&str; 5] = [
    "█▀▄▀█ ▄▀█ █▀▀ █   █▀▀ █▀█ █▀▄ █▀▀",
    "█ ▀ █ █▀█ █ █ █   █   █ █ █ █ █▀▀",
    "▀   ▀ ▀ ▀ ▀▀▀ ▀   ▀▀▀ ▀▀▀ ▀▀  ▀▀▀",
    "",
    "Follow the white rabbit, but take your time.",
];
const SHORTCUTS: [(&str, &str); 10] = [
    ("[Alt-A]", "Show/hide right panel"),
    ("[Tab]", "Cycle pane focus"),
    ("[Alt-P]", "Focus prompt"),
    ("[Alt-1/2]", "Activity / Summary tab"),
    ("/skills", "Manage global / project skills"),
    ("/models", "Manage available models"),
    (
        "/summarize-start | /summarize-stop",
        "Start / stop Summary agent",
    ),
    ("/fast", "Toggle Fast mode where supported"),
    ("/compact", "Compact current session"),
    ("/update", "Update Magi-Code"),
];

pub(super) fn draw_welcome(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
    if area.is_empty() {
        return;
    }
    let theme = state.theme;
    let width = area.width.saturating_sub(2).clamp(1, 76);
    let compact = width < 44 || area.height < 26;
    let accent = Style::default().fg(theme.text_accent());
    let command = Style::default()
        .fg(theme.text_command())
        .add_modifier(Modifier::BOLD);
    let mut lines = Vec::new();
    if compact {
        lines.push(logo_line("MAGI CODE", state).centered());
    } else {
        for (index, row) in LOGO.into_iter().enumerate() {
            lines.push(if index < 3 {
                logo_line(row, state).centered()
            } else {
                Line::styled(row, accent).centered()
            });
        }
        lines.push(Line::default());
        for row in [r"(\_/)", r"( •_•)", r"/ >_ "] {
            lines.push(Line::styled(row, theme.muted(false)).centered());
        }
    }
    lines.push(Line::default());
    for (index, (key, description)) in SHORTCUTS.into_iter().enumerate() {
        if index == 0 || index == 4 {
            if index == 4 {
                lines.push(Line::default());
            }
            let heading = if index == 0 {
                "Useful hotkeys"
            } else {
                "Useful Commands"
            };
            lines.push(Line::styled(heading, accent.add_modifier(Modifier::BOLD)));
        }
        let full = Line::from(vec![
            Span::styled(key, command),
            Span::styled(format!("  {description}"), theme.muted(false)),
        ]);
        // Keep commands intact on narrow panes; descriptions are optional help.
        if full.width() <= usize::from(width) {
            lines.push(full);
        } else if key.len() <= usize::from(width) {
            lines.push(Line::styled(key, command));
        } else {
            for part in key.split(" | ") {
                lines.push(Line::styled(part, command));
            }
        }
    }
    lines.push(Line::default());
    lines.push(Line::styled("Type a prompt below to begin.", theme.muted(false)).centered());
    let height = u16::try_from(lines.len())
        .unwrap_or(u16::MAX)
        .min(area.height);
    let content = Rect::new(
        area.x + area.width.saturating_sub(width) / 2,
        area.y + area.height.saturating_sub(height) / 2,
        width,
        height,
    );
    if state.welcome_animation_active() {
        draw_rain(frame, area, theme, state.welcome_animation_phase);
    }
    // Keep a quiet, opaque reading area over the moving background.
    frame.render_widget(Clear, content);
    frame.render_widget(Paragraph::new(lines).style(theme.pane()), content);
}

fn logo_line(text: &str, state: &MissionControlState) -> Line<'static> {
    let style = Style::default()
        .fg(state.theme.text_accent())
        .add_modifier(Modifier::BOLD);
    if state.welcome_animation_active() {
        Line::from(super::super::prompt::fast_mode_rainbow_spans(
            text,
            state.welcome_animation_phase,
            style,
            state.theme,
        ))
    } else {
        Line::styled(text.to_owned(), style)
    }
}

// Original, deterministic character rain: no particles, RNG state or allocations.
// Work is bounded by the visible width and a short tail per column.
fn draw_rain(frame: &mut Frame<'_>, area: Rect, theme: MissionControlTheme, phase: usize) {
    const GLYPHS: &[u8] = b"01.:|+*";
    for column in (0..area.width).step_by(3) {
        let seed = usize::from(column).wrapping_mul(2654435761);
        let tail_length = 4 + seed % 7;
        let cycle = usize::from(area.height) + tail_length + 9;
        let head = (phase / (2 + seed % 3)).wrapping_add(seed) % cycle;
        for tail in 0..tail_length {
            let Some(row) = head
                .checked_sub(tail)
                .filter(|&row| row < usize::from(area.height))
            else {
                continue;
            };
            let color = if tail == 0 {
                theme.text_accent()
            } else {
                theme.text_muted()
            };
            let glyph = GLYPHS[seed.wrapping_add(row).wrapping_add(phase / 6) % GLYPHS.len()];
            frame.buffer_mut()[(area.x + column, area.y + row as u16)]
                .set_char(char::from(glyph))
                .set_style(Style::default().fg(color).add_modifier(Modifier::DIM));
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tui::{
        render::{test_support::find_text, transcript::draw_transcript},
        state::MissionControlState,
    };
    use ratatui::{Terminal, backend::TestBackend};

    #[test]
    fn empty_transcript_shows_welcome_and_theme_colored_commands() {
        let state = MissionControlState::default();
        let mut terminal = Terminal::new(TestBackend::new(100, 34)).unwrap();
        terminal
            .draw(|frame| draw_welcome(frame, frame.area(), &state))
            .unwrap();
        let buffer = terminal.backend().buffer();
        let tagline = find_text(buffer, "Follow the white rabbit, but take your time.").unwrap();
        let rabbit = find_text(buffer, r"(\_/)").unwrap();
        assert_eq!(rabbit.1, tagline.1 + 2);
        for x in tagline.0..tagline.0 + 42 {
            assert_eq!(buffer[(x, tagline.1 + 1)].symbol(), " ");
        }
        let hotkeys = find_text(buffer, "Useful hotkeys").unwrap();
        let commands = find_text(buffer, "Useful Commands").unwrap();
        let last_hotkey = find_text(buffer, "[Alt-1/2]").unwrap();
        assert!(hotkeys.1 < last_hotkey.1);
        assert_eq!(commands.1, last_hotkey.1 + 2);
        for x in commands.0..commands.0 + 50 {
            assert_eq!(buffer[(x, commands.1 - 1)].symbol(), " ");
        }
        for (command, _) in SHORTCUTS {
            let position = find_text(buffer, command).expect(command);
            assert_eq!(buffer[position].fg, state.theme.text_command());
        }
        assert!(find_text(buffer, "Transcript will appear here").is_none());
    }

    #[test]
    fn compact_welcome_keeps_commands_and_yields_to_session_content() {
        let mut state = MissionControlState::default();
        let mut terminal = Terminal::new(TestBackend::new(40, 23)).unwrap();
        terminal
            .draw(|frame| draw_transcript(frame, frame.area(), &state))
            .unwrap();
        let buffer = terminal.backend().buffer();
        for text in [
            "MAGI CODE",
            "/summarize-start",
            "/summarize-stop",
            "/update",
        ] {
            assert!(find_text(buffer, text).is_some(), "missing {text}");
        }
        state.transcript = vec!["A new conversation".to_string()].into();
        terminal
            .draw(|frame| draw_transcript(frame, frame.area(), &state))
            .unwrap();
        assert!(find_text(terminal.backend().buffer(), "MAGI CODE").is_none());
        assert!(find_text(terminal.backend().buffer(), "A new conversation").is_some());
    }

    #[test]
    fn welcome_animates_logo_and_background_without_changing_help() {
        let mut state = MissionControlState {
            color_enabled: true,
            reduced_motion: false,
            ..Default::default()
        };
        let mut terminal = Terminal::new(TestBackend::new(100, 34)).unwrap();
        terminal
            .draw(|frame| draw_welcome(frame, frame.area(), &state))
            .unwrap();
        let first = terminal.backend().buffer().clone();
        state.welcome_animation_phase = 12;
        terminal
            .draw(|frame| draw_welcome(frame, frame.area(), &state))
            .unwrap();
        let second = terminal.backend().buffer();
        let logo = find_text(&first, LOGO[0]).unwrap();
        assert_ne!(first[logo].fg, second[logo].fg);
        assert!((0..34).any(|y| first[(0, y)] != second[(0, y)]));
        let help = find_text(&first, "/skills").unwrap();
        assert_eq!(first[help], second[help]);
        for (reduced_motion, color_enabled) in [(true, true), (false, false)] {
            state.reduced_motion = reduced_motion;
            state.color_enabled = color_enabled;
            terminal
                .draw(|frame| draw_welcome(frame, frame.area(), &state))
                .unwrap();
            let still = terminal.backend().buffer().clone();
            state.welcome_animation_phase += 1;
            terminal
                .draw(|frame| draw_welcome(frame, frame.area(), &state))
                .unwrap();
            assert_eq!(&still, terminal.backend().buffer());
            assert!((0..34).all(|y| still[(0, y)].symbol() == " "));
        }
    }

    #[test]
    fn welcome_handles_tiny_and_empty_areas() {
        for (width, height) in [(0, 0), (1, 1), (8, 3)] {
            let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
            terminal
                .draw(|frame| draw_welcome(frame, frame.area(), &MissionControlState::default()))
                .unwrap();
        }
    }
}