mirror of
https://github.com/openai/codex.git
synced 2026-05-05 22:01:37 +03:00
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:
@@ -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(),
|
||||
])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user