mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08: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:
committed by
GitHub
Unverified
parent
ec13aaac89
commit
da86cedbd4
@@ -1561,6 +1561,18 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn reset_memories_with_app_server(&mut self, app_server: &mut AppServerSession) {
|
||||||
|
if let Err(err) = app_server.memory_reset().await {
|
||||||
|
tracing::error!(error = %err, "failed to reset memories");
|
||||||
|
self.chat_widget
|
||||||
|
.add_error_message(format!("Failed to reset memories: {err}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.chat_widget
|
||||||
|
.add_info_message("Reset local memories.".to_string(), /*hint*/ None);
|
||||||
|
}
|
||||||
|
|
||||||
fn open_url_in_browser(&mut self, url: String) {
|
fn open_url_in_browser(&mut self, url: String) {
|
||||||
if let Err(err) = webbrowser::open(&url) {
|
if let Err(err) = webbrowser::open(&url) {
|
||||||
self.chat_widget
|
self.chat_widget
|
||||||
@@ -5413,6 +5425,9 @@ impl App {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
AppEvent::ResetMemories => {
|
||||||
|
self.reset_memories_with_app_server(app_server).await;
|
||||||
|
}
|
||||||
AppEvent::SkipNextWorldWritableScan => {
|
AppEvent::SkipNextWorldWritableScan => {
|
||||||
self.windows_sandbox.skip_world_writable_scan_once = true;
|
self.windows_sandbox.skip_world_writable_scan_once = true;
|
||||||
}
|
}
|
||||||
@@ -8121,6 +8136,35 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reset_memories_clears_local_memory_directories() -> Result<()> {
|
||||||
|
let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||||
|
let codex_home = tempdir()?;
|
||||||
|
app.config.codex_home = codex_home.path().to_path_buf().abs();
|
||||||
|
app.config.sqlite_home = codex_home.path().to_path_buf();
|
||||||
|
|
||||||
|
let memory_root = codex_home.path().join("memories");
|
||||||
|
let extensions_root = codex_home.path().join("memories_extensions");
|
||||||
|
std::fs::create_dir_all(memory_root.join("rollout_summaries"))?;
|
||||||
|
std::fs::create_dir_all(&extensions_root)?;
|
||||||
|
std::fs::write(memory_root.join("MEMORY.md"), "stale memory\n")?;
|
||||||
|
std::fs::write(
|
||||||
|
memory_root.join("rollout_summaries").join("stale.md"),
|
||||||
|
"stale summary\n",
|
||||||
|
)?;
|
||||||
|
std::fs::write(extensions_root.join("stale.txt"), "stale extension\n")?;
|
||||||
|
|
||||||
|
let mut app_server = crate::start_embedded_app_server_for_picker(&app.config).await?;
|
||||||
|
|
||||||
|
app.reset_memories_with_app_server(&mut app_server).await;
|
||||||
|
|
||||||
|
assert_eq!(std::fs::read_dir(&memory_root)?.count(), 0);
|
||||||
|
assert_eq!(std::fs::read_dir(&extensions_root)?.count(), 0);
|
||||||
|
|
||||||
|
app_server.shutdown().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn update_feature_flags_enabling_guardian_selects_guardian_approvals() -> Result<()> {
|
async fn update_feature_flags_enabling_guardian_selects_guardian_approvals() -> Result<()> {
|
||||||
let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await;
|
let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await;
|
||||||
|
|||||||
@@ -470,6 +470,9 @@ pub(crate) enum AppEvent {
|
|||||||
generate_memories: bool,
|
generate_memories: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Clear all persisted local memory artifacts via the app-server.
|
||||||
|
ResetMemories,
|
||||||
|
|
||||||
/// Update whether the full access warning prompt has been acknowledged.
|
/// Update whether the full access warning prompt has been acknowledged.
|
||||||
UpdateFullAccessWarningAcknowledged(bool),
|
UpdateFullAccessWarningAcknowledged(bool),
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use codex_app_server_protocol::GetAccountParams;
|
|||||||
use codex_app_server_protocol::GetAccountRateLimitsResponse;
|
use codex_app_server_protocol::GetAccountRateLimitsResponse;
|
||||||
use codex_app_server_protocol::GetAccountResponse;
|
use codex_app_server_protocol::GetAccountResponse;
|
||||||
use codex_app_server_protocol::JSONRPCErrorError;
|
use codex_app_server_protocol::JSONRPCErrorError;
|
||||||
|
use codex_app_server_protocol::MemoryResetResponse;
|
||||||
use codex_app_server_protocol::Model as ApiModel;
|
use codex_app_server_protocol::Model as ApiModel;
|
||||||
use codex_app_server_protocol::ModelListParams;
|
use codex_app_server_protocol::ModelListParams;
|
||||||
use codex_app_server_protocol::ModelListResponse;
|
use codex_app_server_protocol::ModelListResponse;
|
||||||
@@ -535,6 +536,19 @@ impl AppServerSession {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn memory_reset(&mut self) -> Result<()> {
|
||||||
|
let request_id = self.next_request_id();
|
||||||
|
let _: MemoryResetResponse = self
|
||||||
|
.client
|
||||||
|
.request_typed(ClientRequest::MemoryReset {
|
||||||
|
request_id,
|
||||||
|
params: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.wrap_err("memory/reset failed in TUI")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn thread_unsubscribe(&mut self, thread_id: ThreadId) -> Result<()> {
|
pub(crate) async fn thread_unsubscribe(&mut self, thread_id: ThreadId) -> Result<()> {
|
||||||
let request_id = self.next_request_id();
|
let request_id = self.next_request_id();
|
||||||
let _: ThreadUnsubscribeResponse = self
|
let _: ThreadUnsubscribeResponse = self
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use ratatui::widgets::Widget;
|
|||||||
|
|
||||||
use crate::app_event::AppEvent;
|
use crate::app_event::AppEvent;
|
||||||
use crate::app_event_sender::AppEventSender;
|
use crate::app_event_sender::AppEventSender;
|
||||||
|
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
|
||||||
use crate::key_hint;
|
use crate::key_hint;
|
||||||
use crate::render::Insets;
|
use crate::render::Insets;
|
||||||
use crate::render::RectExt as _;
|
use crate::render::RectExt as _;
|
||||||
@@ -35,21 +36,32 @@ enum MemoriesSetting {
|
|||||||
Generate,
|
Generate,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MemoriesSettingItem {
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
setting: MemoriesSetting,
|
enum MemoriesAction {
|
||||||
name: &'static str,
|
Reset,
|
||||||
description: &'static str,
|
}
|
||||||
enabled: bool,
|
|
||||||
|
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 {
|
pub(crate) struct MemoriesSettingsView {
|
||||||
items: Vec<MemoriesSettingItem>,
|
items: Vec<MemoriesMenuItem>,
|
||||||
state: ScrollState,
|
state: ScrollState,
|
||||||
|
reset_confirmation: Option<ScrollState>,
|
||||||
complete: bool,
|
complete: bool,
|
||||||
app_event_tx: AppEventSender,
|
app_event_tx: AppEventSender,
|
||||||
header: Box<dyn Renderable>,
|
|
||||||
docs_link: Line<'static>,
|
docs_link: Line<'static>,
|
||||||
footer_hint: Line<'static>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MemoriesSettingsView {
|
impl MemoriesSettingsView {
|
||||||
@@ -58,36 +70,34 @@ impl MemoriesSettingsView {
|
|||||||
generate_memories: bool,
|
generate_memories: bool,
|
||||||
app_event_tx: AppEventSender,
|
app_event_tx: AppEventSender,
|
||||||
) -> Self {
|
) -> 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 {
|
let mut view = Self {
|
||||||
items: vec![
|
items: vec![
|
||||||
MemoriesSettingItem {
|
MemoriesMenuItem::Setting {
|
||||||
setting: MemoriesSetting::Use,
|
setting: MemoriesSetting::Use,
|
||||||
name: "Use memories",
|
name: "Use memories",
|
||||||
description: "Use memories in the following threads. Applied at next thread.",
|
description: "Use memories in the following threads. Applied at next thread.",
|
||||||
enabled: use_memories,
|
enabled: use_memories,
|
||||||
},
|
},
|
||||||
MemoriesSettingItem {
|
MemoriesMenuItem::Setting {
|
||||||
setting: MemoriesSetting::Generate,
|
setting: MemoriesSetting::Generate,
|
||||||
name: "Generate memories",
|
name: "Generate memories",
|
||||||
description: "Generate memories from the following threads. Current thread included.",
|
description: "Generate memories from the following threads. Current thread included.",
|
||||||
enabled: generate_memories,
|
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(),
|
state: ScrollState::new(),
|
||||||
|
reset_confirmation: None,
|
||||||
complete: false,
|
complete: false,
|
||||||
app_event_tx,
|
app_event_tx,
|
||||||
header: Box::new(header),
|
|
||||||
docs_link: Line::from(vec![
|
docs_link: Line::from(vec![
|
||||||
"Learn more: ".dim(),
|
"Learn more: ".dim(),
|
||||||
MEMORIES_DOC_URL.cyan().underlined(),
|
MEMORIES_DOC_URL.cyan().underlined(),
|
||||||
]),
|
]),
|
||||||
footer_hint: memories_settings_hint_line(),
|
|
||||||
};
|
};
|
||||||
view.initialize_selection();
|
view.initialize_selection();
|
||||||
view
|
view
|
||||||
@@ -97,29 +107,93 @@ impl MemoriesSettingsView {
|
|||||||
self.state.selected_idx = (!self.items.is_empty()).then_some(0);
|
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 {
|
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> {
|
fn build_rows(&self) -> Vec<GenericDisplayRow> {
|
||||||
let mut rows = Vec::with_capacity(self.items.len());
|
if let Some(state) = self.reset_confirmation.as_ref() {
|
||||||
let selected_idx = self.state.selected_idx;
|
return ["Reset all memories", "Go back"]
|
||||||
for (idx, item) in self.items.iter().enumerate() {
|
.into_iter()
|
||||||
let prefix = if selected_idx == Some(idx) {
|
.enumerate()
|
||||||
'›'
|
.map(|(idx, name)| GenericDisplayRow {
|
||||||
} else {
|
name: if state.selected_idx == Some(idx) {
|
||||||
' '
|
format!("› {name}")
|
||||||
};
|
} else {
|
||||||
let marker = if item.enabled { 'x' } else { ' ' };
|
format!(" {name}")
|
||||||
let name = format!("{prefix} [{marker}] {}", item.name);
|
},
|
||||||
rows.push(GenericDisplayRow {
|
description: Some(match idx {
|
||||||
name,
|
0 => "Delete local memory files and rollout summaries.".to_string(),
|
||||||
description: Some(item.description.to_string()),
|
1 => "Return to memory settings.".to_string(),
|
||||||
..Default::default()
|
_ => 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) {
|
fn move_up(&mut self) {
|
||||||
@@ -127,8 +201,9 @@ impl MemoriesSettingsView {
|
|||||||
if len == 0 {
|
if len == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.state.move_up_wrap(len);
|
let state = self.active_state_mut();
|
||||||
self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
|
state.move_up_wrap(len);
|
||||||
|
state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_down(&mut self) {
|
fn move_down(&mut self) {
|
||||||
@@ -136,17 +211,22 @@ impl MemoriesSettingsView {
|
|||||||
if len == 0 {
|
if len == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.state.move_down_wrap(len);
|
let state = self.active_state_mut();
|
||||||
self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
|
state.move_down_wrap(len);
|
||||||
|
state.ensure_visible(len, MAX_POPUP_ROWS.min(len));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_selected(&mut self) {
|
fn toggle_selected(&mut self) {
|
||||||
|
if self.reset_confirmation.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(selected_idx) = self.state.selected_idx else {
|
let Some(selected_idx) = self.state.selected_idx else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(item) = self.items.get_mut(selected_idx) {
|
if let Some(MemoriesMenuItem::Setting { enabled, .. }) = self.items.get_mut(selected_idx) {
|
||||||
item.enabled = !item.enabled;
|
*enabled = !*enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,8 +237,34 @@ impl MemoriesSettingsView {
|
|||||||
fn current_setting(&self, setting: MemoriesSetting) -> bool {
|
fn current_setting(&self, setting: MemoriesSetting) -> bool {
|
||||||
self.items
|
self.items
|
||||||
.iter()
|
.iter()
|
||||||
.find(|item| item.setting == setting)
|
.find_map(|item| match item {
|
||||||
.is_some_and(|item| item.enabled)
|
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 {
|
impl MemoriesSettingsView {
|
||||||
fn save(&mut self) {
|
fn save(&mut self) {
|
||||||
self.app_event_tx.send(AppEvent::UpdateMemorySettings {
|
if let Some(state) = self.reset_confirmation.as_ref() {
|
||||||
use_memories: self.current_setting(MemoriesSetting::Use),
|
match state.selected_idx {
|
||||||
generate_memories: self.current_setting(MemoriesSetting::Generate),
|
Some(0) => {
|
||||||
});
|
self.app_event_tx.send(AppEvent::ResetMemories);
|
||||||
self.complete = true;
|
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) {
|
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())
|
.style(user_message_style())
|
||||||
.render(content_area, buf);
|
.render(content_area, buf);
|
||||||
|
|
||||||
let header_height = self
|
let header = if self.reset_confirmation.is_some() {
|
||||||
.header
|
self.reset_confirmation_header()
|
||||||
.desired_height(content_area.width.saturating_sub(4));
|
} else {
|
||||||
|
self.settings_header()
|
||||||
|
};
|
||||||
|
let header_height = header.desired_height(content_area.width.saturating_sub(4));
|
||||||
let rows = self.build_rows();
|
let rows = self.build_rows();
|
||||||
let rows_width = Self::rows_width(content_area.width);
|
let rows_width = Self::rows_width(content_area.width);
|
||||||
let rows_height = measure_rows_height(
|
let rows_height = measure_rows_height(
|
||||||
&rows,
|
&rows,
|
||||||
&self.state,
|
self.active_state(),
|
||||||
MAX_POPUP_ROWS,
|
MAX_POPUP_ROWS,
|
||||||
rows_width.saturating_add(1),
|
rows_width.saturating_add(1),
|
||||||
);
|
);
|
||||||
@@ -276,7 +409,7 @@ impl Renderable for MemoriesSettingsView {
|
|||||||
])
|
])
|
||||||
.areas(content_area.inset(Insets::vh(/*v*/ 1, /*h*/ 2)));
|
.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 {
|
if list_area.height > 0 {
|
||||||
let render_area = Rect {
|
let render_area = Rect {
|
||||||
@@ -289,12 +422,14 @@ impl Renderable for MemoriesSettingsView {
|
|||||||
render_area,
|
render_area,
|
||||||
buf,
|
buf,
|
||||||
&rows,
|
&rows,
|
||||||
&self.state,
|
self.active_state(),
|
||||||
MAX_POPUP_ROWS,
|
MAX_POPUP_ROWS,
|
||||||
" No memory settings available",
|
" 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 {
|
let hint_area = Rect {
|
||||||
x: footer_area.x + 2,
|
x: footer_area.x + 2,
|
||||||
@@ -302,21 +437,31 @@ impl Renderable for MemoriesSettingsView {
|
|||||||
width: footer_area.width.saturating_sub(2),
|
width: footer_area.width.saturating_sub(2),
|
||||||
height: footer_area.height,
|
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 {
|
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 = self.build_rows();
|
||||||
let rows_width = Self::rows_width(width);
|
let rows_width = Self::rows_width(width);
|
||||||
let rows_height = measure_rows_height(
|
let rows_height = measure_rows_height(
|
||||||
&rows,
|
&rows,
|
||||||
&self.state,
|
self.active_state(),
|
||||||
MAX_POPUP_ROWS,
|
MAX_POPUP_ROWS,
|
||||||
rows_width.saturating_add(1),
|
rows_width.saturating_add(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut height = self.header.desired_height(width.saturating_sub(4));
|
let docs_height = if self.reset_confirmation.is_some() {
|
||||||
height = height.saturating_add(rows_height + 5);
|
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)
|
height.saturating_add(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,6 +472,6 @@ fn memories_settings_hint_line() -> Line<'static> {
|
|||||||
key_hint::plain(KeyCode::Char(' ')).into(),
|
key_hint::plain(KeyCode::Char(' ')).into(),
|
||||||
" to toggle; ".into(),
|
" to toggle; ".into(),
|
||||||
key_hint::plain(KeyCode::Enter).into(),
|
key_hint::plain(KeyCode::Enter).into(),
|
||||||
" to save".into(),
|
" to save or select".into(),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||||
|
expression: popup
|
||||||
|
---
|
||||||
|
Reset all memories?
|
||||||
|
|
||||||
|
› Reset all memories Delete local memory files and rollout summaries.
|
||||||
|
Go back Return to memory settings.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Press enter to confirm or esc to go back
|
||||||
+3
-1
@@ -9,7 +9,9 @@ expression: popup
|
|||||||
next thread.
|
next thread.
|
||||||
[ ] Generate memories Generate memories from the following threads. Current
|
[ ] Generate memories Generate memories from the following threads. Current
|
||||||
thread included.
|
thread included.
|
||||||
|
Reset all memories Clear local memory files and summaries. Existing
|
||||||
|
threads stay intact.
|
||||||
|
|
||||||
Learn more: https://developers.openai.com/codex/memories
|
Learn more: https://developers.openai.com/codex/memories
|
||||||
|
|
||||||
Press space to toggle; enter to save
|
Press space to toggle; enter to save or select
|
||||||
|
|||||||
@@ -1543,6 +1543,22 @@ async fn memories_settings_popup_snapshot() {
|
|||||||
assert_chatwidget_snapshot!("memories_settings_popup", popup);
|
assert_chatwidget_snapshot!("memories_settings_popup", popup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn memories_reset_confirmation_snapshot() {
|
||||||
|
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
chat.set_feature_enabled(Feature::MemoryTool, /*enabled*/ true);
|
||||||
|
chat.config.memories.use_memories = true;
|
||||||
|
chat.config.memories.generate_memories = false;
|
||||||
|
|
||||||
|
chat.open_memories_popup();
|
||||||
|
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||||
|
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||||
|
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||||
|
|
||||||
|
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||||
|
assert_chatwidget_snapshot!("memories_reset_confirmation", popup);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn memories_settings_toggle_saves_on_enter() {
|
async fn memories_settings_toggle_saves_on_enter() {
|
||||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
@@ -1564,6 +1580,22 @@ async fn memories_settings_toggle_saves_on_enter() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn memories_reset_confirmation_sends_event_on_confirm() {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
chat.set_feature_enabled(Feature::MemoryTool, /*enabled*/ true);
|
||||||
|
chat.config.memories.use_memories = true;
|
||||||
|
chat.config.memories.generate_memories = false;
|
||||||
|
|
||||||
|
chat.open_memories_popup();
|
||||||
|
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||||
|
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||||
|
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||||
|
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||||
|
|
||||||
|
assert_matches!(rx.try_recv(), Ok(AppEvent::ResetMemories));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn model_selection_popup_snapshot() {
|
async fn model_selection_popup_snapshot() {
|
||||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5-codex")).await;
|
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5-codex")).await;
|
||||||
|
|||||||
Reference in New Issue
Block a user