magi-code 0.96.1

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

// Shared by the fast-mode rainbow and welcome animation; neither owns another timer.
pub(crate) const FAST_MODE_RAINBOW_FRAME_INTERVAL: Duration = Duration::from_millis(83);

const SCROLL_COALESCE_MAX_EVENTS: usize = 128;
const SCROLL_COALESCE_MAX_DURATION: Duration = Duration::from_millis(2);

pub(crate) fn prompt_cursor_blink_due(
    state: &state::MissionControlState,
    last_blink: Instant,
) -> bool {
    state.is_prompt_focused()
        && state.prompt_is_empty()
        && last_blink.elapsed() >= PROMPT_CURSOR_BLINK_INTERVAL
}

pub(crate) fn next_ui_timer_timeout(
    state: &state::MissionControlState,
    last_blink: Instant,
    last_footer_git_branch_refresh: Instant,
    last_fast_mode_rainbow: Option<Instant>,
    now: Instant,
) -> Option<Duration> {
    let mut next_due = last_footer_git_branch_refresh + FOOTER_GIT_BRANCH_REFRESH_INTERVAL;
    if state.is_prompt_focused() && state.prompt_is_empty() {
        next_due = next_due.min(last_blink + PROMPT_CURSOR_BLINK_INTERVAL);
    }
    if let Some(toast) = &state.toast {
        next_due = next_due.min(toast.expires_at());
    }
    if state.fast_mode_rainbow_active() || state.welcome_animation_active() {
        let next_rainbow_due = last_fast_mode_rainbow
            .map(|last| last + FAST_MODE_RAINBOW_FRAME_INTERVAL)
            .unwrap_or_else(|| now + FAST_MODE_RAINBOW_FRAME_INTERVAL);
        next_due = next_due.min(next_rainbow_due);
    }
    if let Some(timeout) = state.activity_motion_timeout(now) {
        next_due = next_due.min(now + timeout);
    }
    if state.session_active_time.is_active() {
        next_due = next_due.min(now + Duration::from_secs(1));
    }
    Some(next_due.saturating_duration_since(now))
}

