From 7979ce453a23f41be27aaeb997b4705429b51345 Mon Sep 17 00:00:00 2001 From: Charley Cunningham Date: Mon, 2 Mar 2026 10:26:13 -0800 Subject: [PATCH] tui: restore draft footer hints (#13202) ## Summary - restore `Tab to queue` when a draft is present and the agent is running - keep draft-idle footers passive by showing the normal footer or status line instead of `? for shortcuts` - align footer snapshot coverage with the updated draft footer behavior ## Codex author `codex resume 019c7f1c-43aa-73e0-97c7-40f457396bb0` --------- Co-authored-by: Codex --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 17 ++- codex-rs/tui/src/bottom_pane/footer.rs | 124 ++++++++++++------ ...oter_status_line_overrides_draft_idle.snap | 5 + ...oter_status_line_yields_to_queue_hint.snap | 5 + 4 files changed, 107 insertions(+), 44 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_status_line_overrides_draft_idle.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_status_line_yields_to_queue_hint.snap diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 5ea67d8c3..c012ef75a 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -4108,10 +4108,10 @@ impl ChatComposer { !footer_props.is_task_running && self.collaboration_mode_indicator.is_some(); let show_shortcuts_hint = match footer_props.mode { FooterMode::ComposerEmpty => !self.is_in_paste_burst(), + FooterMode::ComposerHasDraft => false, FooterMode::QuitShortcutReminder | FooterMode::ShortcutOverlay - | FooterMode::EscHint - | FooterMode::ComposerHasDraft => false, + | FooterMode::EscHint => false, }; let show_queue_hint = match footer_props.mode { FooterMode::ComposerHasDraft => footer_props.is_task_running, @@ -4141,10 +4141,13 @@ impl ChatComposer { .as_ref() .map(|line| line.clone().dim()); let status_line_candidate = footer_props.status_line_enabled - && matches!( - footer_props.mode, - FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft - ); + && match footer_props.mode { + FooterMode::ComposerEmpty => true, + FooterMode::ComposerHasDraft => !footer_props.is_task_running, + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay + | FooterMode::EscHint => false, + }; let mut truncated_status_line = if status_line_candidate { status_line.as_ref().map(|line| { truncate_line_with_ellipsis_if_overflow(line.clone(), available_width) @@ -4210,7 +4213,7 @@ impl ChatComposer { can_show_left_with_context(hint_rect, left_width, right_width); let has_override = self.footer_flash_visible() || self.footer_hint_override.is_some(); - let single_line_layout = if has_override { + let single_line_layout = if has_override || status_line_active { None } else { match footer_props.mode { diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index cf1d30277..2ad23272e 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -172,10 +172,10 @@ pub(crate) fn reset_mode_after_activity(current: FooterMode) -> FooterMode { pub(crate) fn footer_height(props: &FooterProps) -> u16 { let show_shortcuts_hint = match props.mode { FooterMode::ComposerEmpty => true, - FooterMode::QuitShortcutReminder - | FooterMode::ShortcutOverlay - | FooterMode::EscHint - | FooterMode::ComposerHasDraft => false, + FooterMode::ComposerHasDraft => false, + FooterMode::QuitShortcutReminder | FooterMode::ShortcutOverlay | FooterMode::EscHint => { + false + } }; let show_queue_hint = match props.mode { FooterMode::ComposerHasDraft => props.is_task_running, @@ -562,13 +562,18 @@ fn footer_from_props_lines( show_shortcuts_hint: bool, show_queue_hint: bool, ) -> Vec> { - // If status line content is present, show it for base modes. + // If status line content is present, show it for passive composer states. + // Active draft states still prefer the queue hint over the passive status + // line so the footer stays actionable while a task is running. if props.status_line_enabled && let Some(status_line) = &props.status_line_value - && matches!( - props.mode, - FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft - ) + && match props.mode { + FooterMode::ComposerEmpty => true, + FooterMode::ComposerHasDraft => !props.is_task_running, + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay + | FooterMode::EscHint => false, + } { return vec![status_line.clone().dim()]; } @@ -601,6 +606,8 @@ fn footer_from_props_lines( let state = LeftSideState { hint: if show_queue_hint { SummaryHintKind::QueueMessage + } else if show_shortcuts_hint { + SummaryHintKind::Shortcuts } else { SummaryHintKind::None }, @@ -1013,10 +1020,10 @@ mod tests { let show_cycle_hint = !props.is_task_running; let show_shortcuts_hint = match props.mode { FooterMode::ComposerEmpty => true, + FooterMode::ComposerHasDraft => false, FooterMode::QuitShortcutReminder | FooterMode::ShortcutOverlay - | FooterMode::EscHint - | FooterMode::ComposerHasDraft => false, + | FooterMode::EscHint => false, }; let show_queue_hint = match props.mode { FooterMode::ComposerHasDraft => props.is_task_running, @@ -1025,13 +1032,21 @@ mod tests { | FooterMode::ShortcutOverlay | FooterMode::EscHint => false, }; - let left_mode_indicator = if props.status_line_enabled { + let status_line_active = props.status_line_enabled + && match props.mode { + FooterMode::ComposerEmpty => true, + FooterMode::ComposerHasDraft => !props.is_task_running, + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay + | FooterMode::EscHint => false, + }; + let left_mode_indicator = if status_line_active { None } else { collaboration_mode_indicator }; let available_width = area.width.saturating_sub(FOOTER_INDENT_COLS as u16) as usize; - let mut truncated_status_line = if props.status_line_enabled + let mut truncated_status_line = if status_line_active && matches!( props.mode, FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft @@ -1044,7 +1059,7 @@ mod tests { } else { None }; - let mut left_width = if props.status_line_enabled { + let mut left_width = if status_line_active { truncated_status_line .as_ref() .map(|line| line.width() as u16) @@ -1058,7 +1073,7 @@ mod tests { show_queue_hint, ) }; - let right_line = if props.status_line_enabled { + let right_line = if status_line_active { let full = mode_indicator_line(collaboration_mode_indicator, show_cycle_hint); let compact = mode_indicator_line(collaboration_mode_indicator, false); let full_width = full.as_ref().map(|line| line.width() as u16).unwrap_or(0); @@ -1077,7 +1092,7 @@ mod tests { .as_ref() .map(|line| line.width() as u16) .unwrap_or(0); - if props.status_line_enabled + if status_line_active && let Some(max_left) = max_left_width_for_right(area, right_width) && left_width > max_left && let Some(line) = props @@ -1097,21 +1112,24 @@ mod tests { props.mode, FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft ) { - let (summary_left, show_context) = single_line_footer_layout( - area, - right_width, - left_mode_indicator, - show_cycle_hint, - show_shortcuts_hint, - show_queue_hint, - ); - match summary_left { - SummaryLeft::Default => { - if props.status_line_enabled { - if let Some(line) = truncated_status_line.clone() { - render_footer_line(area, f.buffer_mut(), line); - } - } else { + if status_line_active { + if let Some(line) = truncated_status_line.clone() { + render_footer_line(area, f.buffer_mut(), line); + } + if can_show_left_and_context && let Some(line) = &right_line { + render_context_right(area, f.buffer_mut(), line); + } + } else { + let (summary_left, show_context) = single_line_footer_layout( + area, + right_width, + left_mode_indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ); + match summary_left { + SummaryLeft::Default => { render_footer_from_props( area, f.buffer_mut(), @@ -1122,14 +1140,14 @@ mod tests { show_queue_hint, ); } + SummaryLeft::Custom(line) => { + render_footer_line(area, f.buffer_mut(), line); + } + SummaryLeft::None => {} } - SummaryLeft::Custom(line) => { - render_footer_line(area, f.buffer_mut(), line); + if show_context && let Some(line) = &right_line { + render_context_right(area, f.buffer_mut(), line); } - SummaryLeft::None => {} - } - if show_context && let Some(line) = &right_line { - render_context_right(area, f.buffer_mut(), line); } } else { render_footer_from_props( @@ -1416,6 +1434,38 @@ mod tests { snapshot_footer("footer_status_line_overrides_shortcuts", props); + let props = FooterProps { + mode: FooterMode::ComposerHasDraft, + esc_backtrack_hint: false, + use_shift_enter_hint: false, + is_task_running: true, + collaboration_modes_enabled: false, + is_wsl: false, + quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), + context_window_percent: None, + context_window_used_tokens: None, + status_line_value: Some(Line::from("Status line content".to_string())), + status_line_enabled: true, + }; + + snapshot_footer("footer_status_line_yields_to_queue_hint", props); + + let props = FooterProps { + mode: FooterMode::ComposerHasDraft, + esc_backtrack_hint: false, + use_shift_enter_hint: false, + is_task_running: false, + collaboration_modes_enabled: false, + is_wsl: false, + quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), + context_window_percent: None, + context_window_used_tokens: None, + status_line_value: Some(Line::from("Status line content".to_string())), + status_line_enabled: true, + }; + + snapshot_footer("footer_status_line_overrides_draft_idle", props); + let props = FooterProps { mode: FooterMode::ComposerEmpty, esc_backtrack_hint: false, diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_status_line_overrides_draft_idle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_status_line_overrides_draft_idle.snap new file mode 100644 index 000000000..bb0e2d33b --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_status_line_overrides_draft_idle.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/bottom_pane/footer.rs +expression: terminal.backend() +--- +" Status line content " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_status_line_yields_to_queue_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_status_line_yields_to_queue_hint.snap new file mode 100644 index 000000000..71370d83b --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_status_line_yields_to_queue_hint.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/bottom_pane/footer.rs +expression: terminal.backend() +--- +" tab to queue message 100% context left "