Codex Handbook
tui/src/bottom_pane/memories_settings_view.rs 470 lines
use crossterm::event::KeyCode;use crossterm::event::KeyEvent;use crossterm::event::KeyModifiers;use ratatui::buffer::Buffer;use ratatui::layout::Constraint;use ratatui::layout::Layout;use ratatui::layout::Rect;use ratatui::style::Stylize;use ratatui::text::Line;use ratatui::widgets::Block;use ratatui::widgets::Widget;use crate::app_event::AppEvent;use crate::app_event_sender::AppEventSender;use crate::bottom_pane::popup_consts::standard_popup_hint_line;use crate::key_hint;use crate::key_hint::KeyBindingListExt;use crate::keymap::ListKeymap;use crate::render::Insets;use crate::render::RectExt as _;use crate::render::renderable::ColumnRenderable;use crate::render::renderable::Renderable;use crate::style::user_message_style;use super::CancellationEvent;use super::bottom_pane_view::BottomPaneView;use super::popup_consts::MAX_POPUP_ROWS;use super::scroll_state::ScrollState;use super::selection_popup_common::GenericDisplayRow;use super::selection_popup_common::measure_rows_height;use super::selection_popup_common::render_rows;const MEMORIES_DOC_URL: &str = "https://developers.openai.com/codex/memories";#[derive(Clone, Copy, PartialEq, Eq)]enum MemoriesSetting {    Use,    Generate,}#[derive(Clone, Copy, PartialEq, Eq)]enum MemoriesAction {    Reset,}enum MemoriesMenuItem {    Setting {        setting: MemoriesSetting,        name: &'static str,        description: &'static str,        enabled: bool,    },    Action {        action: MemoriesAction,        name: &'static str,        description: &'static str,    },}pub(crate) struct MemoriesSettingsView {    items: Vec<MemoriesMenuItem>,    state: ScrollState,    reset_confirmation: Option<ScrollState>,    complete: bool,    app_event_tx: AppEventSender,    docs_link: Line<'static>,    keymap: ListKeymap,}impl MemoriesSettingsView {    pub(crate) fn new(        use_memories: bool,        generate_memories: bool,        app_event_tx: AppEventSender,        keymap: ListKeymap,    ) -> Self {        let mut view = Self {            items: vec![                MemoriesMenuItem::Setting {                    setting: MemoriesSetting::Use,                    name: "Use memories",                    description: "Use memories in the following threads. Applied at next thread.",                    enabled: use_memories,                },                MemoriesMenuItem::Setting {                    setting: MemoriesSetting::Generate,                    name: "Generate memories",                    description: "Generate memories from the following threads. Current thread included.",                    enabled: generate_memories,                },                MemoriesMenuItem::Action {                    action: MemoriesAction::Reset,                    name: "Reset all memories",                    description: "Clear local memory files and summaries. Existing threads stay intact.",                },            ],            state: ScrollState::new(),            reset_confirmation: None,            complete: false,            app_event_tx,            docs_link: Line::from(vec![                "Learn more: ".dim(),                MEMORIES_DOC_URL.cyan().underlined(),            ]),            keymap,        };        view.initialize_selection();        view    }    fn initialize_selection(&mut self) {        self.state.selected_idx = (!self.items.is_empty()).then_some(0);    }    fn settings_header(&self) -> ColumnRenderable<'_> {        let mut header = ColumnRenderable::new();        header.push(Line::from("Memories".bold()));        header.push(Line::from(            "Choose how Codex uses and creates memories. Changes are saved to config.toml".dim(),        ));        header    }    fn reset_confirmation_header(&self) -> ColumnRenderable<'_> {        let mut header = ColumnRenderable::new();        header.push(Line::from("Reset all memories?".bold()));        header.push(Line::from(            "This clears local memory files and rollout summaries for the current Codex home."                .dim(),        ));        header    }    fn active_state(&self) -> &ScrollState {        self.reset_confirmation.as_ref().unwrap_or(&self.state)    }    fn active_state_mut(&mut self) -> &mut ScrollState {        self.reset_confirmation.as_mut().unwrap_or(&mut self.state)    }    fn visible_len(&self) -> usize {        if self.reset_confirmation.is_some() {            2        } else {            self.items.len()        }    }    fn build_rows(&self) -> Vec<GenericDisplayRow> {        if let Some(state) = self.reset_confirmation.as_ref() {            return ["Reset all memories", "Go back"]                .into_iter()                .enumerate()                .map(|(idx, name)| GenericDisplayRow {                    name: if state.selected_idx == Some(idx) {                        format!("› {name}")                    } else {                        format!("  {name}")                    },                    description: Some(match idx {                        0 => "Delete local memory files and rollout summaries.".to_string(),                        1 => "Return to memory settings.".to_string(),                        _ => unreachable!("reset confirmation only renders two rows"),                    }),                    ..Default::default()                })                .collect();        }        let selected_idx = self.state.selected_idx;        self.items            .iter()            .enumerate()            .map(|(idx, item)| {                let prefix = if selected_idx == Some(idx) {                    '›'                } else {                    ' '                };                let (name, description) = match item {                    MemoriesMenuItem::Setting {                        name,                        description,                        enabled,                        ..                    } => (                        format!("{prefix} [{}] {name}", if *enabled { 'x' } else { ' ' }),                        description,                    ),                    MemoriesMenuItem::Action {                        name, description, ..                    } => (format!("{prefix} {name}"), description),                };                GenericDisplayRow {                    name,                    description: Some((*description).to_string()),                    ..Default::default()                }            })            .collect()    }    fn move_up(&mut self) {        let len = self.visible_len();        if len == 0 {            return;        }        let state = self.active_state_mut();        state.move_up_wrap(len);        state.ensure_visible(len, MAX_POPUP_ROWS.min(len));    }    fn move_down(&mut self) {        let len = self.visible_len();        if len == 0 {            return;        }        let state = self.active_state_mut();        state.move_down_wrap(len);        state.ensure_visible(len, MAX_POPUP_ROWS.min(len));    }    fn page_up(&mut self) {        let len = self.visible_len();        let visible = MAX_POPUP_ROWS.min(len);        self.active_state_mut().page_up_clamped(len, visible);    }    fn page_down(&mut self) {        let len = self.visible_len();        let visible = MAX_POPUP_ROWS.min(len);        self.active_state_mut().page_down_clamped(len, visible);    }    fn jump_top(&mut self) {        let len = self.visible_len();        let visible = MAX_POPUP_ROWS.min(len);        self.active_state_mut().jump_top(len, visible);    }    fn jump_bottom(&mut self) {        let len = self.visible_len();        let visible = MAX_POPUP_ROWS.min(len);        self.active_state_mut().jump_bottom(len, visible);    }    fn toggle_selected(&mut self) {        if self.reset_confirmation.is_some() {            return;        }        let Some(selected_idx) = self.state.selected_idx else {            return;        };        if let Some(MemoriesMenuItem::Setting { enabled, .. }) = self.items.get_mut(selected_idx) {            *enabled = !*enabled;        }    }    fn rows_width(total_width: u16) -> u16 {        total_width.saturating_sub(2)    }    fn current_setting(&self, setting: MemoriesSetting) -> bool {        self.items            .iter()            .find_map(|item| match item {                MemoriesMenuItem::Setting {                    setting: item_setting,                    enabled,                    ..                } if *item_setting == setting => Some(*enabled),                _ => None,            })            .unwrap_or(false)    }    fn open_reset_confirmation(&mut self) {        let mut state = ScrollState::new();        state.selected_idx = Some(0);        self.reset_confirmation = Some(state);    }    fn close_reset_confirmation(&mut self) {        self.reset_confirmation = None;        self.state.selected_idx = self.items.len().checked_sub(1);    }    fn footer_hint(&self) -> Line<'static> {        if self.reset_confirmation.is_some() {            standard_popup_hint_line()        } else {            memories_settings_hint_line()        }    }}impl BottomPaneView for MemoriesSettingsView {    fn handle_key_event(&mut self, key_event: KeyEvent) {        match key_event {            _ if self.keymap.move_up.is_pressed(key_event) => self.move_up(),            _ if self.keymap.move_down.is_pressed(key_event) => self.move_down(),            _ if self.keymap.page_up.is_pressed(key_event) => self.page_up(),            _ if self.keymap.page_down.is_pressed(key_event) => self.page_down(),            _ if self.keymap.jump_top.is_pressed(key_event) => self.jump_top(),            _ if self.keymap.jump_bottom.is_pressed(key_event) => self.jump_bottom(),            KeyEvent {                code: KeyCode::Char(' '),                modifiers: KeyModifiers::NONE,                ..            } => self.toggle_selected(),            _ if self.keymap.accept.is_pressed(key_event) => self.save(),            _ if self.keymap.cancel.is_pressed(key_event) => self.cancel(),            _ => {}        }    }    fn is_complete(&self) -> bool {        self.complete    }    fn on_ctrl_c(&mut self) -> CancellationEvent {        self.cancel();        CancellationEvent::Handled    }}impl MemoriesSettingsView {    fn save(&mut self) {        if let Some(state) = self.reset_confirmation.as_ref() {            match state.selected_idx {                Some(0) => {                    self.app_event_tx.send(AppEvent::ResetMemories);                    self.complete = true;                }                Some(1) | None => self.close_reset_confirmation(),                Some(other) => unreachable!("unexpected reset confirmation row: {other}"),            }            return;        }        match self.state.selected_idx.and_then(|idx| self.items.get(idx)) {            Some(MemoriesMenuItem::Action {                action: MemoriesAction::Reset,                ..            }) => self.open_reset_confirmation(),            _ => {                self.app_event_tx.send(AppEvent::UpdateMemorySettings {                    use_memories: self.current_setting(MemoriesSetting::Use),                    generate_memories: self.current_setting(MemoriesSetting::Generate),                });                self.complete = true;            }        }    }    fn cancel(&mut self) {        if self.reset_confirmation.is_some() {            self.close_reset_confirmation();        } else {            self.complete = true;        }    }}impl Renderable for MemoriesSettingsView {    fn render(&self, area: Rect, buf: &mut Buffer) {        if area.height == 0 || area.width == 0 {            return;        }        let [content_area, footer_area] =            Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);        Block::default()            .style(user_message_style())            .render(content_area, buf);        let header = if self.reset_confirmation.is_some() {            self.reset_confirmation_header()        } else {            self.settings_header()        };        let header_height = header.desired_height(content_area.width.saturating_sub(4));        let rows = self.build_rows();        let rows_width = Self::rows_width(content_area.width);        let rows_height = measure_rows_height(            &rows,            self.active_state(),            MAX_POPUP_ROWS,            rows_width.saturating_add(1),        );        let [header_area, _, list_area, _, docs_area] = Layout::vertical([            Constraint::Max(header_height),            Constraint::Max(1),            Constraint::Length(rows_height),            Constraint::Max(1),            Constraint::Length(1),        ])        .areas(content_area.inset(Insets::vh(/*v*/ 1, /*h*/ 2)));        header.render(header_area, buf);        if list_area.height > 0 {            let render_area = Rect {                x: list_area.x.saturating_sub(2),                y: list_area.y,                width: rows_width.max(1),                height: list_area.height,            };            render_rows(                render_area,                buf,                &rows,                self.active_state(),                MAX_POPUP_ROWS,                "  No memory settings available",            );        }        if self.reset_confirmation.is_none() {            self.docs_link.clone().render(docs_area, buf);            crate::terminal_hyperlinks::mark_url_hyperlink(buf, docs_area, MEMORIES_DOC_URL);        }        let hint_area = Rect {            x: footer_area.x + 2,            y: footer_area.y,            width: footer_area.width.saturating_sub(2),            height: footer_area.height,        };        self.footer_hint().render(hint_area, buf);    }    fn desired_height(&self, width: u16) -> u16 {        let header = if self.reset_confirmation.is_some() {            self.reset_confirmation_header()        } else {            self.settings_header()        };        let rows = self.build_rows();        let rows_width = Self::rows_width(width);        let rows_height = measure_rows_height(            &rows,            self.active_state(),            MAX_POPUP_ROWS,            rows_width.saturating_add(1),        );        let docs_height = if self.reset_confirmation.is_some() {            0        } else {            1        };        let mut height = header.desired_height(width.saturating_sub(4));        height = height.saturating_add(rows_height + 4 + docs_height);        height.saturating_add(1)    }}fn memories_settings_hint_line() -> Line<'static> {    Line::from(vec![        "Press ".into(),        key_hint::plain(KeyCode::Char(' ')).into(),        " to toggle; ".into(),        key_hint::plain(KeyCode::Enter).into(),        " to save or select".into(),    ])}