magi-code 0.96.1

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

#[derive(Debug, Default)]
pub(super) struct DrainLoopBudgetState {
    draw_before_next_drain: bool,
    pub(super) continue_after_pending_draw: bool,
    input_paint_barrier: bool,
    #[cfg(any(test, debug_assertions))]
    pub(super) input_paint_started: Option<Instant>,
}

impl DrainLoopBudgetState {
    pub(super) fn should_drain_at_loop_top(&self) -> bool {
        !self.draw_before_next_drain
    }

    pub(super) fn defer_animation_draw_for_queued_input(
        &mut self,
        animation_only: bool,
        input_queued: bool,
        startup_paint_pending: bool,
    ) -> bool {
        if !animation_only
            || !input_queued
            || startup_paint_pending
            || self.draw_before_next_drain
            || self.input_paint_barrier
        {
            return false;
        }
        // Reuse the next-loop paint barrier without recording input latency:
        // even an ignored event must paint before another drain or input turn.
        self.input_paint_barrier = true;
        true
    }

    pub(super) fn request_input_paint(
        &mut self,
        redraw_intent: &mut RedrawIntent,
        perf_enabled: bool,
        input_received_at: Instant,
    ) {
        self.input_paint_barrier = true;
        #[cfg(any(test, debug_assertions))]
        if perf_enabled {
            let earliest = self
                .input_paint_started
                .map_or(input_received_at, |started| started.min(input_received_at));
            self.input_paint_started = Some(earliest);
        }
        #[cfg(not(any(test, debug_assertions)))]
        let _ = (perf_enabled, input_received_at);
        redraw_intent.request_immediate();
    }

    pub(super) fn input_paint_barrier_pending(&self) -> bool {
        self.input_paint_barrier
    }

    #[cfg(any(test, debug_assertions))]
    pub(super) fn take_input_paint_started(&mut self) -> Option<Instant> {
        self.input_paint_started.take()
    }

    pub(super) fn apply_outcome(&mut self, outcome: &DrainOutcome) -> bool {
        let changed = outcome.result.changed;
        if outcome.exhausted && changed {
            self.draw_before_next_drain = true;
            self.continue_after_pending_draw = true;
        }
        changed
    }

