From 5aee6e802c7b4fdb0776121acdf17d203652d75b Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 16 Apr 2026 22:01:39 -0700 Subject: [PATCH 1/2] Support ctrl-p/ctrl-n in resume picker --- codex-rs/tui/src/resume_picker.rs | 132 +++++++++++++++++++++++++----- 1 file changed, 113 insertions(+), 19 deletions(-) diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 6703a606c34b..3fb95295b1e0 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -31,6 +31,7 @@ use color_eyre::eyre::Result; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; +use crossterm::event::KeyModifiers; use ratatui::layout::Constraint; use ratatui::layout::Layout; use ratatui::layout::Rect; @@ -617,16 +618,21 @@ impl PickerState { async fn handle_key(&mut self, key: KeyEvent) -> Result> { self.inline_error = None; - match key.code { - KeyCode::Esc => return Ok(Some(SessionSelection::StartFresh)), - KeyCode::Char('c') - if key - .modifiers - .contains(crossterm::event::KeyModifiers::CONTROL) => - { + match key { + KeyEvent { + code: KeyCode::Esc, .. + } => return Ok(Some(SessionSelection::StartFresh)), + KeyEvent { + code: KeyCode::Char('c'), + modifiers, + .. + } if modifiers.contains(KeyModifiers::CONTROL) => { return Ok(Some(SessionSelection::Exit)); } - KeyCode::Enter => { + KeyEvent { + code: KeyCode::Enter, + .. + } => { if let Some(row) = self.filtered_rows.get(self.selected) { let path = row.path.clone(); let thread_id = match row.thread_id { @@ -656,14 +662,39 @@ impl PickerState { self.request_frame(); } } - KeyCode::Up => { + KeyEvent { + code: KeyCode::Up, .. + } + | KeyEvent { + code: KeyCode::Char('p'), + modifiers: KeyModifiers::CONTROL, + .. + } + | KeyEvent { + code: KeyCode::Char('\u{0010}'), + modifiers: KeyModifiers::NONE, + .. + } /* ^P */ => { if self.selected > 0 { self.selected -= 1; self.ensure_selected_visible(); } self.request_frame(); } - KeyCode::Down => { + KeyEvent { + code: KeyCode::Down, + .. + } + | KeyEvent { + code: KeyCode::Char('n'), + modifiers: KeyModifiers::CONTROL, + .. + } + | KeyEvent { + code: KeyCode::Char('\u{000e}'), + modifiers: KeyModifiers::NONE, + .. + } /* ^N */ => { if self.selected + 1 < self.filtered_rows.len() { self.selected += 1; self.ensure_selected_visible(); @@ -671,7 +702,10 @@ impl PickerState { self.maybe_load_more_for_scroll(); self.request_frame(); } - KeyCode::PageUp => { + KeyEvent { + code: KeyCode::PageUp, + .. + } => { let step = self.view_rows.unwrap_or(10).max(1); if self.selected > 0 { self.selected = self.selected.saturating_sub(step); @@ -679,7 +713,10 @@ impl PickerState { self.request_frame(); } } - KeyCode::PageDown => { + KeyEvent { + code: KeyCode::PageDown, + .. + } => { if !self.filtered_rows.is_empty() { let step = self.view_rows.unwrap_or(10).max(1); let max_index = self.filtered_rows.len().saturating_sub(1); @@ -689,21 +726,28 @@ impl PickerState { self.request_frame(); } } - KeyCode::Tab => { + KeyEvent { + code: KeyCode::Tab, .. + } => { self.toggle_sort_key(); self.request_frame(); } - KeyCode::Backspace => { + KeyEvent { + code: KeyCode::Backspace, + .. + } => { let mut new_query = self.query.clone(); new_query.pop(); self.set_query(new_query); } - KeyCode::Char(c) => { + KeyEvent { + code: KeyCode::Char(c), + modifiers, + .. + } => { // basic text input for search - if !key - .modifiers - .contains(crossterm::event::KeyModifiers::CONTROL) - && !key.modifiers.contains(crossterm::event::KeyModifiers::ALT) + if !modifiers.contains(KeyModifiers::CONTROL) + && !modifiers.contains(KeyModifiers::ALT) { let mut new_query = self.query.clone(); new_query.push(c); @@ -2738,6 +2782,56 @@ mod tests { assert_eq!(state.selected, state.filtered_rows.len().saturating_sub(2)); } + #[tokio::test] + async fn ctrl_p_and_ctrl_n_browse_rows() { + let loader: PageLoader = Arc::new(|_| {}); + let mut state = PickerState::new( + PathBuf::from("/tmp"), + FrameRequester::test_dummy(), + loader, + ProviderFilter::MatchDefault(String::from("openai")), + /*show_all*/ true, + /*filter_cwd*/ None, + SessionPickerAction::Resume, + ); + + state.reset_pagination(); + state.ingest_page(page( + vec![ + make_item("/tmp/a.jsonl", "2025-01-03T00:00:00Z", "third"), + make_item("/tmp/b.jsonl", "2025-01-02T00:00:00Z", "second"), + make_item("/tmp/c.jsonl", "2025-01-01T00:00:00Z", "first"), + ], + /*next_cursor*/ None, + /*num_scanned_files*/ 3, + /*reached_scan_cap*/ false, + )); + + state + .handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL)) + .await + .unwrap(); + assert_eq!(state.selected, 1); + + state + .handle_key(KeyEvent::new(KeyCode::Char('\u{000e}'), KeyModifiers::NONE)) + .await + .unwrap(); + assert_eq!(state.selected, 2); + + state + .handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL)) + .await + .unwrap(); + assert_eq!(state.selected, 1); + + state + .handle_key(KeyEvent::new(KeyCode::Char('\u{0010}'), KeyModifiers::NONE)) + .await + .unwrap(); + assert_eq!(state.selected, 0); + } + #[tokio::test] async fn set_query_loads_until_match_and_respects_scan_cap() { let recorded_requests: Arc>> = Arc::new(Mutex::new(Vec::new())); From 7eabbda9e8a1af926e5b23251d290fd4a8a4d534 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 16 Apr 2026 22:09:59 -0700 Subject: [PATCH 2/2] codex: address PR review feedback (#18267) --- codex-rs/tui/src/resume_picker.rs | 50 ------------------------------- 1 file changed, 50 deletions(-) diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 3fb95295b1e0..93828afe8f2d 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -2782,56 +2782,6 @@ mod tests { assert_eq!(state.selected, state.filtered_rows.len().saturating_sub(2)); } - #[tokio::test] - async fn ctrl_p_and_ctrl_n_browse_rows() { - let loader: PageLoader = Arc::new(|_| {}); - let mut state = PickerState::new( - PathBuf::from("/tmp"), - FrameRequester::test_dummy(), - loader, - ProviderFilter::MatchDefault(String::from("openai")), - /*show_all*/ true, - /*filter_cwd*/ None, - SessionPickerAction::Resume, - ); - - state.reset_pagination(); - state.ingest_page(page( - vec![ - make_item("/tmp/a.jsonl", "2025-01-03T00:00:00Z", "third"), - make_item("/tmp/b.jsonl", "2025-01-02T00:00:00Z", "second"), - make_item("/tmp/c.jsonl", "2025-01-01T00:00:00Z", "first"), - ], - /*next_cursor*/ None, - /*num_scanned_files*/ 3, - /*reached_scan_cap*/ false, - )); - - state - .handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL)) - .await - .unwrap(); - assert_eq!(state.selected, 1); - - state - .handle_key(KeyEvent::new(KeyCode::Char('\u{000e}'), KeyModifiers::NONE)) - .await - .unwrap(); - assert_eq!(state.selected, 2); - - state - .handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL)) - .await - .unwrap(); - assert_eq!(state.selected, 1); - - state - .handle_key(KeyEvent::new(KeyCode::Char('\u{0010}'), KeyModifiers::NONE)) - .await - .unwrap(); - assert_eq!(state.selected, 0); - } - #[tokio::test] async fn set_query_loads_until_match_and_respects_scan_cap() { let recorded_requests: Arc>> = Arc::new(Mutex::new(Vec::new()));