Support Ctrl+P/Ctrl+N in resume picker (#18267)

Fixes #18179.

## Why
The fullscreen `/resume` picker accepted Up/Down navigation but ignored
Ctrl+P/Ctrl+N, which made it inconsistent with other TUI selection flows
such as `ListSelectionView`-backed pickers and composer navigation.

## What Changed
Updated `codex-rs/tui/src/resume_picker.rs` so the resume picker treats
Ctrl+P/Ctrl+N as aliases for Up/Down, including the raw `^P`/`^N`
control-character events some terminals emit without a CONTROL modifier.
This commit is contained in:
Eric Traut
2026-04-17 08:38:47 -07:00
committed by GitHub
Unverified
parent 3421a107e0
commit c3ecb557d3
+63 -19
View File
@@ -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<Option<SessionSelection>> {
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);