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) {
|
||||
if let Err(err) = webbrowser::open(&url) {
|
||||
self.chat_widget
|
||||
@@ -5413,6 +5425,9 @@ impl App {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
AppEvent::ResetMemories => {
|
||||
self.reset_memories_with_app_server(app_server).await;
|
||||
}
|
||||
AppEvent::SkipNextWorldWritableScan => {
|
||||
self.windows_sandbox.skip_world_writable_scan_once = true;
|
||||
}
|
||||
@@ -8121,6 +8136,35 @@ mod tests {
|
||||
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]
|
||||
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;
|
||||
|
||||
@@ -470,6 +470,9 @@ pub(crate) enum AppEvent {
|
||||
generate_memories: bool,
|
||||
},
|
||||
|
||||
/// Clear all persisted local memory artifacts via the app-server.
|
||||
ResetMemories,
|
||||
|
||||
/// Update whether the full access warning prompt has been acknowledged.
|
||||
UpdateFullAccessWarningAcknowledged(bool),
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ use codex_app_server_protocol::GetAccountParams;
|
||||
use codex_app_server_protocol::GetAccountRateLimitsResponse;
|
||||
use codex_app_server_protocol::GetAccountResponse;
|
||||
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::ModelListParams;
|
||||
use codex_app_server_protocol::ModelListResponse;
|
||||
@@ -535,6 +536,19 @@ impl AppServerSession {
|
||||
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<()> {
|
||||
let request_id = self.next_request_id();
|
||||
let _: ThreadUnsubscribeResponse = self
|
||||
|
||||
@@ -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(),
|
||||
])
|
||||
}
|
||||
|
||||
+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.
|
||||
[ ] Generate memories Generate memories from the following threads. Current
|
||||
thread included.
|
||||
Reset all memories Clear local memory files and summaries. Existing
|
||||
threads stay intact.
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#[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]
|
||||
async fn memories_settings_toggle_saves_on_enter() {
|
||||
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]
|
||||
async fn model_selection_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5-codex")).await;
|
||||
|
||||
Reference in New Issue
Block a user