diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index cb3dea5e..2906e3a6 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -427,8 +427,9 @@ impl App { tui.frame_requester().schedule_frame(); } // Esc primes/advances backtracking only in normal (not working) mode - // with an empty composer. In any other state, forward Esc so the - // active UI (e.g. status indicator, modals, popups) handles it. + // with the composer focused and empty. In any other state, forward + // Esc so the active UI (e.g. status indicator, modals, popups) + // handles it. KeyEvent { code: KeyCode::Esc, kind: KeyEventKind::Press | KeyEventKind::Repeat, diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index 5528fa7b..8f889623 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -82,15 +82,16 @@ impl App { /// Handle global Esc presses for backtracking when no overlay is present. pub(crate) fn handle_backtrack_esc_key(&mut self, tui: &mut tui::Tui) { - // Only handle backtracking when composer is empty to avoid clobbering edits. - if self.chat_widget.composer_is_empty() { - if !self.backtrack.primed { - self.prime_backtrack(); - } else if self.overlay.is_none() { - self.open_backtrack_preview(tui); - } else if self.backtrack.overlay_preview_active { - self.step_backtrack_and_highlight(tui); - } + if !self.chat_widget.composer_is_empty() { + return; + } + + if !self.backtrack.primed { + self.prime_backtrack(); + } else if self.overlay.is_none() { + self.open_backtrack_preview(tui); + } else if self.backtrack.overlay_preview_active { + self.step_backtrack_and_highlight(tui); } } diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 39a170ce..56a6e44d 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -857,10 +857,12 @@ impl ChatComposer { return (InputResult::None, true); } if key_event.code == KeyCode::Esc { - let next_mode = esc_hint_mode(self.footer_mode, self.is_task_running); - if next_mode != self.footer_mode { - self.footer_mode = next_mode; - return (InputResult::None, true); + if self.is_empty() { + let next_mode = esc_hint_mode(self.footer_mode, self.is_task_running); + if next_mode != self.footer_mode { + self.footer_mode = next_mode; + return (InputResult::None, true); + } } } else { self.footer_mode = reset_mode_after_activity(self.footer_mode); @@ -1797,6 +1799,35 @@ mod tests { }); } + #[test] + fn esc_hint_stays_hidden_with_draft_content() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + true, + "Ask Codex to do anything".to_string(), + false, + ); + + type_chars_humanlike(&mut composer, &['d']); + + assert!(!composer.is_empty()); + assert_eq!(composer.current_text(), "d"); + assert_eq!(composer.footer_mode, FooterMode::ShortcutSummary); + assert!(matches!(composer.active_popup, ActivePopup::None)); + + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + + assert_eq!(composer.footer_mode, FooterMode::ShortcutSummary); + assert!(!composer.esc_backtrack_hint); + } + #[test] fn question_mark_only_toggles_on_first_char() { use crossterm::event::KeyCode;