mirror of
https://github.com/openai/codex.git
synced 2026-05-05 05:42:33 +03:00
Hide rewind preview when no user message exists (#19510)
## Why Fixes #19508. In a fresh TUI session, pressing `Esc` twice entered the rewind transcript overlay even though there was no user message to rewind to. That produced an empty header-only transcript view and exposed a rewind flow that could not select a valid target. ## What changed The backtrack flow now checks whether a user-message rewind target exists before opening the transcript preview. If no target exists, Codex stays in the main TUI and shows `No previous message to edit.` instead of opening an empty overlay. The same guard applies when starting rewind preview from the transcript overlay, and the first `Esc` no longer advertises the “edit previous message” hint when there is no previous message available. Snapshot coverage was added for the unavailable rewind info message, along with a small target-detection test.
This commit is contained in:
@@ -10,7 +10,8 @@
|
||||
//!
|
||||
//! Backtrack operates as a small state machine:
|
||||
//! - The first `Esc` in the main view "primes" the feature and captures a base thread id.
|
||||
//! - A subsequent `Esc` opens the transcript overlay (`Ctrl+T`) and highlights a user message.
|
||||
//! - A subsequent `Esc` opens the transcript overlay (`Ctrl+T`) and highlights a user message when
|
||||
//! there is a rewind target.
|
||||
//! - `Enter` requests a rollback from core and records a `pending_rollback` guard.
|
||||
//! - On `EventMsg::ThreadRolledBack`, we either finish an in-flight backtrack request or queue a
|
||||
//! rollback trim so it runs in event order with transcript inserts.
|
||||
@@ -44,6 +45,8 @@ use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyEventKind;
|
||||
|
||||
const NO_PREVIOUS_MESSAGE_TO_EDIT: &str = "No previous message to edit.";
|
||||
|
||||
/// Aggregates all backtrack-related state used by the App.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct BacktrackState {
|
||||
@@ -266,11 +269,21 @@ impl App {
|
||||
self.backtrack.primed = true;
|
||||
self.backtrack.nth_user_message = usize::MAX;
|
||||
self.backtrack.base_id = self.chat_widget.thread_id();
|
||||
self.chat_widget.show_esc_backtrack_hint();
|
||||
if has_backtrack_target(&self.transcript_cells) {
|
||||
self.chat_widget.show_esc_backtrack_hint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Open overlay and begin backtrack preview flow (first step + highlight).
|
||||
fn open_backtrack_preview(&mut self, tui: &mut tui::Tui) {
|
||||
if !has_backtrack_target(&self.transcript_cells) {
|
||||
self.reset_backtrack_state();
|
||||
self.chat_widget
|
||||
.add_info_message(NO_PREVIOUS_MESSAGE_TO_EDIT.to_string(), /*hint*/ None);
|
||||
tui.frame_requester().schedule_frame();
|
||||
return;
|
||||
}
|
||||
|
||||
self.open_transcript_overlay(tui);
|
||||
self.backtrack.overlay_preview_active = true;
|
||||
// Composer is hidden by overlay; clear its hint.
|
||||
@@ -280,6 +293,14 @@ impl App {
|
||||
|
||||
/// When overlay is already open, begin preview mode and select latest user message.
|
||||
fn begin_overlay_backtrack_preview(&mut self, tui: &mut tui::Tui) {
|
||||
if !has_backtrack_target(&self.transcript_cells) {
|
||||
self.close_transcript_overlay(tui);
|
||||
self.chat_widget
|
||||
.add_info_message(NO_PREVIOUS_MESSAGE_TO_EDIT.to_string(), /*hint*/ None);
|
||||
tui.frame_requester().schedule_frame();
|
||||
return;
|
||||
}
|
||||
|
||||
self.backtrack.primed = true;
|
||||
self.backtrack.base_id = self.chat_widget.thread_id();
|
||||
self.backtrack.overlay_preview_active = true;
|
||||
@@ -613,6 +634,10 @@ pub(crate) fn user_count(cells: &[Arc<dyn crate::history_cell::HistoryCell>]) ->
|
||||
user_positions_iter(cells).count()
|
||||
}
|
||||
|
||||
fn has_backtrack_target(cells: &[Arc<dyn crate::history_cell::HistoryCell>]) -> bool {
|
||||
user_count(cells) > 0
|
||||
}
|
||||
|
||||
fn nth_user_position(
|
||||
cells: &[Arc<dyn crate::history_cell::HistoryCell>],
|
||||
nth: usize,
|
||||
@@ -674,9 +699,22 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::history_cell::AgentMessageCell;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::prelude::Line;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn render_lines(lines: &[Line<'static>]) -> Vec<String> {
|
||||
lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
line.spans
|
||||
.iter()
|
||||
.map(|span| span.content.as_ref())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_transcript_for_first_user_drops_user_and_newer_cells() {
|
||||
let mut cells: Vec<Arc<dyn HistoryCell>> = vec![
|
||||
@@ -885,4 +923,40 @@ mod tests {
|
||||
|
||||
assert_eq!(agent_group_count(&cells), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backtrack_target_requires_user_message() {
|
||||
let mut cells: Vec<Arc<dyn HistoryCell>> = vec![
|
||||
Arc::new(AgentMessageCell::new(
|
||||
vec![Line::from("assistant")],
|
||||
/*is_first_line*/ true,
|
||||
)) as Arc<dyn HistoryCell>,
|
||||
Arc::new(crate::history_cell::new_info_event(
|
||||
"Context compacted".to_string(),
|
||||
/*hint*/ None,
|
||||
)) as Arc<dyn HistoryCell>,
|
||||
];
|
||||
|
||||
assert!(!has_backtrack_target(&cells));
|
||||
|
||||
cells.push(Arc::new(UserHistoryCell {
|
||||
message: "hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
local_image_paths: Vec::new(),
|
||||
remote_image_urls: Vec::new(),
|
||||
}) as Arc<dyn HistoryCell>);
|
||||
|
||||
assert!(has_backtrack_target(&cells));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backtrack_unavailable_info_message_snapshot() {
|
||||
let cell = crate::history_cell::new_info_event(
|
||||
NO_PREVIOUS_MESSAGE_TO_EDIT.to_string(),
|
||||
/*hint*/ None,
|
||||
);
|
||||
let rendered = render_lines(&cell.display_lines(/*width*/ 80)).join("\n");
|
||||
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user