    pub(super) fn after_draw(&mut self) -> bool {
        self.draw_before_next_drain = false;
        self.input_paint_barrier = false;
        #[cfg(any(test, debug_assertions))]
        {
            self.input_paint_started = None;
        }
        std::mem::take(&mut self.continue_after_pending_draw)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PostDrainFlow {
    Continue,
    Exit,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct PostDrainProcessing {
    pub(super) flow: PostDrainFlow,
    pub(super) redraw_requested: bool,
    pub(super) skip_loop: bool,
}

pub(super) struct PostDrainContext<'a> {
    pub(super) receiver: &'a Receiver<TuiEvent>,
    pub(super) input_events: &'a TerminalInputBridge,
    pub(super) terminal_area: ratatui::layout::Rect,
    pub(super) pending_input_events: &'a mut VecDeque<TerminalInputEvent>,
    pub(super) drain_budget_state: &'a mut DrainLoopBudgetState,
    pub(super) finish_completed_worker: bool,
}

impl MissionControlApp {
    pub(super) fn process_post_drain(
        &mut self,
        ui_state: &mut state::MissionControlState,
        drain_outcome: &mut DrainOutcome,
        autocomplete_candidates: &mut Vec<state::AutocompleteCandidate>,
        context: PostDrainContext<'_>,
    ) -> Result<PostDrainProcessing> {
        let mut redraw_requested = false;
        redraw_requested |= self.reap_session_maintenance(ui_state);
        redraw_requested |= self.reap_auth_worker(ui_state);
        redraw_requested |= self.reconcile_system_prompt_worker(ui_state);
        redraw_requested |= self.reap_panel_layout_persistence(ui_state);
        if self.reap_settings_persistence_worker(ui_state) {
            refresh_skill_autocomplete_candidates(autocomplete_candidates, &self.skills);
            if self.settings_persistence.take_runtime_refresh_requested() {
                refresh_file_autocomplete_candidates(
                    autocomplete_candidates,
                    &self.state.cwd,
                    self.config.file_autocomplete_respects_gitignore,
                );
                ui_state.recompute_autocomplete(autocomplete_candidates);
            }
            redraw_requested = true;
        }
        if self.reap_rewind_workers(ui_state) {
            redraw_requested = true;
        }
        if self.handle_rewind_drain(ui_state, &drain_outcome.result) {
            redraw_requested = true;
        }
        if self.reap_export_workers(ui_state) {
            redraw_requested = true;
        }
        if self.handle_export_drain(ui_state, &drain_outcome.result) {
            redraw_requested = true;
        }
        if self.reap_theme_workers(ui_state) {
            redraw_requested = true;
        }
        if self.reap_fast_mode_persistence_workers(ui_state) {
            redraw_requested = true;
        }
        if self.handle_fast_mode_persistence_drain(ui_state, &drain_outcome.result) {
            redraw_requested = true;
        }
        if self.reap_model_selection_workers(ui_state) {
            redraw_requested = true;
        }
        if self.handle_model_selection_drain(ui_state, &drain_outcome.result) {
            redraw_requested = true;
        }
        for refresh in std::mem::take(&mut drain_outcome.result.compaction_runtime_refresh) {
            if self.apply_compaction_runtime_refresh(ui_state, refresh) {
                redraw_requested = true;
            }
        }
        if self.handle_model_catalog_drain(ui_state, &drain_outcome.result) {
            redraw_requested = true;
        }
        for (request_id, result) in std::mem::take(&mut drain_outcome.result.theme_catalog_loaded) {
            if self.handle_theme_catalog(ui_state, request_id, result) {
                redraw_requested = true;
            }
        }
        for (request_id, id, revision, result) in
            std::mem::take(&mut drain_outcome.result.theme_persisted)
        {
            if self.handle_theme_persisted(ui_state, request_id, id, revision, result) {
                redraw_requested = true;
            }
        }

        for (provider_id, provider_result) in
            std::mem::take(&mut drain_outcome.result.custom_provider_finished)
        {
            self.reconcile_worker_final_event_effect(
                ui_state,
                WorkerFinalEventEffect::CustomProviderFinished {
                    provider_id,
                    result: provider_result,
                },
            );
            redraw_requested = true;
        }

        // The control receiver is intentionally handled before terminal input
        // in the biased select below. When critical readiness and input meet,
        // capture only the events accepted at this boundary and keep the
        // readiness result deferred until that finite prefix has gone through
        // Loading policy. Later input does not extend the boundary, so a
        // replenished input queue cannot starve readiness.
        let critical_result_available = self.startup_readiness == StartupReadiness::Loading
            && (!drain_outcome.result.startup_critical_loaded.is_empty()
                || !self.deferred_startup_critical_loaded.is_empty());
        if critical_result_available && self.startup_input_boundary.is_none() {
            self.startup_input_boundary = Some(StartupInputBoundary::capture(
                context.pending_input_events.len(),
                context.input_events.receiver.len(),
            ));
        }
        let startup_input_boundary_open = self
            .startup_input_boundary
            .is_some_and(|boundary| !boundary.is_complete());
        if startup_input_boundary_open {
            self.deferred_startup_critical_loaded
                .append(&mut drain_outcome.result.startup_critical_loaded);
        } else if !self.deferred_startup_critical_loaded.is_empty() {
            let deferred = std::mem::take(&mut self.deferred_startup_critical_loaded);
            drain_outcome
                .result
                .startup_critical_loaded
                .splice(0..0, deferred);
        }

        // Classify an untouched --prompt while the app is still Loading. This
        // deliberately happens before applying a readiness result, so a
        // normal prompt follows the same queue path as typed input and slash
        // commands cannot become deferred startup work.
        self.request_initial_prompt_flush(context.input_events)?;
        match self.submit_initial_prompt_if_ready(
            ui_state,
            context.receiver,
            context.terminal_area,
            &context.input_events.receiver,
            context.pending_input_events,
        )? {
            Some(actions::AppControlFlow::Exit) => {
                return Ok(PostDrainProcessing {
                    flow: PostDrainFlow::Exit,
                    redraw_requested,
                    skip_loop: false,
                });
            }
            Some(actions::AppControlFlow::Continue) => redraw_requested = true,
            None => {}
        }
        if let Some(error) = self.startup_failure.clone() {
            return Err(anyhow::anyhow!(error));
        }

        if self.handle_startup_drain(ui_state, &mut drain_outcome.result, autocomplete_candidates) {
            redraw_requested = true;
        }
        if self.startup_readiness != StartupReadiness::Loading {
            self.startup_input_boundary = None;
        }
        self.check_startup_worker_health(ui_state);
        if let Some(error) = self.startup_failure.clone() {
            return Err(anyhow::anyhow!(error));
        }
        if self.handle_background_session_drain(ui_state, &drain_outcome.result, context.receiver) {
            redraw_requested = true;
        }
        redraw_requested |= self.poll_summarizer(ui_state);

        // A queued normal prompt may become launchable after the critical
        // readiness result above. It still needs the painted frame and input
        // fence checks in submit_initial_prompt_if_ready.
        self.request_initial_prompt_flush(context.input_events)?;
        match self.submit_initial_prompt_if_ready(
            ui_state,
            context.receiver,
            context.terminal_area,
            &context.input_events.receiver,
            context.pending_input_events,
        )? {
            Some(actions::AppControlFlow::Exit) => {
                return Ok(PostDrainProcessing {
                    flow: PostDrainFlow::Exit,
                    redraw_requested,
                    skip_loop: false,
                });
            }
            Some(actions::AppControlFlow::Continue) => redraw_requested = true,
            None => {}
        }
        if let Some(error) = self.startup_failure.clone() {
            return Err(anyhow::anyhow!(error));
        }
        if self.sync_steering_feedback(ui_state) {
            redraw_requested = true;
        }
        let run_finished_worker_ids = drain_outcome.result.run_finished_worker_ids.clone();
        let model_catalog_finished = drain_outcome.result.model_catalog_finished;
        for worker_id in run_finished_worker_ids {
            if self
                .worker
                .as_ref()
                .is_some_and(|worker| worker.outcome.worker_id() == worker_id)
            {
                self.active_run = false;
                if self.join_completed_worker(ui_state) {
                    redraw_requested = true;
                }
            }
        }
        if context.finish_completed_worker
            && self
                .worker
                .as_ref()
                .is_some_and(|worker| worker.handle.is_finished())
            && self.finish_worker_if_ready(ui_state)
        {
            redraw_requested = true;
        }
        if context.drain_budget_state.apply_outcome(drain_outcome) {
            redraw_requested = true;
        }
        if model_catalog_finished {
            self.model_catalog_loading = false;
        }
        let skip_loop = drain_outcome.exhausted
            && !drain_outcome.result.changed
            && context.pending_input_events.is_empty();
        Ok(PostDrainProcessing {
            flow: PostDrainFlow::Continue,
            redraw_requested,
            skip_loop,
        })
    }
}