feat: reset memories button (#17937)

<img width="720" height="175" alt="Screenshot 2026-04-15 at 14 35 02"
src="https://github.com/user-attachments/assets/041d73ff-8c16-42a9-8e92-c245805084f0"
/>
This commit is contained in:
jif-oai
2026-04-15 15:34:25 +01:00
committed by GitHub
parent ec13aaac89
commit da86cedbd4
7 changed files with 314 additions and 62 deletions

View File

@@ -12,6 +12,7 @@ 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::render::Insets;
use crate::render::RectExt as _;
@@ -35,21 +36,32 @@ enum MemoriesSetting {
Generate,
}
struct MemoriesSettingItem {
setting: MemoriesSetting,
name: &'static str,
description: &'static str,
enabled: bool,
#[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<MemoriesSettingItem>,
items: Vec<MemoriesMenuItem>,
state: ScrollState,
reset_confirmation: Option<ScrollState>,
complete: bool,
app_event_tx: AppEventSender,
header: Box<dyn Renderable>,
docs_link: Line<'static>,
footer_hint: Line<'static>,
}
impl MemoriesSettingsView {
@@ -58,36 +70,34 @@ impl MemoriesSettingsView {
generate_memories: bool,
app_event_tx: AppEventSender,
) -> Self {
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(),
));
let mut view = Self {
items: vec![
MemoriesSettingItem {
MemoriesMenuItem::Setting {
setting: MemoriesSetting::Use,
name: "Use memories",
description: "Use memories in the following threads. Applied at next thread.",
enabled: use_memories,
},
MemoriesSettingItem {
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,
header: Box::new(header),
docs_link: Line::from(vec![
"Learn more: ".dim(),
MEMORIES_DOC_URL.cyan().underlined(),
]),
footer_hint: memories_settings_hint_line(),
};
view.initialize_selection();
view
@@ -97,29 +107,93 @@ impl MemoriesSettingsView {
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 {
self.items.len()
if self.reset_confirmation.is_some() {
2
} else {
self.items.len()
}
}
fn build_rows(&self) -> Vec<GenericDisplayRow> {
let mut rows = Vec::with_capacity(self.items.len());
let selected_idx = self.state.selected_idx;
for (idx, item) in self.items.iter().enumerate() {
let prefix = if selected_idx == Some(idx) {
''
} else {
' '
};
let marker = if item.enabled { 'x' } else { ' ' };
let name = format!("{prefix} [{marker}] {}", item.name);
rows.push(GenericDisplayRow {
name,
description: Some(item.description.to_string()),
..Default::default()
});
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();
}
rows
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) {
@@ -127,8 +201,9 @@ impl MemoriesSettingsView {
if len == 0 {
return;
}
self.state.move_up_wrap(len);
self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
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) {
@@ -136,17 +211,22 @@ impl MemoriesSettingsView {
if len == 0 {
return;
}
self.state.move_down_wrap(len);
self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
let state = self.active_state_mut();
state.move_down_wrap(len);
state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
}
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(item) = self.items.get_mut(selected_idx) {
item.enabled = !item.enabled;
if let Some(MemoriesMenuItem::Setting { enabled, .. }) = self.items.get_mut(selected_idx) {
*enabled = !*enabled;
}
}
@@ -157,8 +237,34 @@ impl MemoriesSettingsView {
fn current_setting(&self, setting: MemoriesSetting) -> bool {
self.items
.iter()
.find(|item| item.setting == setting)
.is_some_and(|item| item.enabled)
.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()
}
}
}
@@ -231,15 +337,39 @@ impl BottomPaneView for MemoriesSettingsView {
impl MemoriesSettingsView {
fn save(&mut self) {
self.app_event_tx.send(AppEvent::UpdateMemorySettings {
use_memories: self.current_setting(MemoriesSetting::Use),
generate_memories: self.current_setting(MemoriesSetting::Generate),
});
self.complete = true;
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) {
self.complete = true;
if self.reset_confirmation.is_some() {
self.close_reset_confirmation();
} else {
self.complete = true;
}
}
}
@@ -256,14 +386,17 @@ impl Renderable for MemoriesSettingsView {
.style(user_message_style())
.render(content_area, buf);
let header_height = self
.header
.desired_height(content_area.width.saturating_sub(4));
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.state,
self.active_state(),
MAX_POPUP_ROWS,
rows_width.saturating_add(1),
);
@@ -276,7 +409,7 @@ impl Renderable for MemoriesSettingsView {
])
.areas(content_area.inset(Insets::vh(/*v*/ 1, /*h*/ 2)));
self.header.render(header_area, buf);
header.render(header_area, buf);
if list_area.height > 0 {
let render_area = Rect {
@@ -289,12 +422,14 @@ impl Renderable for MemoriesSettingsView {
render_area,
buf,
&rows,
&self.state,
self.active_state(),
MAX_POPUP_ROWS,
" No memory settings available",
);
}
self.docs_link.clone().render(docs_area, buf);
if self.reset_confirmation.is_none() {
self.docs_link.clone().render(docs_area, buf);
}
let hint_area = Rect {
x: footer_area.x + 2,
@@ -302,21 +437,31 @@ impl Renderable for MemoriesSettingsView {
width: footer_area.width.saturating_sub(2),
height: footer_area.height,
};
self.footer_hint.clone().dim().render(hint_area, buf);
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.state,
self.active_state(),
MAX_POPUP_ROWS,
rows_width.saturating_add(1),
);
let mut height = self.header.desired_height(width.saturating_sub(4));
height = height.saturating_add(rows_height + 5);
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)
}
}
@@ -327,6 +472,6 @@ fn memories_settings_hint_line() -> Line<'static> {
key_hint::plain(KeyCode::Char(' ')).into(),
" to toggle; ".into(),
key_hint::plain(KeyCode::Enter).into(),
" to save".into(),
" to save or select".into(),
])
}