magi-code 0.96.1

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

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PreviewMessage {
    pub(crate) role: &'static str,
    pub(crate) text: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SessionPreview {
    pub(crate) first: PreviewMessage,
    pub(crate) most_recent: Option<PreviewMessage>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SessionPreviewPartial {
    pub(crate) first: Option<PreviewMessage>,
    pub(crate) most_recent: Option<PreviewMessage>,
    pub(crate) reason: &'static str,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SessionPreviewState {
    Ready(SessionPreview),
    Empty,
    Partial(SessionPreviewPartial),
    Error(String),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SessionPickerFocus {
    List,
    Preview,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SessionPickerRow {
    pub(crate) id: String,
    pub(crate) label: String,
    pub(crate) is_current: bool,
    pub(crate) preview: Option<SessionPreviewState>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SessionPickerState {
    pub(crate) rows: Vec<SessionPickerRow>,
    pub(crate) selected: usize,
    pub(crate) focus: SessionPickerFocus,
}
impl SessionPickerState {
    pub(in crate::tui::state) fn clamp(&mut self) {
        let item_count = self.rows.len();
        clamp_picker_selection(&mut self.selected, item_count);
    }
}

fn format_session_preview(preview: Option<&SessionPreviewState>) -> String {
    match preview {
        Some(SessionPreviewState::Ready(preview)) => format_ready_session_preview(preview),
        Some(SessionPreviewState::Empty) => "No previewable conversational messages".to_string(),
        Some(SessionPreviewState::Partial(partial)) => {
            let mut text = format!("Preview limited: {}", partial.reason);
            if let Some(first) = &partial.first {
                text.push_str("\n\nFirst message\n");
                text.push_str(&format_preview_message(first));
            }
            if let Some(recent) = &partial.most_recent {
                text.push_str("\n\nMost recent message\n");
                text.push_str(&format_preview_message(recent));
            }
            text
        }
        Some(SessionPreviewState::Error(error)) => format!("Could not read preview: {error}"),
        None => "Preview not loaded yet".to_string(),
    }
}

fn format_ready_session_preview(preview: &SessionPreview) -> String {
    match &preview.most_recent {
        Some(recent) => format!(
            "First message\n{}\n\nMost recent message\n{}",
            format_preview_message(&preview.first),
            format_preview_message(recent)
        ),
        None => format!(
            "First / most recent message\n{}",
            format_preview_message(&preview.first)
        ),
    }
}

fn format_preview_message(message: &PreviewMessage) -> String {
    format!("{}: {}", message.role, message.text)
}

impl MissionControlState {
    pub(crate) fn open_session_picker(
        &mut self,
        rows: Vec<SessionPickerRow>,
        _visible_rows: usize,
    ) {
        let mut picker = SessionPickerState {
            rows,
            selected: 0,
            focus: SessionPickerFocus::List,
        };
        picker.clamp();
        self.modals.session_picker = Some(picker);
        self.set_scroll_offset(&self.scroll_views.session_list, 0);
        self.set_scroll_offset(&self.scroll_views.session_preview, 0);
        self.hide_autocomplete();
    }

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

    pub(crate) fn session_picker_visible(&self) -> bool {
        self.modals.session_picker.is_some()
    }

    pub(crate) fn session_picker_up(&mut self, visible_rows: usize) -> bool {
        self.move_session_picker(1, false, visible_rows)
    }

    pub(crate) fn session_picker_down(&mut self, visible_rows: usize) -> bool {
        self.move_session_picker(1, true, visible_rows)
    }

    pub(crate) fn session_picker_page_up(&mut self, visible_rows: usize) -> bool {
        let rows = visible_rows.max(1);
        self.move_session_picker(rows, false, rows)
    }

    pub(crate) fn session_picker_page_down(&mut self, visible_rows: usize) -> bool {
        let rows = visible_rows.max(1);
        self.move_session_picker(rows, true, rows)
    }

    pub(super) fn move_session_picker(
        &mut self,
        amount: usize,
        down: bool,
        visible_rows: usize,
    ) -> bool {
        let before_offset = self.scroll_offset(&self.scroll_views.session_list);
        let (before_selected, selected, offset) = {
            let Some(picker) = self.modals.session_picker.as_mut() else {
                return false;
            };
            let before_selected = picker.selected;
            let (selected, offset) = picker_selection_after_move(
                picker.selected,
                picker.rows.len(),
                amount,
                down,
                visible_rows,
                before_offset,
            );
            picker.selected = selected;
            (before_selected, selected, offset)
        };
        self.set_scroll_offset(&self.scroll_views.session_list, offset);
        let changed = before_selected != selected
            || before_offset != self.scroll_offset(&self.scroll_views.session_list);
        if changed && before_selected != selected {
            self.set_scroll_offset(&self.scroll_views.session_preview, 0);
        }
        changed
    }

    pub(crate) fn session_picker_focus_next(&mut self) -> bool {
        let Some(picker) = &mut self.modals.session_picker else {
            return false;
        };
        picker.focus = match picker.focus {
            SessionPickerFocus::List => SessionPickerFocus::Preview,
            SessionPickerFocus::Preview => SessionPickerFocus::List,
        };
        true
    }

    pub(crate) fn session_picker_focus_previous(&mut self) -> bool {
        self.session_picker_focus_next()
    }

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

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

    pub(crate) fn session_picker_preview_scroll_overflow(
        &self,
        visible_rows: u16,
        wrap_width: u16,
    ) -> usize {
        transcript::ratatui_wrapped_visual_rows(
            &self.session_picker_preview_text(),
            wrap_width.max(1),
        )
        .saturating_sub(visible_rows.max(1) as usize)
    }

    pub(crate) fn session_picker_preview_text(&self) -> String {
        let Some(picker) = &self.modals.session_picker else {
            return String::new();
        };
        let Some(row) = picker.rows.get(picker.selected) else {
            return "No previewable conversational messages".to_string();
        };
        format_session_preview(row.preview.as_ref())
    }

    pub(crate) fn set_session_preview(
        &mut self,
        session_id: &str,
        preview: SessionPreviewState,
    ) -> bool {
        let Some(picker) = &mut self.modals.session_picker else {
            return false;
        };
        let Some(row) = picker.rows.iter_mut().find(|row| row.id == session_id) else {
            return false;
        };
        let changed = row.preview.as_ref() != Some(&preview);
        row.preview = Some(preview);
        if changed {
            self.set_scroll_offset(&self.scroll_views.session_preview, 0);
        }
        changed
    }

    pub(crate) fn selected_session_preview_needs_load(&self) -> Option<String> {
        self.modals
            .session_picker
            .as_ref()
            .and_then(|picker| picker.rows.get(picker.selected))
            .filter(|row| row.preview.is_none())
            .map(|row| row.id.clone())
    }

    pub(crate) fn session_picker_select_visible_row(
        &mut self,
        visible_index: usize,
        _visible_rows: usize,
    ) -> bool {
        let target = self
            .scroll_offset(&self.scroll_views.session_list)
            .saturating_add(visible_index);
        let Some(picker) = self.modals.session_picker.as_mut() else {
            return false;
        };
        if target >= picker.rows.len() {
            return false;
        }
        let before_selected = picker.selected;
        picker.selected = target;
        picker.clamp();
        let selected = picker.selected;
        if before_selected != selected {
            self.set_scroll_offset(&self.scroll_views.session_preview, 0);
        }
        before_selected != selected
    }
    pub(crate) fn selected_session_id(&self) -> Option<String> {
        self.modals
            .session_picker
            .as_ref()
            .and_then(|picker| picker.rows.get(picker.selected))
            .map(|row| row.id.clone())
    }
}