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)
}
}