From 6c51bf0c7c4b14fb0be97e7454df1cfa9b3f5b1f Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 27 Apr 2026 09:51:12 -0700 Subject: [PATCH] Hide rewind preview when no user message exists (#19510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. --- codex-rs/tui/src/app_backtrack.rs | 78 ++++++++++++++++++- ...ack_unavailable_info_message_snapshot.snap | 5 ++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 codex-rs/tui/src/snapshots/codex_tui__app_backtrack__tests__backtrack_unavailable_info_message_snapshot.snap diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index da1f82e62..2afcaa3b5 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -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]) -> user_positions_iter(cells).count() } +fn has_backtrack_target(cells: &[Arc]) -> bool { + user_count(cells) > 0 +} + fn nth_user_position( cells: &[Arc], 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 { + lines + .iter() + .map(|line| { + line.spans + .iter() + .map(|span| span.content.as_ref()) + .collect::() + }) + .collect() + } + #[test] fn trim_transcript_for_first_user_drops_user_and_newer_cells() { let mut cells: Vec> = 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> = vec![ + Arc::new(AgentMessageCell::new( + vec![Line::from("assistant")], + /*is_first_line*/ true, + )) as Arc, + Arc::new(crate::history_cell::new_info_event( + "Context compacted".to_string(), + /*hint*/ None, + )) as Arc, + ]; + + 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); + + 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); + } } diff --git a/codex-rs/tui/src/snapshots/codex_tui__app_backtrack__tests__backtrack_unavailable_info_message_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__app_backtrack__tests__backtrack_unavailable_info_message_snapshot.snap new file mode 100644 index 000000000..8e896e7d7 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__app_backtrack__tests__backtrack_unavailable_info_message_snapshot.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/app_backtrack.rs +expression: rendered +--- +• No previous message to edit.