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