Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions codex-rs/tui/src/app_backtrack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ pub(crate) struct PendingBacktrackRollback {
impl App {
/// Route overlay events while the transcript overlay is active.
///
/// If backtrack preview is active, Esc steps the selection and Enter confirms it.
/// Otherwise, Esc begins preview mode and all other events are forwarded to the overlay.
/// If backtrack preview is active, Esc / Left steps selection, Right steps forward, Enter
/// confirms. Otherwise, Esc begins preview mode and all other events are forwarded to the
/// overlay.
pub(crate) async fn handle_backtrack_overlay_event(
&mut self,
tui: &mut tui::Tui,
Expand All @@ -110,6 +111,22 @@ impl App {
self.overlay_step_backtrack(tui, event)?;
Ok(true)
}
TuiEvent::Key(KeyEvent {
code: KeyCode::Left,
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
}) => {
self.overlay_step_backtrack(tui, event)?;
Ok(true)
}
TuiEvent::Key(KeyEvent {
code: KeyCode::Right,
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
}) => {
self.overlay_step_backtrack_forward(tui, event)?;
Ok(true)
}
TuiEvent::Key(KeyEvent {
code: KeyCode::Enter,
kind: KeyEventKind::Press,
Expand Down Expand Up @@ -277,6 +294,27 @@ impl App {
tui.frame_requester().schedule_frame();
}

/// Step selection to the next newer user message and update overlay.
fn step_forward_backtrack_and_highlight(&mut self, tui: &mut tui::Tui) {
let count = user_count(&self.transcript_cells);
if count == 0 {
return;
}

let last_index = count.saturating_sub(1);
let next_selection = if self.backtrack.nth_user_message == usize::MAX {
last_index
} else {
self.backtrack
.nth_user_message
.saturating_add(1)
.min(last_index)
};

self.apply_backtrack_selection_internal(next_selection);
tui.frame_requester().schedule_frame();
}

/// Apply a computed backtrack selection to the overlay and internal counter.
fn apply_backtrack_selection_internal(&mut self, nth_user_message: usize) {
if let Some(cell_idx) = nth_user_position(&self.transcript_cells, nth_user_message) {
Expand Down Expand Up @@ -364,6 +402,20 @@ impl App {
Ok(())
}

/// Handle Right in overlay backtrack preview: step selection forward if armed, else forward.
fn overlay_step_backtrack_forward(
&mut self,
tui: &mut tui::Tui,
event: TuiEvent,
) -> Result<()> {
if self.backtrack.base_id.is_some() {
self.step_forward_backtrack_and_highlight(tui);
} else {
self.overlay_forward_event(tui, event)?;
}
Ok(())
}

/// Confirm a primed backtrack from the main view (no overlay visible).
/// Computes the prefill from the selected user message for rollback.
pub(crate) fn confirm_backtrack_from_main(&mut self) -> Option<BacktrackSelection> {
Expand Down
41 changes: 29 additions & 12 deletions codex-rs/tui/src/pager_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ const KEY_SPACE: KeyBinding = key_hint::plain(KeyCode::Char(' '));
const KEY_SHIFT_SPACE: KeyBinding = key_hint::shift(KeyCode::Char(' '));
const KEY_HOME: KeyBinding = key_hint::plain(KeyCode::Home);
const KEY_END: KeyBinding = key_hint::plain(KeyCode::End);
const KEY_LEFT: KeyBinding = key_hint::plain(KeyCode::Left);
const KEY_RIGHT: KeyBinding = key_hint::plain(KeyCode::Right);
const KEY_CTRL_F: KeyBinding = key_hint::ctrl(KeyCode::Char('f'));
const KEY_CTRL_D: KeyBinding = key_hint::ctrl(KeyCode::Char('d'));
const KEY_CTRL_B: KeyBinding = key_hint::ctrl(KeyCode::Char('b'));
Expand Down Expand Up @@ -637,10 +639,13 @@ impl TranscriptOverlay {
let line2 = Rect::new(area.x, area.y.saturating_add(1), area.width, 1);
render_key_hints(line1, buf, PAGER_KEY_HINTS);

let mut pairs: Vec<(&[KeyBinding], &str)> =
vec![(&[KEY_Q], "to quit"), (&[KEY_ESC], "to edit prev")];
let mut pairs: Vec<(&[KeyBinding], &str)> = vec![(&[KEY_Q], "to quit")];
if self.highlight_cell.is_some() {
pairs.push((&[KEY_ESC, KEY_LEFT], "to edit prev"));
pairs.push((&[KEY_RIGHT], "to edit next"));
pairs.push((&[KEY_ENTER], "to edit message"));
} else {
pairs.push((&[KEY_ESC], "to edit prev"));
}
render_key_hints(line2, buf, &pairs);
}
Expand Down Expand Up @@ -816,25 +821,37 @@ mod tests {
lines: vec![Line::from("hello")],
})]);

// Render into a small buffer and assert the backtrack hint is present
let area = Rect::new(0, 0, 40, 10);
// Render into a wide buffer so the footer hints aren't truncated.
let area = Rect::new(0, 0, 120, 10);
let mut buf = Buffer::empty(area);
overlay.render(area, &mut buf);

// Flatten buffer to a string and check for the hint text
let mut s = String::new();
for y in area.y..area.bottom() {
for x in area.x..area.right() {
s.push(buf[(x, y)].symbol().chars().next().unwrap_or(' '));
}
s.push('\n');
}
let s = buffer_to_text(&buf, area);
assert!(
s.contains("edit prev"),
"expected 'edit prev' hint in overlay footer, got: {s:?}"
);
}

#[test]
fn edit_next_hint_is_visible_when_highlighted() {
let mut overlay = TranscriptOverlay::new(vec![Arc::new(TestCell {
lines: vec![Line::from("hello")],
})]);
overlay.set_highlight_cell(Some(0));

// Render into a wide buffer so the footer hints aren't truncated.
let area = Rect::new(0, 0, 120, 10);
let mut buf = Buffer::empty(area);
overlay.render(area, &mut buf);

let s = buffer_to_text(&buf, area);
assert!(
s.contains("edit next"),
"expected 'edit next' hint in overlay footer, got: {s:?}"
);
}

#[test]
fn transcript_overlay_snapshot_basic() {
// Prepare a transcript overlay with a few lines
Expand Down
56 changes: 54 additions & 2 deletions codex-rs/tui2/src/app_backtrack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ pub(crate) struct PendingBacktrackRollback {
impl App {
/// Route overlay events while the transcript overlay is active.
///
/// If backtrack preview is active, Esc steps the selection and Enter confirms it.
/// Otherwise, Esc begins preview mode and all other events are forwarded to the overlay.
/// If backtrack preview is active, Esc / Left steps selection, Right steps forward, Enter
/// confirms. Otherwise, Esc begins preview mode and all other events are forwarded to the
/// overlay.
pub(crate) async fn handle_backtrack_overlay_event(
&mut self,
tui: &mut tui::Tui,
Expand All @@ -111,6 +112,22 @@ impl App {
self.overlay_step_backtrack(tui, event)?;
Ok(true)
}
TuiEvent::Key(KeyEvent {
code: KeyCode::Left,
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
}) => {
self.overlay_step_backtrack(tui, event)?;
Ok(true)
}
TuiEvent::Key(KeyEvent {
code: KeyCode::Right,
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
}) => {
self.overlay_step_backtrack_forward(tui, event)?;
Ok(true)
}
TuiEvent::Key(KeyEvent {
code: KeyCode::Enter,
kind: KeyEventKind::Press,
Expand Down Expand Up @@ -308,6 +325,27 @@ impl App {
tui.frame_requester().schedule_frame();
}

/// Step selection to the next newer user message and update overlay.
fn step_forward_backtrack_and_highlight(&mut self, tui: &mut tui::Tui) {
let count = user_count(&self.transcript_cells);
if count == 0 {
return;
}

let last_index = count.saturating_sub(1);
let next_selection = if self.backtrack.nth_user_message == usize::MAX {
last_index
} else {
self.backtrack
.nth_user_message
.saturating_add(1)
.min(last_index)
};

self.apply_backtrack_selection_internal(next_selection);
tui.frame_requester().schedule_frame();
}

/// Apply a computed backtrack selection to the overlay and internal counter.
fn apply_backtrack_selection_internal(&mut self, nth_user_message: usize) {
if let Some(cell_idx) = nth_user_position(&self.transcript_cells, nth_user_message) {
Expand Down Expand Up @@ -387,6 +425,20 @@ impl App {
Ok(())
}

/// Handle Right in overlay backtrack preview: step selection forward if armed, else forward.
fn overlay_step_backtrack_forward(
&mut self,
tui: &mut tui::Tui,
event: TuiEvent,
) -> Result<()> {
if self.backtrack.base_id.is_some() {
self.step_forward_backtrack_and_highlight(tui);
} else {
self.overlay_forward_event(tui, event)?;
}
Ok(())
}

/// Confirm a primed backtrack from the main view (no overlay visible).
/// Computes the prefill from the selected user message for rollback.
pub(crate) fn confirm_backtrack_from_main(&mut self) -> Option<BacktrackSelection> {
Expand Down
41 changes: 29 additions & 12 deletions codex-rs/tui2/src/pager_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ const KEY_SPACE: KeyBinding = key_hint::plain(KeyCode::Char(' '));
const KEY_SHIFT_SPACE: KeyBinding = key_hint::shift(KeyCode::Char(' '));
const KEY_HOME: KeyBinding = key_hint::plain(KeyCode::Home);
const KEY_END: KeyBinding = key_hint::plain(KeyCode::End);
const KEY_LEFT: KeyBinding = key_hint::plain(KeyCode::Left);
const KEY_RIGHT: KeyBinding = key_hint::plain(KeyCode::Right);
const KEY_CTRL_F: KeyBinding = key_hint::ctrl(KeyCode::Char('f'));
const KEY_CTRL_D: KeyBinding = key_hint::ctrl(KeyCode::Char('d'));
const KEY_CTRL_B: KeyBinding = key_hint::ctrl(KeyCode::Char('b'));
Expand Down Expand Up @@ -656,10 +658,13 @@ impl TranscriptOverlay {
let line2 = Rect::new(area.x, area.y.saturating_add(1), area.width, 1);
render_key_hints(line1, buf, PAGER_KEY_HINTS);

let mut pairs: Vec<(&[KeyBinding], &str)> =
vec![(&[KEY_Q], "to quit"), (&[KEY_ESC], "to edit prev")];
let mut pairs: Vec<(&[KeyBinding], &str)> = vec![(&[KEY_Q], "to quit")];
if self.highlight_cell.is_some() {
pairs.push((&[KEY_ESC, KEY_LEFT], "to edit prev"));
pairs.push((&[KEY_RIGHT], "to edit next"));
pairs.push((&[KEY_ENTER], "to edit message"));
} else {
pairs.push((&[KEY_ESC], "to edit prev"));
}
render_key_hints(line2, buf, &pairs);
}
Expand Down Expand Up @@ -837,25 +842,37 @@ mod tests {
lines: vec![Line::from("hello")],
})]);

// Render into a small buffer and assert the backtrack hint is present
let area = Rect::new(0, 0, 40, 10);
// Render into a wide buffer so the footer hints aren't truncated.
let area = Rect::new(0, 0, 120, 10);
let mut buf = Buffer::empty(area);
overlay.render(area, &mut buf);

// Flatten buffer to a string and check for the hint text
let mut s = String::new();
for y in area.y..area.bottom() {
for x in area.x..area.right() {
s.push(buf[(x, y)].symbol().chars().next().unwrap_or(' '));
}
s.push('\n');
}
let s = buffer_to_text(&buf, area);
assert!(
s.contains("edit prev"),
"expected 'edit prev' hint in overlay footer, got: {s:?}"
);
}

#[test]
fn edit_next_hint_is_visible_when_highlighted() {
let mut overlay = TranscriptOverlay::new(vec![Arc::new(TestCell {
lines: vec![Line::from("hello")],
})]);
overlay.set_highlight_cell(Some(0));

// Render into a wide buffer so the footer hints aren't truncated.
let area = Rect::new(0, 0, 120, 10);
let mut buf = Buffer::empty(area);
overlay.render(area, &mut buf);

let s = buffer_to_text(&buf, area);
assert!(
s.contains("edit next"),
"expected 'edit next' hint in overlay footer, got: {s:?}"
);
}

#[test]
fn transcript_overlay_snapshot_basic() {
// Prepare a transcript overlay with a few lines
Expand Down
Loading