1+ use crate :: key_hint;
2+ use crate :: key_hint:: KeyBinding ;
13use crate :: key_hint:: has_ctrl_or_alt;
24use crossterm:: event:: KeyCode ;
35use crossterm:: event:: KeyEvent ;
@@ -48,7 +50,6 @@ use codex_protocol::custom_prompts::CustomPrompt;
4850use codex_protocol:: custom_prompts:: PROMPTS_CMD_PREFIX ;
4951
5052use crate :: app_event:: AppEvent ;
51- use crate :: app_event:: ExitMode ;
5253use crate :: app_event_sender:: AppEventSender ;
5354use crate :: bottom_pane:: textarea:: TextArea ;
5455use crate :: bottom_pane:: textarea:: TextAreaState ;
@@ -107,7 +108,8 @@ pub(crate) struct ChatComposer {
107108 active_popup : ActivePopup ,
108109 app_event_tx : AppEventSender ,
109110 history : ChatComposerHistory ,
110- ctrl_c_quit_hint : bool ,
111+ quit_shortcut_expires_at : Option < Instant > ,
112+ quit_shortcut_key : KeyBinding ,
111113 esc_backtrack_hint : bool ,
112114 use_shift_enter_hint : bool ,
113115 dismissed_file_popup_token : Option < String > ,
@@ -160,7 +162,8 @@ impl ChatComposer {
160162 active_popup : ActivePopup :: None ,
161163 app_event_tx,
162164 history : ChatComposerHistory :: new ( ) ,
163- ctrl_c_quit_hint : false ,
165+ quit_shortcut_expires_at : None ,
166+ quit_shortcut_key : key_hint:: ctrl ( KeyCode :: Char ( 'c' ) ) ,
164167 esc_backtrack_hint : false ,
165168 use_shift_enter_hint,
166169 dismissed_file_popup_token : None ,
@@ -457,16 +460,37 @@ impl ChatComposer {
457460 }
458461 }
459462
460- pub fn set_ctrl_c_quit_hint ( & mut self , show : bool , has_focus : bool ) {
461- self . ctrl_c_quit_hint = show;
462- if show {
463- self . footer_mode = FooterMode :: CtrlCReminder ;
464- } else {
465- self . footer_mode = reset_mode_after_activity ( self . footer_mode ) ;
466- }
463+ /// Show the transient "press again to quit" hint for `key`.
464+ ///
465+ /// The owner (`BottomPane`/`ChatWidget`) is responsible for scheduling a
466+ /// redraw after [`super::QUIT_SHORTCUT_TIMEOUT`] so the hint can disappear
467+ /// even when the UI is otherwise idle.
468+ pub fn show_quit_shortcut_hint ( & mut self , key : KeyBinding , has_focus : bool ) {
469+ self . quit_shortcut_expires_at = Instant :: now ( )
470+ . checked_add ( super :: QUIT_SHORTCUT_TIMEOUT )
471+ . or_else ( || Some ( Instant :: now ( ) ) ) ;
472+ self . quit_shortcut_key = key;
473+ self . footer_mode = FooterMode :: QuitShortcutReminder ;
474+ self . set_has_focus ( has_focus) ;
475+ }
476+
477+ /// Clear the "press again to quit" hint immediately.
478+ pub fn clear_quit_shortcut_hint ( & mut self , has_focus : bool ) {
479+ self . quit_shortcut_expires_at = None ;
480+ self . footer_mode = reset_mode_after_activity ( self . footer_mode ) ;
467481 self . set_has_focus ( has_focus) ;
468482 }
469483
484+ /// Whether the quit shortcut hint should currently be shown.
485+ ///
486+ /// This is time-based rather than event-based: it may become false without
487+ /// any additional user input, so the UI schedules a redraw when the hint
488+ /// expires.
489+ pub ( crate ) fn quit_shortcut_hint_visible ( & self ) -> bool {
490+ self . quit_shortcut_expires_at
491+ . is_some_and ( |expires_at| Instant :: now ( ) < expires_at)
492+ }
493+
470494 fn next_large_paste_placeholder ( & mut self , char_count : usize ) -> String {
471495 let base = format ! ( "[Pasted Content {char_count} chars]" ) ;
472496 let next_suffix = self . large_paste_counters . entry ( char_count) . or_insert ( 0 ) ;
@@ -1184,11 +1208,7 @@ impl ChatComposer {
11841208 modifiers : crossterm:: event:: KeyModifiers :: CONTROL ,
11851209 kind : KeyEventKind :: Press ,
11861210 ..
1187- } if self . is_empty ( ) => {
1188- self . app_event_tx
1189- . send ( AppEvent :: Exit ( ExitMode :: ShutdownFirst { confirm : true } ) ) ;
1190- ( InputResult :: None , true )
1191- }
1211+ } if self . is_empty ( ) => ( InputResult :: None , false ) ,
11921212 // -------------------------------------------------------------
11931213 // History navigation (Up / Down) – only when the composer is not
11941214 // empty or when the cursor is at the correct position, to avoid
@@ -1687,7 +1707,7 @@ impl ChatComposer {
16871707 return false ;
16881708 }
16891709
1690- let next = toggle_shortcut_mode ( self . footer_mode , self . ctrl_c_quit_hint ) ;
1710+ let next = toggle_shortcut_mode ( self . footer_mode , self . quit_shortcut_hint_visible ( ) ) ;
16911711 let changed = next != self . footer_mode ;
16921712 self . footer_mode = next;
16931713 changed
@@ -1698,7 +1718,7 @@ impl ChatComposer {
16981718 mode : self . footer_mode ( ) ,
16991719 esc_backtrack_hint : self . esc_backtrack_hint ,
17001720 use_shift_enter_hint : self . use_shift_enter_hint ,
1701- is_task_running : self . is_task_running ,
1721+ quit_shortcut_key : self . quit_shortcut_key ,
17021722 context_window_percent : self . context_window_percent ,
17031723 context_window_used_tokens : self . context_window_used_tokens ,
17041724 }
@@ -1708,8 +1728,13 @@ impl ChatComposer {
17081728 match self . footer_mode {
17091729 FooterMode :: EscHint => FooterMode :: EscHint ,
17101730 FooterMode :: ShortcutOverlay => FooterMode :: ShortcutOverlay ,
1711- FooterMode :: CtrlCReminder => FooterMode :: CtrlCReminder ,
1712- FooterMode :: ShortcutSummary if self . ctrl_c_quit_hint => FooterMode :: CtrlCReminder ,
1731+ FooterMode :: QuitShortcutReminder if self . quit_shortcut_hint_visible ( ) => {
1732+ FooterMode :: QuitShortcutReminder
1733+ }
1734+ FooterMode :: QuitShortcutReminder => FooterMode :: ShortcutSummary ,
1735+ FooterMode :: ShortcutSummary if self . quit_shortcut_hint_visible ( ) => {
1736+ FooterMode :: QuitShortcutReminder
1737+ }
17131738 FooterMode :: ShortcutSummary if !self . is_empty ( ) => FooterMode :: ContextOnly ,
17141739 other => other,
17151740 }
@@ -2250,16 +2275,16 @@ mod tests {
22502275 } ) ;
22512276
22522277 snapshot_composer_state ( "footer_mode_ctrl_c_quit" , true , |composer| {
2253- composer. set_ctrl_c_quit_hint ( true , true ) ;
2278+ composer. show_quit_shortcut_hint ( key_hint :: ctrl ( KeyCode :: Char ( 'c' ) ) , true ) ;
22542279 } ) ;
22552280
22562281 snapshot_composer_state ( "footer_mode_ctrl_c_interrupt" , true , |composer| {
22572282 composer. set_task_running ( true ) ;
2258- composer. set_ctrl_c_quit_hint ( true , true ) ;
2283+ composer. show_quit_shortcut_hint ( key_hint :: ctrl ( KeyCode :: Char ( 'c' ) ) , true ) ;
22592284 } ) ;
22602285
22612286 snapshot_composer_state ( "footer_mode_ctrl_c_then_esc_hint" , true , |composer| {
2262- composer. set_ctrl_c_quit_hint ( true , true ) ;
2287+ composer. show_quit_shortcut_hint ( key_hint :: ctrl ( KeyCode :: Char ( 'c' ) ) , true ) ;
22632288 let _ = composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Esc , KeyModifiers :: NONE ) ) ;
22642289 } ) ;
22652290
0 commit comments