magi-code 0.96.1

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

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RewindModalState {
    pub(crate) content: String,
    pub(crate) plan: Option<crate::checkpoints::RestorePlan>,
    pub(crate) plans: Vec<crate::checkpoints::RestorePlan>,
    pub(crate) plan_previews: Vec<String>,
    pub(crate) selected: usize,
    pub(crate) dry_run: bool,
    pub(crate) mode: crate::checkpoints::RewindMode,
    pub(crate) show_mode_choices: bool,
}

impl RewindModalState {
    pub(crate) fn hotkeys(&self) -> String {
        let help = if self.plan.is_some() || !self.plans.is_empty() {
            "[Enter] apply rewind • [Up]/[Down] select/scroll"
        } else {
            "[Up]/[Down] scroll"
        };
        if self.show_mode_choices {
            format!("{help} • {}", crate::tui::input::REWIND_MODE_HELP)
        } else {
            help.to_owned()
        }
    }

    pub(crate) fn body_text(&self) -> String {
        if !self.show_mode_choices {
            return self.content.clone();
        }
        let effect = match self.mode {
            crate::checkpoints::RewindMode::Conversation => {
                "Fork before this prompt; restore prompt for editing. Files unchanged."
            }
            crate::checkpoints::RewindMode::Files => {
                "Restore tracked files from this prompt onward. Conversation unchanged."
            }
            crate::checkpoints::RewindMode::Both => {
                "Fork before this prompt and restore tracked files. Restore prompt for editing."
            }
        };
        let prefix = self
            .plans
            .get(self.selected)
            .map(|plan| {
                format!(
                    "Target {}/{}: turn {}\n\n",
                    self.selected + 1,
                    self.plans.len(),
                    plan.target_turn
                )
            })
            .unwrap_or_default();
        format!(
            "Modes: conversation only | files only | both\nSelected: {}\n{effect}\nOriginal conversation remains in the session picker.\n\n{prefix}{}",
            self.mode.label(),
            self.content
        )
    }
}

impl MissionControlState {
    pub(crate) fn open_rewind_modal(
        &mut self,
        content: String,
        plan: Option<crate::checkpoints::RestorePlan>,
        dry_run: bool,
    ) {
        self.modals.rewind_modal = Some(RewindModalState {
            show_mode_choices: plan.is_some(),
            content,
            plan,
            plans: Vec::new(),
            plan_previews: Vec::new(),
            selected: 0,
            dry_run,
            mode: crate::checkpoints::RewindMode::default(),
        });
        self.set_scroll_offset(&self.scroll_views.rewind_modal, 0);
        self.hide_autocomplete();
    }

    pub(crate) fn open_rewind_picker_with_previews(
        &mut self,
        plans: Vec<crate::checkpoints::RestorePlan>,
        plan_previews: Vec<String>,
    ) {
        let content = plan_previews
            .first()
            .cloned()
            .unwrap_or_else(|| "no rewindable file changes".to_string());
        self.modals.rewind_modal = Some(RewindModalState {
            show_mode_choices: true,
            content,
            plan: None,
            plans,
            plan_previews,
            selected: 0,
            dry_run: false,
            mode: crate::checkpoints::RewindMode::default(),
        });
        self.set_scroll_offset(&self.scroll_views.rewind_modal, 0);
        self.hide_autocomplete();
    }

    pub(crate) fn rewind_picker_up(&mut self) -> bool {
        let changed = {
            let Some(modal) = &mut self.modals.rewind_modal else {
                return false;
            };
            if modal.plans.is_empty() {
                return false;
            }
            let before = modal.selected;
            modal.selected = modal.selected.saturating_sub(1);
            if before != modal.selected
                && let Some(preview) = modal.plan_previews.get(modal.selected)
            {
                modal.content = preview.clone();
            }
            before != modal.selected
        };
        if changed {
            self.set_scroll_offset(&self.scroll_views.rewind_modal, 0);
        }
        changed
    }

    pub(crate) fn rewind_picker_down(&mut self) -> bool {
        let changed = {
            let Some(modal) = &mut self.modals.rewind_modal else {
                return false;
            };
            if modal.plans.is_empty() {
                return false;
            }
            let before = modal.selected;
            modal.selected = modal.selected.saturating_add(1).min(modal.plans.len() - 1);
            if before != modal.selected
                && let Some(preview) = modal.plan_previews.get(modal.selected)
            {
                modal.content = preview.clone();
            }
            before != modal.selected
        };
        if changed {
            self.set_scroll_offset(&self.scroll_views.rewind_modal, 0);
        }
        changed
    }

    pub(crate) fn close_rewind_modal(&mut self) -> bool {
        self.modals.rewind_modal.take().is_some()
    }

    pub(crate) fn take_rewind_plan(
        &mut self,
    ) -> Option<(
        crate::checkpoints::RestorePlan,
        crate::checkpoints::RewindMode,
    )> {
        self.modals.rewind_modal.take().and_then(|modal| {
            modal
                .plan
                .or_else(|| modal.plans.get(modal.selected).cloned())
                .map(|plan| (plan, modal.mode))
        })
    }

    pub(crate) fn scroll_rewind_up_by(
        &mut self,
        rows: u16,
        visible_rows: u16,
        wrap_width: u16,
    ) -> bool {
        let before = self.scroll_offset(&self.scroll_views.rewind_modal);
        let overflow = self.rewind_scroll_overflow(visible_rows, wrap_width);
        self.set_scroll_offset(
            &self.scroll_views.rewind_modal,
            before.saturating_sub(usize::from(rows)).min(overflow),
        );
        self.scroll_offset(&self.scroll_views.rewind_modal) != before
    }

    pub(crate) fn scroll_rewind_down_by(
        &mut self,
        rows: u16,
        visible_rows: u16,
        wrap_width: u16,
    ) -> bool {
        let before = self.scroll_offset(&self.scroll_views.rewind_modal);
        let overflow = self.rewind_scroll_overflow(visible_rows, wrap_width);
        self.set_scroll_offset(
            &self.scroll_views.rewind_modal,
            before.saturating_add(usize::from(rows)).min(overflow),
        );
        self.scroll_offset(&self.scroll_views.rewind_modal) != before
    }

    pub(crate) fn rewind_scroll_overflow(&self, visible_rows: u16, wrap_width: u16) -> usize {
        let Some(modal) = &self.modals.rewind_modal else {
            return 0;
        };
        transcript::ratatui_wrapped_visual_rows(&modal.body_text(), wrap_width.max(1))
            .saturating_sub(visible_rows.max(1) as usize)
    }
}