pub(crate) fn tick_fast_mode_rainbow(
    state: &mut state::MissionControlState,
    last_fast_mode_rainbow: &mut Option<Instant>,
    now: Instant,
) -> bool {
    if !state.fast_mode_rainbow_active() && !state.welcome_animation_active() {
        *last_fast_mode_rainbow = None;
        return false;
    }

    let Some(last_frame) = *last_fast_mode_rainbow else {
        *last_fast_mode_rainbow = Some(now);
        return false;
    };
    if now.saturating_duration_since(last_frame) < FAST_MODE_RAINBOW_FRAME_INTERVAL {
        return false;
    }

    // Re-anchor to the observed tick time so a delayed frame advances once,
    // rather than catching up with a burst of phase changes.
    *last_fast_mode_rainbow = Some(now);
    let rainbow_advanced = state.advance_fast_mode_rainbow_phase();
    let welcome_advanced = state.welcome_animation_active();
    if welcome_advanced {
        state.welcome_animation_phase = state.welcome_animation_phase.wrapping_add(1);
    }
    rainbow_advanced || welcome_advanced
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum RedrawIntent {
    #[default]
    None,
    ScrollOnly,
    Immediate,
}

impl RedrawIntent {
    pub(crate) fn request_immediate(&mut self) {
        *self = Self::Immediate;
    }

    pub(crate) fn request_scroll(&mut self) {
        if *self == Self::None {
            *self = Self::ScrollOnly;
        }
    }

    pub(crate) fn clear(&mut self) {
        *self = Self::None;
    }
}

pub(crate) fn should_draw_frame(intent: RedrawIntent) -> bool {
    matches!(intent, RedrawIntent::Immediate | RedrawIntent::ScrollOnly)
}

pub(crate) fn coalesce_mouse_scroll_event(
    first: crossterm::event::MouseEvent,
    terminal_area: ratatui::layout::Rect,
    state: &state::MissionControlState,
    bridged_events: &Receiver<TerminalInputEvent>,
    pending_events: &mut VecDeque<TerminalInputEvent>,
) -> (crossterm::event::MouseEvent, u16, usize) {
    let Some(first_batch) = mouse_scroll_batch(first, terminal_area, state) else {
        return (first, state::MOUSE_WHEEL_SCROLL_ROWS, 0);
    };
    let mut notches = 1u16;
    let mut coalesced_events: usize = 0;
    let started = Instant::now();
    while coalesced_events < SCROLL_COALESCE_MAX_EVENTS
        && started.elapsed() < SCROLL_COALESCE_MAX_DURATION
    {
        let Some(event) = pending_events
            .pop_front()
            .or_else(|| bridged_events.try_recv().ok())
        else {
            break;
        };
        let TerminalInputEvent::Event(crossterm::event::Event::Mouse(mouse)) = event else {
            pending_events.push_front(event);
            break;
        };
        let Some(next_batch) = mouse_scroll_batch(mouse, terminal_area, state) else {
            pending_events.push_front(TerminalInputEvent::Event(crossterm::event::Event::Mouse(
                mouse,
            )));
            break;
        };
        if !first_batch.coalesces_with(next_batch) {
            pending_events.push_front(TerminalInputEvent::Event(crossterm::event::Event::Mouse(
                mouse,
            )));
            break;
        }
        notches = notches.saturating_add(1);
        coalesced_events = coalesced_events.saturating_add(1);
    }
    (
        first,
        notches.saturating_mul(state::MOUSE_WHEEL_SCROLL_ROWS),
        coalesced_events,
    )
}

impl DrainResult {
    pub(crate) fn merge(&mut self, other: DrainResult) {
        let DrainResult {
            changed,
            run_finished,
            run_finished_worker_ids,
            model_catalog_finished,
            model_catalog_loaded,
            fast_mode_persisted,
            theme_catalog_loaded,
            theme_persisted,
            startup_critical_loaded,
            usage_loaded,
            startup_decorative_loaded,
            custom_provider_finished,
            session_preview_loaded,
            session_switch_loaded,
            session_title_updated,
            rewind_finished,
            export_finished,
            model_selection_finished,
            compaction_runtime_refresh,
            footer_git_branch_loaded,
        } = other;
        self.changed |= changed;
        self.run_finished |= run_finished;
        self.run_finished_worker_ids.extend(run_finished_worker_ids);
        self.model_catalog_finished |= model_catalog_finished;
        self.model_catalog_loaded.extend(model_catalog_loaded);
        self.fast_mode_persisted.extend(fast_mode_persisted);
        self.theme_catalog_loaded.extend(theme_catalog_loaded);
        self.theme_persisted.extend(theme_persisted);
        self.startup_critical_loaded.extend(startup_critical_loaded);
        self.usage_loaded.extend(usage_loaded);
        self.startup_decorative_loaded
            .extend(startup_decorative_loaded);
        self.custom_provider_finished
            .extend(custom_provider_finished);
        self.session_preview_loaded.extend(session_preview_loaded);
        self.session_switch_loaded.extend(session_switch_loaded);
        self.session_title_updated.extend(session_title_updated);
        self.rewind_finished.extend(rewind_finished);
        self.export_finished.extend(export_finished);
        self.model_selection_finished
            .extend(model_selection_finished);
        self.compaction_runtime_refresh
            .extend(compaction_runtime_refresh);
        self.footer_git_branch_loaded
            .extend(footer_git_branch_loaded);
    }
}

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

    fn mouse(
        kind: crossterm::event::MouseEventKind,
        column: u16,
        row: u16,
    ) -> crossterm::event::MouseEvent {
        crossterm::event::MouseEvent {
            kind,
            column,
            row,
            modifiers: crossterm::event::KeyModifiers::NONE,
        }
    }

    fn scroll_event(
        kind: crossterm::event::MouseEventKind,
        column: u16,
        row: u16,
    ) -> TerminalInputEvent {
        TerminalInputEvent::Event(crossterm::event::Event::Mouse(mouse(kind, column, row)))
    }

    #[test]
    #[ignore = "release-mode scroll burst measurement; run with --release --ignored --nocapture"]
    fn scroll_coalescing_burst_measurement() {
        let area = ratatui::layout::Rect::new(0, 0, 100, 30);
        let state = state::MissionControlState::default();
        for queued in [8usize, 128, 1_023] {
            let (sender, receiver) = bounded(1_024);
            for _ in 0..queued {
                sender
                    .send(scroll_event(
                        crossterm::event::MouseEventKind::ScrollUp,
                        1,
                        4,
                    ))
                    .unwrap();
            }
            let mut pending = VecDeque::new();
            let started = Instant::now();
            let (_, rows, coalesced) = coalesce_mouse_scroll_event(
                mouse(crossterm::event::MouseEventKind::ScrollUp, 1, 4),
                area,
                &state,
                &receiver,
                &mut pending,
            );
            eprintln!(
                "scroll_coalescing queued={} coalesced={} rows={} elapsed_us={} pending={}",
                queued,
                coalesced,
                rows,
                started.elapsed().as_micros(),
                pending.len()
            );
            assert!(coalesced <= queued.min(SCROLL_COALESCE_MAX_EVENTS));
            assert_eq!(receiver.len() + pending.len() + coalesced, queued);
        }
    }
}