From b8cccf08afbfd526b256f4ecc0239d666dae1d05 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 15:01:59 -0800 Subject: [PATCH 01/26] TUI footer: right-align context and degrade shortcut summary + mode cleanly --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 84 ++++-- codex-rs/tui/src/bottom_pane/footer.rs | 254 ++++++++++++++---- ...mposer__tests__backspace_after_pastes.snap | 2 +- ...tom_pane__chat_composer__tests__empty.snap | 2 +- ...__tests__footer_mode_ctrl_c_interrupt.snap | 2 +- ...poser__tests__footer_mode_ctrl_c_quit.snap | 2 +- ...sts__footer_mode_ctrl_c_then_esc_hint.snap | 2 +- ...tests__footer_mode_esc_hint_backtrack.snap | 2 +- ...ts__footer_mode_esc_hint_from_overlay.snap | 2 +- ...ests__footer_mode_hidden_while_typing.snap | 2 +- ...r_mode_overlay_then_external_esc_hint.snap | 2 +- ...__tests__footer_mode_shortcut_overlay.snap | 2 +- ...er__tests__image_placeholder_multiple.snap | 3 +- ...oser__tests__image_placeholder_single.snap | 3 +- ...tom_pane__chat_composer__tests__large.snap | 2 +- ...chat_composer__tests__multiple_pastes.snap | 2 +- ...tom_pane__chat_composer__tests__small.snap | 2 +- ...oter_context_only_queue_hint_disabled.snap | 2 +- ...ooter_context_only_queue_hint_enabled.snap | 2 +- ...er__tests__footer_context_tokens_used.snap | 2 +- ...ooter__tests__footer_ctrl_c_quit_idle.snap | 2 +- ...er__tests__footer_ctrl_c_quit_running.snap | 2 +- ...__footer__tests__footer_esc_hint_idle.snap | 2 +- ...footer__tests__footer_esc_hint_primed.snap | 2 +- ...r_mode_indicator_narrow_overlap_hides.snap | 2 +- ...ter_mode_indicator_running_hides_hint.snap | 2 +- ...er__tests__footer_mode_indicator_wide.snap | 2 +- ...sts__footer_shortcuts_context_running.snap | 2 +- ...oter__tests__footer_shortcuts_default.snap | 2 +- ...tests__footer_shortcuts_shift_and_esc.snap | 2 +- ...s_visible_when_status_hidden_snapshot.snap | 2 +- ...er_fill_height_without_bottom_padding.snap | 2 +- ...__status_and_queued_messages_snapshot.snap | 2 +- ...tom_pane__tests__status_only_snapshot.snap | 2 +- ..._details_and_queued_messages_snapshot.snap | 2 +- ...exec_and_status_layout_vt100_snapshot.snap | 2 +- ...i__chatwidget__tests__chatwidget_tall.snap | 2 +- ...et__tests__mcp_startup_header_booting.snap | 2 +- ..._review_queues_user_messages_snapshot.snap | 2 +- ...atwidget__tests__status_widget_active.snap | 3 +- 40 files changed, 309 insertions(+), 108 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index c4fba97f658a..99676ad55edd 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -79,11 +79,9 @@ use crossterm::event::KeyModifiers; use ratatui::buffer::Buffer; use ratatui::layout::Constraint; use ratatui::layout::Layout; -use ratatui::layout::Margin; use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; -use ratatui::text::Span; use ratatui::widgets::Block; use ratatui::widgets::StatefulWidgetRef; use ratatui::widgets::WidgetRef; @@ -96,15 +94,20 @@ use super::file_search_popup::FileSearchPopup; use super::footer::CollaborationModeIndicator; use super::footer::FooterMode; use super::footer::FooterProps; +use super::footer::SummaryLeft; +use super::footer::can_show_left_with_context; +use super::footer::context_window_line; use super::footer::esc_hint_mode; use super::footer::footer_height; use super::footer::footer_hint_items_width; use super::footer::footer_line_width; use super::footer::inset_footer_hint_area; +use super::footer::render_context_right; use super::footer::render_footer; use super::footer::render_footer_hint_items; -use super::footer::render_mode_indicator; +use super::footer::render_footer_line; use super::footer::reset_mode_after_activity; +use super::footer::shortcut_summary_layout; use super::footer::toggle_shortcut_mode; use super::paste_burst::CharDecision; use super::paste_burst::PasteBurst; @@ -2502,6 +2505,12 @@ impl Renderable for ChatComposer { } ActivePopup::None => { let footer_props = self.footer_props(); + let show_cycle_hint = !footer_props.is_task_running; + let context_line = context_window_line( + footer_props.context_window_percent, + footer_props.context_window_used_tokens, + ); + let context_width = context_line.width() as u16; let custom_height = self.custom_footer_height(); let footer_hint_height = custom_height.unwrap_or_else(|| footer_height(footer_props)); @@ -2516,26 +2525,60 @@ impl Renderable for ChatComposer { } else { popup_rect }; - let mut left_content_width = None; - if self.footer_flash_visible() { + let indicator = self.collaboration_mode_indicator; + let left_width = if self.footer_flash_visible() { + self.footer_flash + .as_ref() + .map(|flash| flash.line.width() as u16) + .unwrap_or(0) + } else if let Some(items) = self.footer_hint_override.as_ref() { + footer_hint_items_width(items) + } else { + footer_line_width(footer_props, indicator, show_cycle_hint) + }; + let can_show_both = + can_show_left_with_context(hint_rect, left_width, context_width); + let has_override = + self.footer_flash_visible() || self.footer_hint_override.is_some(); + let summary_layout = + if !has_override && matches!(footer_props.mode, FooterMode::ShortcutSummary) { + Some(shortcut_summary_layout( + hint_rect, + context_width, + indicator, + show_cycle_hint, + )) + } else { + None + }; + let show_context = summary_layout + .as_ref() + .map(|(_, show_context)| *show_context) + .unwrap_or(can_show_both); + + if let Some((summary_left, _)) = summary_layout { + match summary_left { + SummaryLeft::Default => { + render_footer(hint_rect, buf, footer_props, indicator, show_cycle_hint); + } + SummaryLeft::Custom(line) => { + render_footer_line(hint_rect, buf, line); + } + SummaryLeft::None => {} + } + } else if self.footer_flash_visible() { if let Some(flash) = self.footer_flash.as_ref() { flash.line.render(inset_footer_hint_area(hint_rect), buf); - left_content_width = Some(flash.line.width() as u16); } } else if let Some(items) = self.footer_hint_override.as_ref() { render_footer_hint_items(hint_rect, buf, items); - left_content_width = Some(footer_hint_items_width(items)); } else { - render_footer(hint_rect, buf, footer_props); - left_content_width = Some(footer_line_width(footer_props)); + render_footer(hint_rect, buf, footer_props, indicator, show_cycle_hint); + } + + if show_context { + render_context_right(hint_rect, buf, &context_line); } - render_mode_indicator( - hint_rect, - buf, - self.collaboration_mode_indicator, - !footer_props.is_task_running, - left_content_width, - ); } } let style = user_message_style(); @@ -2565,8 +2608,13 @@ impl Renderable for ChatComposer { .unwrap_or("Input disabled.") .to_string() }; - let placeholder = Span::from(text).dim(); - Line::from(vec![placeholder]).render_ref(textarea_rect.inner(Margin::new(0, 0)), buf); + if !textarea_rect.is_empty() { + let text_x = textarea_rect.x; + let text_y = textarea_rect.y; + let text_width = textarea_rect.width; + let placeholder = text.dim(); + buf.set_span(text_x, text_y, &placeholder, text_width); + } } } } diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 16f4f22a94b4..106fc7b63b56 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -130,51 +130,157 @@ pub(crate) fn reset_mode_after_activity(current: FooterMode) -> FooterMode { } pub(crate) fn footer_height(props: FooterProps) -> u16 { - footer_lines(props).len() as u16 + footer_lines(props, None, false).len() as u16 } -pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps) { +pub(crate) fn render_footer( + area: Rect, + buf: &mut Buffer, + props: FooterProps, + indicator: Option, + show_cycle_hint: bool, +) { + Paragraph::new(prefix_lines( + footer_lines(props, indicator, show_cycle_hint), + " ".repeat(FOOTER_INDENT_COLS).into(), + " ".repeat(FOOTER_INDENT_COLS).into(), + )) + .render(area, buf); +} + +pub(crate) fn render_footer_line(area: Rect, buf: &mut Buffer, line: Line<'static>) { Paragraph::new(prefix_lines( - footer_lines(props), + vec![line], " ".repeat(FOOTER_INDENT_COLS).into(), " ".repeat(FOOTER_INDENT_COLS).into(), )) .render(area, buf); } -pub(crate) fn render_mode_indicator( +pub(crate) fn left_fits(area: Rect, left_width: u16) -> bool { + let max_width = area.width.saturating_sub(FOOTER_INDENT_COLS as u16); + left_width <= max_width +} + +pub(crate) fn shortcut_summary_line( + indicator: Option, + show_cycle_hint: bool, + show_shortcuts_hint: bool, +) -> Line<'static> { + let mut line = Line::from(""); + if show_shortcuts_hint { + line.push_span(key_hint::plain(KeyCode::Char('?'))); + line.push_span(" for shortcuts".dim()); + } + + if let Some(indicator) = indicator { + if show_shortcuts_hint { + line.push_span(" · ".dim()); + } + line.push_span(indicator.styled_span(show_cycle_hint)); + } + + line +} + +pub(crate) enum SummaryLeft { + Default, + Custom(Line<'static>), + None, +} + +pub(crate) fn shortcut_summary_layout( area: Rect, - buf: &mut Buffer, + context_width: u16, indicator: Option, show_cycle_hint: bool, - left_content_width: Option, -) { +) -> (SummaryLeft, bool) { + let default_line = shortcut_summary_line(indicator, show_cycle_hint, true); + let default_width = default_line.width() as u16; + if can_show_left_with_context(area, default_width, context_width) { + return (SummaryLeft::Default, true); + } + let Some(indicator) = indicator else { - return; + return (SummaryLeft::None, true); }; + + if show_cycle_hint { + let mode_with_cycle = shortcut_summary_line(Some(indicator), true, false); + let mode_with_cycle_width = mode_with_cycle.width() as u16; + if can_show_left_with_context(area, mode_with_cycle_width, context_width) { + return (SummaryLeft::Custom(mode_with_cycle), true); + } + } + + let mode_only = shortcut_summary_line(Some(indicator), false, false); + let mode_only_width = mode_only.width() as u16; + if can_show_left_with_context(area, mode_only_width, context_width) { + return (SummaryLeft::Custom(mode_only), true); + } + if left_fits(area, mode_only_width) { + return (SummaryLeft::Custom(mode_only), false); + } + + (SummaryLeft::None, true) +} + +fn right_aligned_x(area: Rect, content_width: u16) -> Option { if area.is_empty() { - return; + return None; + } + + let right_padding = FOOTER_INDENT_COLS as u16; + let max_width = area.width.saturating_sub(right_padding); + if content_width == 0 || max_width == 0 { + return None; } - let span = indicator.styled_span(show_cycle_hint); - let label_width = span.width() as u16; - if label_width == 0 || label_width > area.width { + if content_width >= max_width { + return Some(area.x.saturating_add(right_padding)); + } + + Some( + area.x + .saturating_add(area.width) + .saturating_sub(content_width) + .saturating_sub(right_padding), + ) +} + +pub(crate) fn can_show_left_with_context(area: Rect, left_width: u16, context_width: u16) -> bool { + let Some(context_x) = right_aligned_x(area, context_width) else { + return true; + }; + let left_extent = FOOTER_INDENT_COLS as u16 + left_width; + left_extent <= context_x.saturating_sub(area.x) +} + +pub(crate) fn render_context_right(area: Rect, buf: &mut Buffer, line: &Line<'static>) { + if area.is_empty() { return; } - let x = area - .x - .saturating_add(area.width) - .saturating_sub(label_width) - .saturating_sub(FOOTER_INDENT_COLS as u16); + let context_width = line.width() as u16; + let Some(mut x) = right_aligned_x(area, context_width) else { + return; + }; let y = area.y + area.height.saturating_sub(1); - if let Some(left_content_width) = left_content_width { - let left_extent = FOOTER_INDENT_COLS as u16 + left_content_width; - if left_extent >= x.saturating_sub(area.x) { - return; + let max_x = area.x.saturating_add(area.width); + + for span in &line.spans { + if x >= max_x { + break; + } + let span_width = span.width() as u16; + if span_width == 0 { + continue; } + let remaining = max_x.saturating_sub(x); + let draw_width = span_width.min(remaining); + buf.set_span(x, y, span, draw_width); + x = x.saturating_add(span_width); } - buf.set_span(x, y, &span, label_width); } pub(crate) fn inset_footer_hint_area(mut area: Rect) -> Rect { @@ -193,25 +299,26 @@ pub(crate) fn render_footer_hint_items(area: Rect, buf: &mut Buffer, items: &[(S footer_hint_items_line(items).render(inset_footer_hint_area(area), buf); } -fn footer_lines(props: FooterProps) -> Vec> { - // Show the context indicator on the left, appended after the primary hint - // (e.g., "? for shortcuts"). Keep it visible even when typing (i.e., when - // the shortcut hint is hidden). Hide it only for the multi-line - // ShortcutOverlay. +fn footer_lines( + props: FooterProps, + indicator: Option, + show_cycle_hint: bool, +) -> Vec> { + // Render left-side hints only. The context indicator is rendered + // separately, right-aligned by the caller. match props.mode { FooterMode::QuitShortcutReminder => { vec![quit_shortcut_reminder_line(props.quit_shortcut_key)] } FooterMode::ShortcutSummary => { - let mut line = context_window_line( - props.context_window_percent, - props.context_window_used_tokens, - ); - line.push_span(" · ".dim()); - line.extend(vec![ + let mut line = Line::from(vec![ key_hint::plain(KeyCode::Char('?')).into(), " for shortcuts".dim(), ]); + if let Some(indicator) = indicator { + line.push_span(" · ".dim()); + line.push_span(indicator.styled_span(show_cycle_hint)); + } vec![line] } FooterMode::ShortcutOverlay => { @@ -230,12 +337,8 @@ fn footer_lines(props: FooterProps) -> Vec> { } FooterMode::EscHint => vec![esc_hint_line(props.esc_backtrack_hint)], FooterMode::ContextOnly => { - let mut line = context_window_line( - props.context_window_percent, - props.context_window_used_tokens, - ); + let mut line = Line::from(""); if props.is_task_running && props.steer_enabled { - line.push_span(" · ".dim()); line.push_span(key_hint::plain(KeyCode::Tab)); line.push_span(" to queue message".dim()); } @@ -244,8 +347,12 @@ fn footer_lines(props: FooterProps) -> Vec> { } } -pub(crate) fn footer_line_width(props: FooterProps) -> u16 { - footer_lines(props) +pub(crate) fn footer_line_width( + props: FooterProps, + indicator: Option, + show_cycle_hint: bool, +) -> u16 { + footer_lines(props, indicator, show_cycle_hint) .last() .map(|line| line.width() as u16) .unwrap_or(0) @@ -396,7 +503,7 @@ fn build_columns(entries: Vec>) -> Vec> { .collect() } -fn context_window_line(percent: Option, used_tokens: Option) -> Line<'static> { +pub(crate) fn context_window_line(percent: Option, used_tokens: Option) -> Line<'static> { if let Some(percent) = percent { let percent = percent.clamp(0, 100); return Line::from(vec![Span::from(format!("{percent}% context left")).dim()]); @@ -620,7 +727,35 @@ mod tests { terminal .draw(|f| { let area = Rect::new(0, 0, f.area().width, height); - render_footer(area, f.buffer_mut(), props); + let context_line = context_window_line( + props.context_window_percent, + props.context_window_used_tokens, + ); + let context_width = context_line.width() as u16; + let show_cycle_hint = !props.is_task_running; + let left_width = footer_line_width(props, None, show_cycle_hint); + let can_show_both = can_show_left_with_context(area, left_width, context_width); + if matches!(props.mode, FooterMode::ShortcutSummary) { + let (summary_left, show_context) = + shortcut_summary_layout(area, context_width, None, show_cycle_hint); + match summary_left { + SummaryLeft::Default => { + render_footer(area, f.buffer_mut(), props, None, show_cycle_hint); + } + SummaryLeft::Custom(line) => { + render_footer_line(area, f.buffer_mut(), line); + } + SummaryLeft::None => {} + } + if show_context { + render_context_right(area, f.buffer_mut(), &context_line); + } + } else { + render_footer(area, f.buffer_mut(), props, None, show_cycle_hint); + if can_show_both { + render_context_right(area, f.buffer_mut(), &context_line); + } + } }) .unwrap(); assert_snapshot!(name, terminal.backend()); @@ -637,14 +772,35 @@ mod tests { terminal .draw(|f| { let area = Rect::new(0, 0, f.area().width, height); - render_footer(area, f.buffer_mut(), props); - render_mode_indicator( - area, - f.buffer_mut(), - indicator, - !props.is_task_running, - Some(footer_line_width(props)), + let context_line = context_window_line( + props.context_window_percent, + props.context_window_used_tokens, ); + let context_width = context_line.width() as u16; + let show_cycle_hint = !props.is_task_running; + let left_width = footer_line_width(props, indicator, show_cycle_hint); + let can_show_both = can_show_left_with_context(area, left_width, context_width); + if matches!(props.mode, FooterMode::ShortcutSummary) { + let (summary_left, show_context) = + shortcut_summary_layout(area, context_width, indicator, show_cycle_hint); + match summary_left { + SummaryLeft::Default => { + render_footer(area, f.buffer_mut(), props, indicator, show_cycle_hint); + } + SummaryLeft::Custom(line) => { + render_footer_line(area, f.buffer_mut(), line); + } + SummaryLeft::None => {} + } + if show_context { + render_context_right(area, f.buffer_mut(), &context_line); + } + } else { + render_footer(area, f.buffer_mut(), props, indicator, show_cycle_hint); + if can_show_both { + render_context_right(area, f.buffer_mut(), &context_line); + } + } }) .unwrap(); assert_snapshot!(name, terminal.backend()); diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap index e4cc9ffefd57..0b88e19a22f0 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap index 53e0aee4cf90..47c97c74d22d 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" 100% context left · ? for shortcuts " +" ? for shortcuts 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap index 7ecc5bba7196..2edeebc1720b 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" ctrl + c again to quit " +" ctrl + c again to quit 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap index 7ecc5bba7196..2edeebc1720b 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" ctrl + c again to quit " +" ctrl + c again to quit 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap index 9cad17b86482..e9b0d2b51af9 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" esc esc to edit previous message " +" esc esc to edit previous message 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap index 2fce42cc26b7..5b101f024b21 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" esc again to edit previous message " +" esc again to edit previous message 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap index 9cad17b86482..e9b0d2b51af9 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" esc esc to edit previous message " +" esc esc to edit previous message 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_hidden_while_typing.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_hidden_while_typing.snap index 67e616e917fc..5faacfa64f0b 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_hidden_while_typing.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_hidden_while_typing.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap index 2fce42cc26b7..5b101f024b21 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" esc again to edit previous message " +" esc again to edit previous message 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap index 8486a9ec6f37..5be85fc63b70 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap @@ -15,4 +15,4 @@ expression: terminal.backend() " @ for file paths ctrl + v to paste images " " ctrl + g to edit in external editor esc again to edit previous message " " ctrl + c to exit " -" ctrl + t to view transcript " +" ctrl + t to view transcript 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_multiple.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_multiple.snap index 3f1adf62916b..49eca416c24d 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_multiple.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_multiple.snap @@ -1,6 +1,5 @@ --- source: tui/src/bottom_pane/chat_composer.rs -assertion_line: 2116 expression: terminal.backend() --- " " @@ -11,4 +10,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_single.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_single.snap index e46fa0a740c9..3a5dd7a758f1 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_single.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__image_placeholder_single.snap @@ -1,6 +1,5 @@ --- source: tui/src/bottom_pane/chat_composer.rs -assertion_line: 2116 expression: terminal.backend() --- " " @@ -11,4 +10,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap index 6b018021ecec..d2f77dbec3f7 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap index 40098faee016..0d16cec0b497 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap index 498ed7693660..8d3f8216db2c 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap @@ -11,4 +11,4 @@ expression: terminal.backend() " " " " " " -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_disabled.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_disabled.snap index ce36b2ada8ef..6fdeda07b165 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_disabled.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_disabled.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 100% context left " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_enabled.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_enabled.snap index b9733866d772..71370d83ba83 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_enabled.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_enabled.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 100% context left · tab to queue message " +" tab to queue message 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_tokens_used.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_tokens_used.snap index a77ca5565b68..b7ee60704ce6 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_tokens_used.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_tokens_used.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 123K used · ? for shortcuts " +" ? for shortcuts 123K used " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap index 31a1b743b8e3..46c42df67f7a 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" ctrl + c again to quit " +" ctrl + c again to quit 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap index 31a1b743b8e3..46c42df67f7a 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" ctrl + c again to quit " +" ctrl + c again to quit 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap index b2333b025f6f..b6d064c6248c 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" esc esc to edit previous message " +" esc esc to edit previous message 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap index 20f9b178b4b8..93af76783c73 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" esc again to edit previous message " +" esc again to edit previous message 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_narrow_overlap_hides.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_narrow_overlap_hides.snap index ed9fea7c885b..8e1767b8a671 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_narrow_overlap_hides.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_narrow_overlap_hides.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 100% context left · ? for shortcuts " +" Plan mode 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_running_hides_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_running_hides_hint.snap index fe5868b87cfd..9f9be080da15 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_running_hides_hint.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_running_hides_hint.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 100% context left · ? for shortcuts Plan mode " +" ? for shortcuts · Plan mode 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_wide.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_wide.snap index 7212d6de5103..8c32ee50dc82 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_wide.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_wide.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 100% context left · ? for shortcuts Plan mode (shift+tab to cycle) " +" ? for shortcuts · Plan mode (shift+tab to cycle) 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_context_running.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_context_running.snap index d05ac90a9113..2a81b8557604 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_context_running.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_context_running.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 72% context left · ? for shortcuts " +" ? for shortcuts 72% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap index c95a5dc0b3d6..02804e5735ed 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_default.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" 100% context left · ? for shortcuts " +" ? for shortcuts 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap index c1f00d443774..11f782632393 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap @@ -7,4 +7,4 @@ expression: terminal.backend() " @ for file paths ctrl + v to paste images " " ctrl + g to edit in external editor esc again to edit previous message " " ctrl + c to exit " -" ctrl + t to view transcript " +" ctrl + t to view transcript 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap index 5aea41519097..47581631c2b4 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__queued_messages_visible_when_status_hidden_snapshot.snap @@ -7,4 +7,4 @@ expression: "render_snapshot(&pane, area)" › Ask Codex to do anything - 100% context left · ? for shortcuts + ? for shortcuts 100% context left diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_composer_fill_height_without_bottom_padding.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_composer_fill_height_without_bottom_padding.snap index 86e3da45730f..494883e4c376 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_composer_fill_height_without_bottom_padding.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_composer_fill_height_without_bottom_padding.snap @@ -7,4 +7,4 @@ expression: "render_snapshot(&pane, area)" › Ask Codex to do anything - 100% context left · ? for sh + 100% context left diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_queued_messages_snapshot.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_queued_messages_snapshot.snap index e651ec9274b7..a82f6512cf4f 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_queued_messages_snapshot.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_and_queued_messages_snapshot.snap @@ -9,4 +9,4 @@ expression: "render_snapshot(&pane, area)" › Ask Codex to do anything - 100% context left · ? for shortcuts + ? for shortcuts 100% context left diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_only_snapshot.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_only_snapshot.snap index 79e1e126eb03..136c35805547 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_only_snapshot.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_only_snapshot.snap @@ -7,4 +7,4 @@ expression: "render_snapshot(&pane, area)" › Ask Codex to do anything - 100% context left · ? for shortcuts + ? for shortcuts 100% context left diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap index 12090d09e911..b714c69d88ed 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__tests__status_with_details_and_queued_messages_snapshot.snap @@ -11,4 +11,4 @@ expression: "render_snapshot(&pane, area)" › Ask Codex to do anything - 100% context left · ? for shortcuts + ? for shortcuts 100% context left diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap index b51d759fe7d1..52779fd8406a 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap @@ -41,4 +41,4 @@ expression: term.backend().vt100().screen().contents() › Summarize recent commits - 100% context left · tab to queue message + tab to queue message 100% context left diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap index 64361e90f91e..ebffeb8f53d8 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap @@ -25,4 +25,4 @@ expression: term.backend().vt100().screen().contents() › Ask Codex to do anything - 100% context left · ? for shortcuts + ? for shortcuts 100% context left diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__mcp_startup_header_booting.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__mcp_startup_header_booting.snap index c6866c1b5113..6074ed1f2067 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__mcp_startup_header_booting.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__mcp_startup_header_booting.snap @@ -8,4 +8,4 @@ expression: terminal.backend() " " "› Ask Codex to do anything " " " -" 100% context left · ? for shortcuts " +" ? for shortcuts 100% context left " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap index 1c02350a6d0d..ce28175ea62f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap @@ -19,4 +19,4 @@ expression: term.backend().vt100().screen().contents() › Ask Codex to do anything - 100% context left · ? for shortcuts + ? for shortcuts 100% context left diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap index 9fbebfb500f9..3acfd95eec8d 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap @@ -1,6 +1,5 @@ --- source: tui/src/chatwidget/tests.rs -assertion_line: 1577 expression: terminal.backend() --- " " @@ -9,4 +8,4 @@ expression: terminal.backend() " " "› Ask Codex to do anything " " " -" 100% context left · ? for shortcuts " +" ? for shortcuts 100% context left " From 547abe61d43d7d674504f79df363ca9d5ec4f422 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 15:33:16 -0800 Subject: [PATCH 02/26] Fix "? for shortcuts" show/hide behavior --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 43 ++++++- codex-rs/tui/src/bottom_pane/footer.rs | 113 +++++++++++++----- 2 files changed, 123 insertions(+), 33 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 99676ad55edd..8089c97f182b 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2506,6 +2506,12 @@ impl Renderable for ChatComposer { ActivePopup::None => { let footer_props = self.footer_props(); let show_cycle_hint = !footer_props.is_task_running; + let show_shortcuts_hint = matches!(footer_props.mode, FooterMode::ShortcutSummary) + && self.is_empty() + && !self.is_in_paste_burst(); + let show_queue_hint = matches!(footer_props.mode, FooterMode::ContextOnly) + && footer_props.is_task_running + && footer_props.steer_enabled; let context_line = context_window_line( footer_props.context_window_percent, footer_props.context_window_used_tokens, @@ -2534,7 +2540,12 @@ impl Renderable for ChatComposer { } else if let Some(items) = self.footer_hint_override.as_ref() { footer_hint_items_width(items) } else { - footer_line_width(footer_props, indicator, show_cycle_hint) + footer_line_width( + footer_props, + indicator, + show_cycle_hint, + show_shortcuts_hint, + ) }; let can_show_both = can_show_left_with_context(hint_rect, left_width, context_width); @@ -2547,6 +2558,18 @@ impl Renderable for ChatComposer { context_width, indicator, show_cycle_hint, + show_shortcuts_hint, + )) + } else if !has_override + && matches!(footer_props.mode, FooterMode::ContextOnly) + && !show_queue_hint + { + Some(shortcut_summary_layout( + hint_rect, + context_width, + indicator, + show_cycle_hint, + false, )) } else { None @@ -2559,7 +2582,14 @@ impl Renderable for ChatComposer { if let Some((summary_left, _)) = summary_layout { match summary_left { SummaryLeft::Default => { - render_footer(hint_rect, buf, footer_props, indicator, show_cycle_hint); + render_footer( + hint_rect, + buf, + footer_props, + indicator, + show_cycle_hint, + show_shortcuts_hint, + ); } SummaryLeft::Custom(line) => { render_footer_line(hint_rect, buf, line); @@ -2573,7 +2603,14 @@ impl Renderable for ChatComposer { } else if let Some(items) = self.footer_hint_override.as_ref() { render_footer_hint_items(hint_rect, buf, items); } else { - render_footer(hint_rect, buf, footer_props, indicator, show_cycle_hint); + render_footer( + hint_rect, + buf, + footer_props, + indicator, + show_cycle_hint, + show_shortcuts_hint, + ); } if show_context { diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 106fc7b63b56..345ecbb92897 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -130,7 +130,7 @@ pub(crate) fn reset_mode_after_activity(current: FooterMode) -> FooterMode { } pub(crate) fn footer_height(props: FooterProps) -> u16 { - footer_lines(props, None, false).len() as u16 + footer_lines(props, None, false, true).len() as u16 } pub(crate) fn render_footer( @@ -139,9 +139,10 @@ pub(crate) fn render_footer( props: FooterProps, indicator: Option, show_cycle_hint: bool, + show_shortcuts_hint: bool, ) { Paragraph::new(prefix_lines( - footer_lines(props, indicator, show_cycle_hint), + footer_lines(props, indicator, show_cycle_hint, show_shortcuts_hint), " ".repeat(FOOTER_INDENT_COLS).into(), " ".repeat(FOOTER_INDENT_COLS).into(), )) @@ -194,10 +195,11 @@ pub(crate) fn shortcut_summary_layout( context_width: u16, indicator: Option, show_cycle_hint: bool, + show_shortcuts_hint: bool, ) -> (SummaryLeft, bool) { - let default_line = shortcut_summary_line(indicator, show_cycle_hint, true); + let default_line = shortcut_summary_line(indicator, show_cycle_hint, show_shortcuts_hint); let default_width = default_line.width() as u16; - if can_show_left_with_context(area, default_width, context_width) { + if default_width > 0 && can_show_left_with_context(area, default_width, context_width) { return (SummaryLeft::Default, true); } @@ -303,6 +305,7 @@ fn footer_lines( props: FooterProps, indicator: Option, show_cycle_hint: bool, + show_shortcuts_hint: bool, ) -> Vec> { // Render left-side hints only. The context indicator is rendered // separately, right-aligned by the caller. @@ -311,15 +314,11 @@ fn footer_lines( vec![quit_shortcut_reminder_line(props.quit_shortcut_key)] } FooterMode::ShortcutSummary => { - let mut line = Line::from(vec![ - key_hint::plain(KeyCode::Char('?')).into(), - " for shortcuts".dim(), - ]); - if let Some(indicator) = indicator { - line.push_span(" · ".dim()); - line.push_span(indicator.styled_span(show_cycle_hint)); - } - vec![line] + vec![shortcut_summary_line( + indicator, + show_cycle_hint, + show_shortcuts_hint, + )] } FooterMode::ShortcutOverlay => { #[cfg(target_os = "linux")] @@ -337,12 +336,13 @@ fn footer_lines( } FooterMode::EscHint => vec![esc_hint_line(props.esc_backtrack_hint)], FooterMode::ContextOnly => { - let mut line = Line::from(""); if props.is_task_running && props.steer_enabled { - line.push_span(key_hint::plain(KeyCode::Tab)); - line.push_span(" to queue message".dim()); + return vec![Line::from(vec![ + key_hint::plain(KeyCode::Tab).into(), + " to queue message".dim(), + ])]; } - vec![line] + vec![shortcut_summary_line(indicator, show_cycle_hint, false)] } } } @@ -351,8 +351,9 @@ pub(crate) fn footer_line_width( props: FooterProps, indicator: Option, show_cycle_hint: bool, + show_shortcuts_hint: bool, ) -> u16 { - footer_lines(props, indicator, show_cycle_hint) + footer_lines(props, indicator, show_cycle_hint, show_shortcuts_hint) .last() .map(|line| line.width() as u16) .unwrap_or(0) @@ -733,14 +734,33 @@ mod tests { ); let context_width = context_line.width() as u16; let show_cycle_hint = !props.is_task_running; - let left_width = footer_line_width(props, None, show_cycle_hint); + let show_shortcuts_hint = matches!(props.mode, FooterMode::ShortcutSummary); + let show_queue_hint = matches!(props.mode, FooterMode::ContextOnly) + && props.is_task_running + && props.steer_enabled; + let left_width = + footer_line_width(props, None, show_cycle_hint, show_shortcuts_hint); let can_show_both = can_show_left_with_context(area, left_width, context_width); - if matches!(props.mode, FooterMode::ShortcutSummary) { - let (summary_left, show_context) = - shortcut_summary_layout(area, context_width, None, show_cycle_hint); + if matches!(props.mode, FooterMode::ShortcutSummary) + || (matches!(props.mode, FooterMode::ContextOnly) && !show_queue_hint) + { + let (summary_left, show_context) = shortcut_summary_layout( + area, + context_width, + None, + show_cycle_hint, + show_shortcuts_hint, + ); match summary_left { SummaryLeft::Default => { - render_footer(area, f.buffer_mut(), props, None, show_cycle_hint); + render_footer( + area, + f.buffer_mut(), + props, + None, + show_cycle_hint, + show_shortcuts_hint, + ); } SummaryLeft::Custom(line) => { render_footer_line(area, f.buffer_mut(), line); @@ -751,7 +771,14 @@ mod tests { render_context_right(area, f.buffer_mut(), &context_line); } } else { - render_footer(area, f.buffer_mut(), props, None, show_cycle_hint); + render_footer( + area, + f.buffer_mut(), + props, + None, + show_cycle_hint, + show_shortcuts_hint, + ); if can_show_both { render_context_right(area, f.buffer_mut(), &context_line); } @@ -778,14 +805,33 @@ mod tests { ); let context_width = context_line.width() as u16; let show_cycle_hint = !props.is_task_running; - let left_width = footer_line_width(props, indicator, show_cycle_hint); + let show_shortcuts_hint = matches!(props.mode, FooterMode::ShortcutSummary); + let show_queue_hint = matches!(props.mode, FooterMode::ContextOnly) + && props.is_task_running + && props.steer_enabled; + let left_width = + footer_line_width(props, indicator, show_cycle_hint, show_shortcuts_hint); let can_show_both = can_show_left_with_context(area, left_width, context_width); - if matches!(props.mode, FooterMode::ShortcutSummary) { - let (summary_left, show_context) = - shortcut_summary_layout(area, context_width, indicator, show_cycle_hint); + if matches!(props.mode, FooterMode::ShortcutSummary) + || (matches!(props.mode, FooterMode::ContextOnly) && !show_queue_hint) + { + let (summary_left, show_context) = shortcut_summary_layout( + area, + context_width, + indicator, + show_cycle_hint, + show_shortcuts_hint, + ); match summary_left { SummaryLeft::Default => { - render_footer(area, f.buffer_mut(), props, indicator, show_cycle_hint); + render_footer( + area, + f.buffer_mut(), + props, + indicator, + show_cycle_hint, + show_shortcuts_hint, + ); } SummaryLeft::Custom(line) => { render_footer_line(area, f.buffer_mut(), line); @@ -796,7 +842,14 @@ mod tests { render_context_right(area, f.buffer_mut(), &context_line); } } else { - render_footer(area, f.buffer_mut(), props, indicator, show_cycle_hint); + render_footer( + area, + f.buffer_mut(), + props, + indicator, + show_cycle_hint, + show_shortcuts_hint, + ); if can_show_both { render_context_right(area, f.buffer_mut(), &context_line); } From 539a46aa02d4edadb70d446f97d6101cb5f87cd6 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 16:50:06 -0800 Subject: [PATCH 03/26] Handle "tab to queue" tooltip, add snapshot tests --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 152 ++++++++-- codex-rs/tui/src/bottom_pane/footer.rs | 261 ++++++++++++++---- ...er__tests__footer_collapse_empty_full.snap | 13 + ...ollapse_empty_mode_cycle_with_context.snap | 13 + ...apse_empty_mode_cycle_without_context.snap | 13 + ...ests__footer_collapse_empty_mode_only.snap | 13 + ...er__tests__footer_collapse_queue_full.snap | 13 + ...ollapse_queue_message_without_context.snap | 13 + ...ests__footer_collapse_queue_mode_only.snap | 13 + ...ter_collapse_queue_short_with_context.snap | 13 + ..._collapse_queue_short_without_context.snap | 13 + 11 files changed, 452 insertions(+), 78 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_full.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_with_context.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_without_context.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_full.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_message_without_context.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_mode_only.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_with_context.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_without_context.snap diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 8089c97f182b..803a1eb6d14a 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2545,35 +2545,29 @@ impl Renderable for ChatComposer { indicator, show_cycle_hint, show_shortcuts_hint, + show_queue_hint, ) }; let can_show_both = can_show_left_with_context(hint_rect, left_width, context_width); let has_override = self.footer_flash_visible() || self.footer_hint_override.is_some(); - let summary_layout = - if !has_override && matches!(footer_props.mode, FooterMode::ShortcutSummary) { - Some(shortcut_summary_layout( - hint_rect, - context_width, - indicator, - show_cycle_hint, - show_shortcuts_hint, - )) - } else if !has_override - && matches!(footer_props.mode, FooterMode::ContextOnly) - && !show_queue_hint - { - Some(shortcut_summary_layout( - hint_rect, - context_width, - indicator, - show_cycle_hint, - false, - )) - } else { - None - }; + let summary_layout = if !has_override + && matches!( + footer_props.mode, + FooterMode::ShortcutSummary | FooterMode::ContextOnly + ) { + Some(shortcut_summary_layout( + hint_rect, + context_width, + indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + )) + } else { + None + }; let show_context = summary_layout .as_ref() .map(|(_, show_context)| *show_context) @@ -2589,6 +2583,7 @@ impl Renderable for ChatComposer { indicator, show_cycle_hint, show_shortcuts_hint, + show_queue_hint, ); } SummaryLeft::Custom(line) => { @@ -2610,6 +2605,7 @@ impl Renderable for ChatComposer { indicator, show_cycle_hint, show_shortcuts_hint, + show_queue_hint, ); } @@ -2864,14 +2860,17 @@ mod tests { ); } - fn snapshot_composer_state(name: &str, enhanced_keys_supported: bool, setup: F) - where + fn snapshot_composer_state_with_width( + name: &str, + width: u16, + enhanced_keys_supported: bool, + setup: F, + ) where F: FnOnce(&mut ChatComposer), { use ratatui::Terminal; use ratatui::backend::TestBackend; - let width = 100; let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); let mut composer = ChatComposer::new( @@ -2893,6 +2892,13 @@ mod tests { insta::assert_snapshot!(name, terminal.backend()); } + fn snapshot_composer_state(name: &str, enhanced_keys_supported: bool, setup: F) + where + F: FnOnce(&mut ChatComposer), + { + snapshot_composer_state_with_width(name, 100, enhanced_keys_supported, setup); + } + #[test] fn footer_mode_snapshots() { use crossterm::event::KeyCode; @@ -2945,6 +2951,100 @@ mod tests { }); } + #[test] + fn footer_collapse_snapshots() { + fn setup_collab_footer( + composer: &mut ChatComposer, + context_percent: i64, + indicator: CollaborationModeIndicator, + ) { + composer.set_collaboration_modes_enabled(true); + composer.set_collaboration_mode_indicator(Some(indicator)); + composer.set_context_window(Some(context_percent), None); + } + + // Empty textarea, agent idle: shortcuts hint can show, and cycle hint is available. + snapshot_composer_state_with_width("footer_collapse_empty_full", 120, true, |composer| { + setup_collab_footer(composer, 100, CollaborationModeIndicator::Code); + }); + snapshot_composer_state_with_width( + "footer_collapse_empty_mode_cycle_with_context", + 60, + true, + |composer| { + setup_collab_footer(composer, 100, CollaborationModeIndicator::Code); + }, + ); + snapshot_composer_state_with_width( + "footer_collapse_empty_mode_cycle_without_context", + 44, + true, + |composer| { + setup_collab_footer(composer, 100, CollaborationModeIndicator::Code); + }, + ); + snapshot_composer_state_with_width( + "footer_collapse_empty_mode_only", + 26, + true, + |composer| { + setup_collab_footer(composer, 100, CollaborationModeIndicator::Code); + }, + ); + + // Textarea has content, agent running, steer enabled: queue hint is shown. + snapshot_composer_state_with_width("footer_collapse_queue_full", 120, true, |composer| { + setup_collab_footer(composer, 98, CollaborationModeIndicator::Code); + composer.set_steer_enabled(true); + composer.set_task_running(true); + composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); + }); + snapshot_composer_state_with_width( + "footer_collapse_queue_short_with_context", + 50, + true, + |composer| { + setup_collab_footer(composer, 98, CollaborationModeIndicator::Code); + composer.set_steer_enabled(true); + composer.set_task_running(true); + composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); + }, + ); + snapshot_composer_state_with_width( + "footer_collapse_queue_message_without_context", + 40, + true, + |composer| { + setup_collab_footer(composer, 98, CollaborationModeIndicator::Code); + composer.set_steer_enabled(true); + composer.set_task_running(true); + composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); + }, + ); + snapshot_composer_state_with_width( + "footer_collapse_queue_short_without_context", + 30, + true, + |composer| { + setup_collab_footer(composer, 98, CollaborationModeIndicator::Code); + composer.set_steer_enabled(true); + composer.set_task_running(true); + composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); + }, + ); + snapshot_composer_state_with_width( + "footer_collapse_queue_mode_only", + 20, + true, + |composer| { + setup_collab_footer(composer, 98, CollaborationModeIndicator::Code); + composer.set_steer_enabled(true); + composer.set_task_running(true); + composer.set_text_content("Test".to_string(), Vec::new(), Vec::new()); + }, + ); + } + #[test] fn esc_hint_stays_hidden_with_draft_content() { use crossterm::event::KeyCode; diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 345ecbb92897..9a645a019af4 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -130,7 +130,7 @@ pub(crate) fn reset_mode_after_activity(current: FooterMode) -> FooterMode { } pub(crate) fn footer_height(props: FooterProps) -> u16 { - footer_lines(props, None, false, true).len() as u16 + footer_lines(props, None, false, true, false).len() as u16 } pub(crate) fn render_footer( @@ -140,9 +140,16 @@ pub(crate) fn render_footer( indicator: Option, show_cycle_hint: bool, show_shortcuts_hint: bool, + show_queue_hint: bool, ) { Paragraph::new(prefix_lines( - footer_lines(props, indicator, show_cycle_hint, show_shortcuts_hint), + footer_lines( + props, + indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ), " ".repeat(FOOTER_INDENT_COLS).into(), " ".repeat(FOOTER_INDENT_COLS).into(), )) @@ -163,22 +170,43 @@ pub(crate) fn left_fits(area: Rect, left_width: u16) -> bool { left_width <= max_width } -pub(crate) fn shortcut_summary_line( - indicator: Option, +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SummaryHintKind { + None, + Shortcuts, + QueueMessage, + QueueShort, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct SummarySpec { + hint: SummaryHintKind, show_cycle_hint: bool, - show_shortcuts_hint: bool, -) -> Line<'static> { +} + +fn summary_line(indicator: Option, spec: SummarySpec) -> Line<'static> { let mut line = Line::from(""); - if show_shortcuts_hint { - line.push_span(key_hint::plain(KeyCode::Char('?'))); - line.push_span(" for shortcuts".dim()); - } + match spec.hint { + SummaryHintKind::None => {} + SummaryHintKind::Shortcuts => { + line.push_span(key_hint::plain(KeyCode::Char('?'))); + line.push_span(" for shortcuts".dim()); + } + SummaryHintKind::QueueMessage => { + line.push_span(key_hint::plain(KeyCode::Tab)); + line.push_span(" to queue message".dim()); + } + SummaryHintKind::QueueShort => { + line.push_span(key_hint::plain(KeyCode::Tab)); + line.push_span(" to queue".dim()); + } + }; if let Some(indicator) = indicator { - if show_shortcuts_hint { + if !matches!(spec.hint, SummaryHintKind::None) { line.push_span(" · ".dim()); } - line.push_span(indicator.styled_span(show_cycle_hint)); + line.push_span(indicator.styled_span(spec.show_cycle_hint)); } line @@ -196,32 +224,129 @@ pub(crate) fn shortcut_summary_layout( indicator: Option, show_cycle_hint: bool, show_shortcuts_hint: bool, + show_queue_hint: bool, ) -> (SummaryLeft, bool) { - let default_line = shortcut_summary_line(indicator, show_cycle_hint, show_shortcuts_hint); + let hint_kind = if show_queue_hint { + SummaryHintKind::QueueMessage + } else if show_shortcuts_hint { + SummaryHintKind::Shortcuts + } else { + SummaryHintKind::None + }; + let default_spec = SummarySpec { + hint: hint_kind, + show_cycle_hint, + }; + let default_line = summary_line(indicator, default_spec); let default_width = default_line.width() as u16; if default_width > 0 && can_show_left_with_context(area, default_width, context_width) { return (SummaryLeft::Default, true); } - let Some(indicator) = indicator else { - return (SummaryLeft::None, true); + let spec_width = |spec: SummarySpec| -> u16 { + if spec == default_spec { + default_width + } else { + summary_line(indicator, spec).width() as u16 + } + }; + let spec_line = |spec: SummarySpec| -> Line<'static> { + if spec == default_spec { + default_line.clone() + } else { + summary_line(indicator, spec) + } }; - if show_cycle_hint { - let mode_with_cycle = shortcut_summary_line(Some(indicator), true, false); - let mode_with_cycle_width = mode_with_cycle.width() as u16; - if can_show_left_with_context(area, mode_with_cycle_width, context_width) { - return (SummaryLeft::Custom(mode_with_cycle), true); + if show_queue_hint { + // In queue mode, prefer dropping context before dropping the queue hint. + let queue_specs = [ + default_spec, + SummarySpec { + hint: SummaryHintKind::QueueMessage, + show_cycle_hint: false, + }, + SummarySpec { + hint: SummaryHintKind::QueueShort, + show_cycle_hint: false, + }, + ]; + + let mut previous_spec: Option = None; + for spec in queue_specs { + if previous_spec == Some(spec) { + continue; + } + previous_spec = Some(spec); + let width = spec_width(spec); + if width > 0 && can_show_left_with_context(area, width, context_width) { + if spec == default_spec { + return (SummaryLeft::Default, true); + } + return (SummaryLeft::Custom(spec_line(spec)), true); + } + } + + let mut previous_spec: Option = None; + for spec in queue_specs { + if previous_spec == Some(spec) { + continue; + } + previous_spec = Some(spec); + let width = spec_width(spec); + if width > 0 && left_fits(area, width) { + if spec == default_spec { + return (SummaryLeft::Default, false); + } + return (SummaryLeft::Custom(spec_line(spec)), false); + } + } + } else if indicator.is_some() { + if show_cycle_hint { + let cycle_spec = SummarySpec { + hint: SummaryHintKind::None, + show_cycle_hint: true, + }; + let cycle_width = spec_width(cycle_spec); + if cycle_width > 0 && can_show_left_with_context(area, cycle_width, context_width) { + return (SummaryLeft::Custom(spec_line(cycle_spec)), true); + } + if cycle_width > 0 && left_fits(area, cycle_width) { + return (SummaryLeft::Custom(spec_line(cycle_spec)), false); + } } - } - let mode_only = shortcut_summary_line(Some(indicator), false, false); - let mode_only_width = mode_only.width() as u16; - if can_show_left_with_context(area, mode_only_width, context_width) { - return (SummaryLeft::Custom(mode_only), true); + let mode_only_spec = SummarySpec { + hint: SummaryHintKind::None, + show_cycle_hint: false, + }; + let mode_only_width = spec_width(mode_only_spec); + if mode_only_width > 0 && can_show_left_with_context(area, mode_only_width, context_width) { + return (SummaryLeft::Custom(spec_line(mode_only_spec)), true); + } + if mode_only_width > 0 && left_fits(area, mode_only_width) { + return (SummaryLeft::Custom(spec_line(mode_only_spec)), false); + } } - if left_fits(area, mode_only_width) { - return (SummaryLeft::Custom(mode_only), false); + + if let Some(indicator) = indicator { + let mode_only_spec = SummarySpec { + hint: SummaryHintKind::None, + show_cycle_hint: false, + }; + let mode_only_width = summary_line(Some(indicator), mode_only_spec).width() as u16; + if can_show_left_with_context(area, mode_only_width, context_width) { + return ( + SummaryLeft::Custom(summary_line(Some(indicator), mode_only_spec)), + true, + ); + } + if left_fits(area, mode_only_width) { + return ( + SummaryLeft::Custom(summary_line(Some(indicator), mode_only_spec)), + false, + ); + } } (SummaryLeft::None, true) @@ -306,6 +431,7 @@ fn footer_lines( indicator: Option, show_cycle_hint: bool, show_shortcuts_hint: bool, + show_queue_hint: bool, ) -> Vec> { // Render left-side hints only. The context indicator is rendered // separately, right-aligned by the caller. @@ -314,11 +440,15 @@ fn footer_lines( vec![quit_shortcut_reminder_line(props.quit_shortcut_key)] } FooterMode::ShortcutSummary => { - vec![shortcut_summary_line( - indicator, + let spec = SummarySpec { + hint: if show_shortcuts_hint { + SummaryHintKind::Shortcuts + } else { + SummaryHintKind::None + }, show_cycle_hint, - show_shortcuts_hint, - )] + }; + vec![summary_line(indicator, spec)] } FooterMode::ShortcutOverlay => { #[cfg(target_os = "linux")] @@ -336,13 +466,15 @@ fn footer_lines( } FooterMode::EscHint => vec![esc_hint_line(props.esc_backtrack_hint)], FooterMode::ContextOnly => { - if props.is_task_running && props.steer_enabled { - return vec![Line::from(vec![ - key_hint::plain(KeyCode::Tab).into(), - " to queue message".dim(), - ])]; - } - vec![shortcut_summary_line(indicator, show_cycle_hint, false)] + let spec = SummarySpec { + hint: if show_queue_hint { + SummaryHintKind::QueueMessage + } else { + SummaryHintKind::None + }, + show_cycle_hint, + }; + vec![summary_line(indicator, spec)] } } } @@ -352,11 +484,18 @@ pub(crate) fn footer_line_width( indicator: Option, show_cycle_hint: bool, show_shortcuts_hint: bool, + show_queue_hint: bool, ) -> u16 { - footer_lines(props, indicator, show_cycle_hint, show_shortcuts_hint) - .last() - .map(|line| line.width() as u16) - .unwrap_or(0) + footer_lines( + props, + indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ) + .last() + .map(|line| line.width() as u16) + .unwrap_or(0) } pub(crate) fn footer_hint_items_width(items: &[(String, String)]) -> u16 { @@ -738,18 +877,25 @@ mod tests { let show_queue_hint = matches!(props.mode, FooterMode::ContextOnly) && props.is_task_running && props.steer_enabled; - let left_width = - footer_line_width(props, None, show_cycle_hint, show_shortcuts_hint); + let left_width = footer_line_width( + props, + None, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ); let can_show_both = can_show_left_with_context(area, left_width, context_width); - if matches!(props.mode, FooterMode::ShortcutSummary) - || (matches!(props.mode, FooterMode::ContextOnly) && !show_queue_hint) - { + if matches!( + props.mode, + FooterMode::ShortcutSummary | FooterMode::ContextOnly + ) { let (summary_left, show_context) = shortcut_summary_layout( area, context_width, None, show_cycle_hint, show_shortcuts_hint, + show_queue_hint, ); match summary_left { SummaryLeft::Default => { @@ -760,6 +906,7 @@ mod tests { None, show_cycle_hint, show_shortcuts_hint, + show_queue_hint, ); } SummaryLeft::Custom(line) => { @@ -778,6 +925,7 @@ mod tests { None, show_cycle_hint, show_shortcuts_hint, + show_queue_hint, ); if can_show_both { render_context_right(area, f.buffer_mut(), &context_line); @@ -809,18 +957,25 @@ mod tests { let show_queue_hint = matches!(props.mode, FooterMode::ContextOnly) && props.is_task_running && props.steer_enabled; - let left_width = - footer_line_width(props, indicator, show_cycle_hint, show_shortcuts_hint); + let left_width = footer_line_width( + props, + indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + ); let can_show_both = can_show_left_with_context(area, left_width, context_width); - if matches!(props.mode, FooterMode::ShortcutSummary) - || (matches!(props.mode, FooterMode::ContextOnly) && !show_queue_hint) - { + if matches!( + props.mode, + FooterMode::ShortcutSummary | FooterMode::ContextOnly + ) { let (summary_left, show_context) = shortcut_summary_layout( area, context_width, indicator, show_cycle_hint, show_shortcuts_hint, + show_queue_hint, ); match summary_left { SummaryLeft::Default => { @@ -831,6 +986,7 @@ mod tests { indicator, show_cycle_hint, show_shortcuts_hint, + show_queue_hint, ); } SummaryLeft::Custom(line) => { @@ -849,6 +1005,7 @@ mod tests { indicator, show_cycle_hint, show_shortcuts_hint, + show_queue_hint, ); if can_show_both { render_context_right(area, f.buffer_mut(), &context_line); diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_full.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_full.snap new file mode 100644 index 000000000000..90a7b12e4a38 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_full.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" ? for shortcuts · Code mode (shift+tab to cycle) 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_with_context.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_with_context.snap new file mode 100644 index 000000000000..cfc93be1e6c5 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_with_context.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" Code mode (shift+tab to cycle) 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_without_context.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_without_context.snap new file mode 100644 index 000000000000..9901a8fbbf2a --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_cycle_without_context.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" Code mode (shift+tab to cycle) " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap new file mode 100644 index 000000000000..98c7c3388919 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anythin " +" " +" " +" " +" " +" " +" " +" Code mode " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_full.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_full.snap new file mode 100644 index 000000000000..cfb47fa9d7de --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_full.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Test " +" " +" " +" " +" " +" " +" " +" tab to queue message · Code mode 98% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_message_without_context.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_message_without_context.snap new file mode 100644 index 000000000000..0a91488b5823 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_message_without_context.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Test " +" " +" " +" " +" " +" " +" " +" tab to queue message · Code mode " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_mode_only.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_mode_only.snap new file mode 100644 index 000000000000..9adcc6e05943 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_mode_only.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Test " +" " +" " +" " +" " +" " +" " +" Code mode " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_with_context.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_with_context.snap new file mode 100644 index 000000000000..091060261615 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_with_context.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Test " +" " +" " +" " +" " +" " +" " +" tab to queue · Code mode 98% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_without_context.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_without_context.snap new file mode 100644 index 000000000000..993dc3403562 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_queue_short_without_context.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Test " +" " +" " +" " +" " +" " +" " +" tab to queue · Code mode " From 39814827e07c99ba784cc27744f211f20d18e264 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 17:01:59 -0800 Subject: [PATCH 04/26] Don't show context % when it wasn't visible previously --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 15 +++++++++++---- codex-rs/tui/src/bottom_pane/footer.rs | 14 ++++++++++++-- ...oser__tests__footer_mode_ctrl_c_interrupt.snap | 2 +- ..._composer__tests__footer_mode_ctrl_c_quit.snap | 2 +- ...__tests__footer_mode_ctrl_c_then_esc_hint.snap | 2 +- ...er__tests__footer_mode_esc_hint_backtrack.snap | 2 +- ..._tests__footer_mode_esc_hint_from_overlay.snap | 2 +- ...ooter_mode_overlay_then_external_esc_hint.snap | 2 +- ...e__footer__tests__footer_ctrl_c_quit_idle.snap | 2 +- ...footer__tests__footer_ctrl_c_quit_running.snap | 2 +- ...pane__footer__tests__footer_esc_hint_idle.snap | 2 +- ...ne__footer__tests__footer_esc_hint_primed.snap | 2 +- ...ooter_mode_indicator_narrow_overlap_hides.snap | 2 +- 13 files changed, 34 insertions(+), 17 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 803a1eb6d14a..219d8b22cb59 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2568,10 +2568,17 @@ impl Renderable for ChatComposer { } else { None }; - let show_context = summary_layout - .as_ref() - .map(|(_, show_context)| *show_context) - .unwrap_or(can_show_both); + let show_context = if matches!( + footer_props.mode, + FooterMode::EscHint | FooterMode::QuitShortcutReminder + ) { + false + } else { + summary_layout + .as_ref() + .map(|(_, show_context)| *show_context) + .unwrap_or(can_show_both) + }; if let Some((summary_left, _)) = summary_layout { match summary_left { diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 9a645a019af4..352e66018484 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -927,7 +927,12 @@ mod tests { show_shortcuts_hint, show_queue_hint, ); - if can_show_both { + let show_context = can_show_both + && !matches!( + props.mode, + FooterMode::EscHint | FooterMode::QuitShortcutReminder + ); + if show_context { render_context_right(area, f.buffer_mut(), &context_line); } } @@ -1007,7 +1012,12 @@ mod tests { show_shortcuts_hint, show_queue_hint, ); - if can_show_both { + let show_context = can_show_both + && !matches!( + props.mode, + FooterMode::EscHint | FooterMode::QuitShortcutReminder + ); + if show_context { render_context_right(area, f.buffer_mut(), &context_line); } } diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap index 2edeebc1720b..7ecc5bba7196 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_interrupt.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" ctrl + c again to quit 100% context left " +" ctrl + c again to quit " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap index 2edeebc1720b..7ecc5bba7196 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_quit.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" ctrl + c again to quit 100% context left " +" ctrl + c again to quit " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap index e9b0d2b51af9..9cad17b86482 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_ctrl_c_then_esc_hint.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" esc esc to edit previous message 100% context left " +" esc esc to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap index 5b101f024b21..2fce42cc26b7 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_backtrack.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" esc again to edit previous message 100% context left " +" esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap index e9b0d2b51af9..9cad17b86482 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_esc_hint_from_overlay.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" esc esc to edit previous message 100% context left " +" esc esc to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap index 5b101f024b21..2fce42cc26b7 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_overlay_then_external_esc_hint.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" esc again to edit previous message 100% context left " +" esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap index 46c42df67f7a..31a1b743b8e3 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_idle.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" ctrl + c again to quit 100% context left " +" ctrl + c again to quit " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap index 46c42df67f7a..31a1b743b8e3 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_ctrl_c_quit_running.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" ctrl + c again to quit 100% context left " +" ctrl + c again to quit " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap index b6d064c6248c..b2333b025f6f 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_idle.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" esc esc to edit previous message 100% context left " +" esc esc to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap index 93af76783c73..20f9b178b4b8 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_esc_hint_primed.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" esc again to edit previous message 100% context left " +" esc again to edit previous message " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_narrow_overlap_hides.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_narrow_overlap_hides.snap index 8e1767b8a671..6266f43d0bb2 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_narrow_overlap_hides.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_mode_indicator_narrow_overlap_hides.snap @@ -2,4 +2,4 @@ source: tui/src/bottom_pane/footer.rs expression: terminal.backend() --- -" Plan mode 100% context left " +" Plan mode (shift+tab to cycle) " From 61043a914abfa3268726552e88a950e3bac3890c Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 17:12:52 -0800 Subject: [PATCH 05/26] Lint --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 12 ++++++++++++ ...oser__tests__footer_collapse_empty_mode_only.snap | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 219d8b22cb59..012de35b66c4 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -150,6 +150,8 @@ use std::collections::VecDeque; use std::path::PathBuf; use std::time::Duration; use std::time::Instant; +use textwrap::Options as TextwrapOptions; +use unicode_width::UnicodeWidthStr; fn windows_degraded_sandbox_active() -> bool { cfg!(target_os = "windows") @@ -2652,6 +2654,16 @@ impl Renderable for ChatComposer { let text_x = textarea_rect.x; let text_y = textarea_rect.y; let text_width = textarea_rect.width; + let max_width = text_width.max(1) as usize; + let text = if UnicodeWidthStr::width(text.as_str()) <= max_width { + text + } else { + let opts = TextwrapOptions::new(max_width).break_words(false); + textwrap::wrap(text.as_str(), opts) + .first() + .map(std::string::ToString::to_string) + .unwrap_or(text) + }; let placeholder = text.dim(); buf.set_span(text_x, text_y, &placeholder, text_width); } diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap index 98c7c3388919..7a1e21da275c 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap @@ -3,7 +3,7 @@ source: tui/src/bottom_pane/chat_composer.rs expression: terminal.backend() --- " " -"› Ask Codex to do anythin " +"› Ask Codex to do " " " " " " " From c51b14b735beb82c74df6c6161175e14bcc69839 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 17:24:09 -0800 Subject: [PATCH 06/26] Add comment --- codex-rs/tui/src/bottom_pane/footer.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 352e66018484..29a7b6b40140 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -451,6 +451,9 @@ fn footer_lines( vec![summary_line(indicator, spec)] } FooterMode::ShortcutOverlay => { + // Intentionally suppress the mode indicator here: the overlay is a + // transient "what can I do next?" state and we want the next action + // hints to take full priority. #[cfg(target_os = "linux")] let is_wsl = is_probably_wsl(); #[cfg(not(target_os = "linux"))] From b6e1dbb7dbcd087484b3a230ca853bc529181cdb Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 17:33:31 -0800 Subject: [PATCH 07/26] Don't show context % in shortcut overlay --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 4 +++- codex-rs/tui/src/bottom_pane/footer.rs | 8 ++++++-- ...hat_composer__tests__footer_mode_shortcut_overlay.snap | 2 +- ...ne__footer__tests__footer_shortcuts_shift_and_esc.snap | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 012de35b66c4..4ffa24d49c9a 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2572,7 +2572,9 @@ impl Renderable for ChatComposer { }; let show_context = if matches!( footer_props.mode, - FooterMode::EscHint | FooterMode::QuitShortcutReminder + FooterMode::EscHint + | FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay ) { false } else { diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 29a7b6b40140..e893ff8a51bf 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -933,7 +933,9 @@ mod tests { let show_context = can_show_both && !matches!( props.mode, - FooterMode::EscHint | FooterMode::QuitShortcutReminder + FooterMode::EscHint + | FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay ); if show_context { render_context_right(area, f.buffer_mut(), &context_line); @@ -1018,7 +1020,9 @@ mod tests { let show_context = can_show_both && !matches!( props.mode, - FooterMode::EscHint | FooterMode::QuitShortcutReminder + FooterMode::EscHint + | FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay ); if show_context { render_context_right(area, f.buffer_mut(), &context_line); diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap index 5be85fc63b70..8486a9ec6f37 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shortcut_overlay.snap @@ -15,4 +15,4 @@ expression: terminal.backend() " @ for file paths ctrl + v to paste images " " ctrl + g to edit in external editor esc again to edit previous message " " ctrl + c to exit " -" ctrl + t to view transcript 100% context left " +" ctrl + t to view transcript " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap index 11f782632393..c1f00d443774 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_shift_and_esc.snap @@ -7,4 +7,4 @@ expression: terminal.backend() " @ for file paths ctrl + v to paste images " " ctrl + g to edit in external editor esc again to edit previous message " " ctrl + c to exit " -" ctrl + t to view transcript 100% context left " +" ctrl + t to view transcript " From 8dcc6b12744d773355ed8ad2fe4f8f1ad6f627ba Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 17:48:44 -0800 Subject: [PATCH 08/26] More exhaustive matches --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 36 ++++++--- codex-rs/tui/src/bottom_pane/footer.rs | 74 ++++++++++++++----- ...mposer_has_draft_queue_hint_disabled.snap} | 0 ...omposer_has_draft_queue_hint_enabled.snap} | 0 4 files changed, 82 insertions(+), 28 deletions(-) rename codex-rs/tui/src/bottom_pane/snapshots/{codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_disabled.snap => codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_disabled.snap} (100%) rename codex-rs/tui/src/bottom_pane/snapshots/{codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_enabled.snap => codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_enabled.snap} (100%) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 4ffa24d49c9a..85d3d507f8ce 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2167,6 +2167,12 @@ impl ChatComposer { } } + /// Resolve the effective footer mode via a small priority waterfall. + /// + /// The stored mode (`self.footer_mode`) is treated as a base mode with + /// sticky transient overrides (Esc hint, overlay, quit reminder). Only + /// the default summary mode is demoted to `ComposerHasDraft` when the + /// buffer is non-empty. fn footer_mode(&self) -> FooterMode { match self.footer_mode { FooterMode::EscHint => FooterMode::EscHint, @@ -2178,7 +2184,7 @@ impl ChatComposer { FooterMode::ShortcutSummary if self.quit_shortcut_hint_visible() => { FooterMode::QuitShortcutReminder } - FooterMode::ShortcutSummary if !self.is_empty() => FooterMode::ContextOnly, + FooterMode::ShortcutSummary if !self.is_empty() => FooterMode::ComposerHasDraft, other => other, } } @@ -2508,12 +2514,22 @@ impl Renderable for ChatComposer { ActivePopup::None => { let footer_props = self.footer_props(); let show_cycle_hint = !footer_props.is_task_running; - let show_shortcuts_hint = matches!(footer_props.mode, FooterMode::ShortcutSummary) - && self.is_empty() - && !self.is_in_paste_burst(); - let show_queue_hint = matches!(footer_props.mode, FooterMode::ContextOnly) - && footer_props.is_task_running - && footer_props.steer_enabled; + let show_shortcuts_hint = match footer_props.mode { + FooterMode::ShortcutSummary => self.is_empty() && !self.is_in_paste_burst(), + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay + | FooterMode::EscHint + | FooterMode::ComposerHasDraft => false, + }; + let show_queue_hint = match footer_props.mode { + FooterMode::ComposerHasDraft => { + footer_props.is_task_running && footer_props.steer_enabled + } + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutSummary + | FooterMode::ShortcutOverlay + | FooterMode::EscHint => false, + }; let context_line = context_window_line( footer_props.context_window_percent, footer_props.context_window_used_tokens, @@ -2557,7 +2573,7 @@ impl Renderable for ChatComposer { let summary_layout = if !has_override && matches!( footer_props.mode, - FooterMode::ShortcutSummary | FooterMode::ContextOnly + FooterMode::ShortcutSummary | FooterMode::ComposerHasDraft ) { Some(shortcut_summary_layout( hint_rect, @@ -3159,7 +3175,7 @@ mod tests { type_chars_humanlike(&mut composer, &['h']); assert_eq!(composer.textarea.text(), "h"); - assert_eq!(composer.footer_mode(), FooterMode::ContextOnly); + assert_eq!(composer.footer_mode(), FooterMode::ComposerHasDraft); let (result, needs_redraw) = composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); @@ -3168,7 +3184,7 @@ mod tests { let _ = flush_after_paste_burst(&mut composer); assert_eq!(composer.textarea.text(), "h?"); assert_eq!(composer.footer_mode, FooterMode::ShortcutSummary); - assert_eq!(composer.footer_mode(), FooterMode::ContextOnly); + assert_eq!(composer.footer_mode(), FooterMode::ComposerHasDraft); } /// Behavior: while a paste-like burst is being captured, `?` must not toggle the shortcut diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index e893ff8a51bf..1209cff07c5a 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -92,10 +92,14 @@ impl CollaborationModeIndicator { pub(crate) enum FooterMode { /// Transient "press again to quit" reminder (Ctrl+C/Ctrl+D). QuitShortcutReminder, + /// Default single-line summary when the composer is idle/empty. ShortcutSummary, + /// Multi-line shortcut overlay shown after pressing `?`. ShortcutOverlay, + /// Transient "press Esc again" hint shown after the first Esc while idle. EscHint, - ContextOnly, + /// The composer contains a draft, so shortcut hints are suppressed. + ComposerHasDraft, } pub(crate) fn toggle_shortcut_mode(current: FooterMode, ctrl_c_hint: bool) -> FooterMode { @@ -124,13 +128,27 @@ pub(crate) fn reset_mode_after_activity(current: FooterMode) -> FooterMode { FooterMode::EscHint | FooterMode::ShortcutOverlay | FooterMode::QuitShortcutReminder - | FooterMode::ContextOnly => FooterMode::ShortcutSummary, + | FooterMode::ComposerHasDraft => FooterMode::ShortcutSummary, other => other, } } pub(crate) fn footer_height(props: FooterProps) -> u16 { - footer_lines(props, None, false, true, false).len() as u16 + let show_shortcuts_hint = match props.mode { + FooterMode::ShortcutSummary => true, + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay + | FooterMode::EscHint + | FooterMode::ComposerHasDraft => false, + }; + let show_queue_hint = match props.mode { + FooterMode::ComposerHasDraft => props.is_task_running && props.steer_enabled, + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutSummary + | FooterMode::ShortcutOverlay + | FooterMode::EscHint => false, + }; + footer_lines(props, None, false, show_shortcuts_hint, show_queue_hint).len() as u16 } pub(crate) fn render_footer( @@ -468,7 +486,7 @@ fn footer_lines( shortcut_overlay_lines(state) } FooterMode::EscHint => vec![esc_hint_line(props.esc_backtrack_hint)], - FooterMode::ContextOnly => { + FooterMode::ComposerHasDraft => { let spec = SummarySpec { hint: if show_queue_hint { SummaryHintKind::QueueMessage @@ -876,10 +894,20 @@ mod tests { ); let context_width = context_line.width() as u16; let show_cycle_hint = !props.is_task_running; - let show_shortcuts_hint = matches!(props.mode, FooterMode::ShortcutSummary); - let show_queue_hint = matches!(props.mode, FooterMode::ContextOnly) - && props.is_task_running - && props.steer_enabled; + let show_shortcuts_hint = match props.mode { + FooterMode::ShortcutSummary => true, + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay + | FooterMode::EscHint + | FooterMode::ComposerHasDraft => false, + }; + let show_queue_hint = match props.mode { + FooterMode::ComposerHasDraft => props.is_task_running && props.steer_enabled, + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutSummary + | FooterMode::ShortcutOverlay + | FooterMode::EscHint => false, + }; let left_width = footer_line_width( props, None, @@ -890,7 +918,7 @@ mod tests { let can_show_both = can_show_left_with_context(area, left_width, context_width); if matches!( props.mode, - FooterMode::ShortcutSummary | FooterMode::ContextOnly + FooterMode::ShortcutSummary | FooterMode::ComposerHasDraft ) { let (summary_left, show_context) = shortcut_summary_layout( area, @@ -963,10 +991,20 @@ mod tests { ); let context_width = context_line.width() as u16; let show_cycle_hint = !props.is_task_running; - let show_shortcuts_hint = matches!(props.mode, FooterMode::ShortcutSummary); - let show_queue_hint = matches!(props.mode, FooterMode::ContextOnly) - && props.is_task_running - && props.steer_enabled; + let show_shortcuts_hint = match props.mode { + FooterMode::ShortcutSummary => true, + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay + | FooterMode::EscHint + | FooterMode::ComposerHasDraft => false, + }; + let show_queue_hint = match props.mode { + FooterMode::ComposerHasDraft => props.is_task_running && props.steer_enabled, + FooterMode::QuitShortcutReminder + | FooterMode::ShortcutSummary + | FooterMode::ShortcutOverlay + | FooterMode::EscHint => false, + }; let left_width = footer_line_width( props, indicator, @@ -977,7 +1015,7 @@ mod tests { let can_show_both = can_show_left_with_context(area, left_width, context_width); if matches!( props.mode, - FooterMode::ShortcutSummary | FooterMode::ContextOnly + FooterMode::ShortcutSummary | FooterMode::ComposerHasDraft ) { let (summary_left, show_context) = shortcut_summary_layout( area, @@ -1171,9 +1209,9 @@ mod tests { ); snapshot_footer( - "footer_context_only_queue_hint_disabled", + "footer_composer_has_draft_queue_hint_disabled", FooterProps { - mode: FooterMode::ContextOnly, + mode: FooterMode::ComposerHasDraft, esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: true, @@ -1186,9 +1224,9 @@ mod tests { ); snapshot_footer( - "footer_context_only_queue_hint_enabled", + "footer_composer_has_draft_queue_hint_enabled", FooterProps { - mode: FooterMode::ContextOnly, + mode: FooterMode::ComposerHasDraft, esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: true, diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_disabled.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_disabled.snap similarity index 100% rename from codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_disabled.snap rename to codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_disabled.snap diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_enabled.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_enabled.snap similarity index 100% rename from codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_context_only_queue_hint_enabled.snap rename to codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_composer_has_draft_queue_hint_enabled.snap From a4806a46deecf773419d718d3f3a38b7c1fdd661 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 17:58:56 -0800 Subject: [PATCH 09/26] Naming tweak --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 4 ++-- codex-rs/tui/src/bottom_pane/footer.rs | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 85d3d507f8ce..ecf3816c94e9 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2566,7 +2566,7 @@ impl Renderable for ChatComposer { show_queue_hint, ) }; - let can_show_both = + let can_show_left_and_context = can_show_left_with_context(hint_rect, left_width, context_width); let has_override = self.footer_flash_visible() || self.footer_hint_override.is_some(); @@ -2597,7 +2597,7 @@ impl Renderable for ChatComposer { summary_layout .as_ref() .map(|(_, show_context)| *show_context) - .unwrap_or(can_show_both) + .unwrap_or(can_show_left_and_context) }; if let Some((summary_left, _)) = summary_layout { diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 1209cff07c5a..3a987fd853d1 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -915,7 +915,8 @@ mod tests { show_shortcuts_hint, show_queue_hint, ); - let can_show_both = can_show_left_with_context(area, left_width, context_width); + let can_show_left_and_context = + can_show_left_with_context(area, left_width, context_width); if matches!( props.mode, FooterMode::ShortcutSummary | FooterMode::ComposerHasDraft @@ -958,7 +959,7 @@ mod tests { show_shortcuts_hint, show_queue_hint, ); - let show_context = can_show_both + let show_context = can_show_left_and_context && !matches!( props.mode, FooterMode::EscHint @@ -1012,7 +1013,8 @@ mod tests { show_shortcuts_hint, show_queue_hint, ); - let can_show_both = can_show_left_with_context(area, left_width, context_width); + let can_show_left_and_context = + can_show_left_with_context(area, left_width, context_width); if matches!( props.mode, FooterMode::ShortcutSummary | FooterMode::ComposerHasDraft @@ -1055,7 +1057,7 @@ mod tests { show_shortcuts_hint, show_queue_hint, ); - let show_context = can_show_both + let show_context = can_show_left_and_context && !matches!( props.mode, FooterMode::EscHint From e0e9fcd807b6123b38a7907406b951472ea57fca Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 18:50:22 -0800 Subject: [PATCH 10/26] Improve code readability --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 41 +++++++++++-------- codex-rs/tui/src/bottom_pane/footer.rs | 8 ++-- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index ecf3816c94e9..6e35226e304e 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -107,7 +107,7 @@ use super::footer::render_footer; use super::footer::render_footer_hint_items; use super::footer::render_footer_line; use super::footer::reset_mode_after_activity; -use super::footer::shortcut_summary_layout; +use super::footer::single_line_footer_layout; use super::footer::toggle_shortcut_mode; use super::paste_burst::CharDecision; use super::paste_burst::PasteBurst; @@ -2570,21 +2570,28 @@ impl Renderable for ChatComposer { can_show_left_with_context(hint_rect, left_width, context_width); let has_override = self.footer_flash_visible() || self.footer_hint_override.is_some(); - let summary_layout = if !has_override - && matches!( - footer_props.mode, - FooterMode::ShortcutSummary | FooterMode::ComposerHasDraft - ) { - Some(shortcut_summary_layout( - hint_rect, - context_width, - indicator, - show_cycle_hint, - show_shortcuts_hint, - show_queue_hint, - )) - } else { + let single_line_layout = if has_override { None + } else { + match footer_props.mode { + FooterMode::ShortcutSummary | FooterMode::ComposerHasDraft => { + // Both of these modes render the single-line footer style (with + // either the shortcuts hint or the optional queue hint). We still + // want the single-line collapse rules so the mode label can win over + // the context indicator on narrow widths. + Some(single_line_footer_layout( + hint_rect, + context_width, + indicator, + show_cycle_hint, + show_shortcuts_hint, + show_queue_hint, + )) + } + FooterMode::EscHint + | FooterMode::QuitShortcutReminder + | FooterMode::ShortcutOverlay => None, + } }; let show_context = if matches!( footer_props.mode, @@ -2594,13 +2601,13 @@ impl Renderable for ChatComposer { ) { false } else { - summary_layout + single_line_layout .as_ref() .map(|(_, show_context)| *show_context) .unwrap_or(can_show_left_and_context) }; - if let Some((summary_left, _)) = summary_layout { + if let Some((summary_left, _)) = single_line_layout { match summary_left { SummaryLeft::Default => { render_footer( diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 3a987fd853d1..e940776b6227 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -236,7 +236,9 @@ pub(crate) enum SummaryLeft { None, } -pub(crate) fn shortcut_summary_layout( +/// Compute the single-line footer layout and whether the right-side context +/// indicator can be shown alongside it. +pub(crate) fn single_line_footer_layout( area: Rect, context_width: u16, indicator: Option, @@ -921,7 +923,7 @@ mod tests { props.mode, FooterMode::ShortcutSummary | FooterMode::ComposerHasDraft ) { - let (summary_left, show_context) = shortcut_summary_layout( + let (summary_left, show_context) = single_line_footer_layout( area, context_width, None, @@ -1019,7 +1021,7 @@ mod tests { props.mode, FooterMode::ShortcutSummary | FooterMode::ComposerHasDraft ) { - let (summary_left, show_context) = shortcut_summary_layout( + let (summary_left, show_context) = single_line_footer_layout( area, context_width, indicator, From 30d6fedbcabd6d3ecdaf6edb5f77d08895a50e5b Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 22:39:24 -0800 Subject: [PATCH 11/26] Improve naming --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 77 +++++++++++++------ codex-rs/tui/src/bottom_pane/footer.rs | 43 ++++++----- 2 files changed, 75 insertions(+), 45 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 6e35226e304e..5f5bf858facd 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -292,7 +292,7 @@ impl ChatComposer { paste_burst: PasteBurst::default(), disable_paste_burst: false, custom_prompts: Vec::new(), - footer_mode: FooterMode::ShortcutSummary, + footer_mode: FooterMode::ComposerEmpty, footer_hint_override: None, footer_flash: None, context_window_percent: None, @@ -2169,24 +2169,28 @@ impl ChatComposer { /// Resolve the effective footer mode via a small priority waterfall. /// - /// The stored mode (`self.footer_mode`) is treated as a base mode with - /// sticky transient overrides (Esc hint, overlay, quit reminder). Only - /// the default summary mode is demoted to `ComposerHasDraft` when the - /// buffer is non-empty. + /// The base mode is derived solely from whether the composer is empty: + /// `ComposerEmpty` iff empty, otherwise `ComposerHasDraft`. Transient + /// modes (Esc hint, overlay, quit reminder) can override that base when + /// their conditions are active. fn footer_mode(&self) -> FooterMode { - match self.footer_mode { - FooterMode::EscHint => FooterMode::EscHint, - FooterMode::ShortcutOverlay => FooterMode::ShortcutOverlay, - FooterMode::QuitShortcutReminder if self.quit_shortcut_hint_visible() => { - FooterMode::QuitShortcutReminder - } - FooterMode::QuitShortcutReminder => FooterMode::ShortcutSummary, - FooterMode::ShortcutSummary if self.quit_shortcut_hint_visible() => { - FooterMode::QuitShortcutReminder - } - FooterMode::ShortcutSummary if !self.is_empty() => FooterMode::ComposerHasDraft, - other => other, + let base = if self.is_empty() { + FooterMode::ComposerEmpty + } else { + FooterMode::ComposerHasDraft + }; + + if matches!(self.footer_mode, FooterMode::EscHint) && self.is_empty() { + return FooterMode::EscHint; + } + if matches!(self.footer_mode, FooterMode::ShortcutOverlay) && self.is_empty() { + return FooterMode::ShortcutOverlay; + } + if self.quit_shortcut_hint_visible() { + return FooterMode::QuitShortcutReminder; } + + base } fn custom_footer_height(&self) -> Option { @@ -2515,7 +2519,7 @@ impl Renderable for ChatComposer { let footer_props = self.footer_props(); let show_cycle_hint = !footer_props.is_task_running; let show_shortcuts_hint = match footer_props.mode { - FooterMode::ShortcutSummary => self.is_empty() && !self.is_in_paste_burst(), + FooterMode::ComposerEmpty => self.is_empty() && !self.is_in_paste_burst(), FooterMode::QuitShortcutReminder | FooterMode::ShortcutOverlay | FooterMode::EscHint @@ -2526,7 +2530,7 @@ impl Renderable for ChatComposer { footer_props.is_task_running && footer_props.steer_enabled } FooterMode::QuitShortcutReminder - | FooterMode::ShortcutSummary + | FooterMode::ComposerEmpty | FooterMode::ShortcutOverlay | FooterMode::EscHint => false, }; @@ -2574,7 +2578,7 @@ impl Renderable for ChatComposer { None } else { match footer_props.mode { - FooterMode::ShortcutSummary | FooterMode::ComposerHasDraft => { + FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft => { // Both of these modes render the single-line footer style (with // either the shortcuts hint or the optional queue hint). We still // want the single-line collapse rules so the mode label can win over @@ -3109,15 +3113,40 @@ mod tests { assert!(!composer.is_empty()); assert_eq!(composer.current_text(), "d"); - assert_eq!(composer.footer_mode, FooterMode::ShortcutSummary); + assert_eq!(composer.footer_mode, FooterMode::ComposerEmpty); 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_eq!(composer.footer_mode, FooterMode::ComposerEmpty); assert!(!composer.esc_backtrack_hint); } + #[test] + fn base_footer_mode_tracks_empty_state_after_quit_hint_expires() { + use crossterm::event::KeyCode; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + type_chars_humanlike(&mut composer, &['d']); + composer.show_quit_shortcut_hint(key_hint::ctrl(KeyCode::Char('c')), true); + composer.quit_shortcut_expires_at = + Some(Instant::now() - std::time::Duration::from_secs(1)); + + assert_eq!(composer.footer_mode(), FooterMode::ComposerHasDraft); + + composer.set_text_content(String::new(), Vec::new(), Vec::new()); + assert_eq!(composer.footer_mode(), FooterMode::ComposerEmpty); + } + #[test] fn clear_for_ctrl_c_records_cleared_draft() { let (tx, _rx) = unbounded_channel::(); @@ -3178,7 +3207,7 @@ mod tests { // Toggle back to prompt mode so subsequent typing captures characters. let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)); - assert_eq!(composer.footer_mode, FooterMode::ShortcutSummary); + assert_eq!(composer.footer_mode, FooterMode::ComposerEmpty); type_chars_humanlike(&mut composer, &['h']); assert_eq!(composer.textarea.text(), "h"); @@ -3190,7 +3219,7 @@ mod tests { assert!(needs_redraw, "typing should still mark the view dirty"); let _ = flush_after_paste_burst(&mut composer); assert_eq!(composer.textarea.text(), "h?"); - assert_eq!(composer.footer_mode, FooterMode::ShortcutSummary); + assert_eq!(composer.footer_mode, FooterMode::ComposerEmpty); assert_eq!(composer.footer_mode(), FooterMode::ComposerHasDraft); } diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index e940776b6227..debf5fb8774e 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -92,13 +92,16 @@ impl CollaborationModeIndicator { pub(crate) enum FooterMode { /// Transient "press again to quit" reminder (Ctrl+C/Ctrl+D). QuitShortcutReminder, - /// Default single-line summary when the composer is idle/empty. - ShortcutSummary, + /// Base single-line footer when the composer is empty. + ComposerEmpty, /// Multi-line shortcut overlay shown after pressing `?`. ShortcutOverlay, /// Transient "press Esc again" hint shown after the first Esc while idle. EscHint, - /// The composer contains a draft, so shortcut hints are suppressed. + /// Base single-line footer when the composer contains a draft. + /// + /// The shortcuts hint is suppressed here; when a task is running with + /// steer enabled, this mode can show the queue hint instead. ComposerHasDraft, } @@ -108,9 +111,7 @@ pub(crate) fn toggle_shortcut_mode(current: FooterMode, ctrl_c_hint: bool) -> Fo } match current { - FooterMode::ShortcutOverlay | FooterMode::QuitShortcutReminder => { - FooterMode::ShortcutSummary - } + FooterMode::ShortcutOverlay | FooterMode::QuitShortcutReminder => FooterMode::ComposerEmpty, _ => FooterMode::ShortcutOverlay, } } @@ -128,14 +129,14 @@ pub(crate) fn reset_mode_after_activity(current: FooterMode) -> FooterMode { FooterMode::EscHint | FooterMode::ShortcutOverlay | FooterMode::QuitShortcutReminder - | FooterMode::ComposerHasDraft => FooterMode::ShortcutSummary, + | FooterMode::ComposerHasDraft => FooterMode::ComposerEmpty, other => other, } } pub(crate) fn footer_height(props: FooterProps) -> u16 { let show_shortcuts_hint = match props.mode { - FooterMode::ShortcutSummary => true, + FooterMode::ComposerEmpty => true, FooterMode::QuitShortcutReminder | FooterMode::ShortcutOverlay | FooterMode::EscHint @@ -144,7 +145,7 @@ pub(crate) fn footer_height(props: FooterProps) -> u16 { let show_queue_hint = match props.mode { FooterMode::ComposerHasDraft => props.is_task_running && props.steer_enabled, FooterMode::QuitShortcutReminder - | FooterMode::ShortcutSummary + | FooterMode::ComposerEmpty | FooterMode::ShortcutOverlay | FooterMode::EscHint => false, }; @@ -459,7 +460,7 @@ fn footer_lines( FooterMode::QuitShortcutReminder => { vec![quit_shortcut_reminder_line(props.quit_shortcut_key)] } - FooterMode::ShortcutSummary => { + FooterMode::ComposerEmpty => { let spec = SummarySpec { hint: if show_shortcuts_hint { SummaryHintKind::Shortcuts @@ -897,7 +898,7 @@ mod tests { let context_width = context_line.width() as u16; let show_cycle_hint = !props.is_task_running; let show_shortcuts_hint = match props.mode { - FooterMode::ShortcutSummary => true, + FooterMode::ComposerEmpty => true, FooterMode::QuitShortcutReminder | FooterMode::ShortcutOverlay | FooterMode::EscHint @@ -906,7 +907,7 @@ mod tests { let show_queue_hint = match props.mode { FooterMode::ComposerHasDraft => props.is_task_running && props.steer_enabled, FooterMode::QuitShortcutReminder - | FooterMode::ShortcutSummary + | FooterMode::ComposerEmpty | FooterMode::ShortcutOverlay | FooterMode::EscHint => false, }; @@ -921,7 +922,7 @@ mod tests { can_show_left_with_context(area, left_width, context_width); if matches!( props.mode, - FooterMode::ShortcutSummary | FooterMode::ComposerHasDraft + FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft ) { let (summary_left, show_context) = single_line_footer_layout( area, @@ -995,7 +996,7 @@ mod tests { let context_width = context_line.width() as u16; let show_cycle_hint = !props.is_task_running; let show_shortcuts_hint = match props.mode { - FooterMode::ShortcutSummary => true, + FooterMode::ComposerEmpty => true, FooterMode::QuitShortcutReminder | FooterMode::ShortcutOverlay | FooterMode::EscHint @@ -1004,7 +1005,7 @@ mod tests { let show_queue_hint = match props.mode { FooterMode::ComposerHasDraft => props.is_task_running && props.steer_enabled, FooterMode::QuitShortcutReminder - | FooterMode::ShortcutSummary + | FooterMode::ComposerEmpty | FooterMode::ShortcutOverlay | FooterMode::EscHint => false, }; @@ -1019,7 +1020,7 @@ mod tests { can_show_left_with_context(area, left_width, context_width); if matches!( props.mode, - FooterMode::ShortcutSummary | FooterMode::ComposerHasDraft + FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft ) { let (summary_left, show_context) = single_line_footer_layout( area, @@ -1080,7 +1081,7 @@ mod tests { snapshot_footer( "footer_shortcuts_default", FooterProps { - mode: FooterMode::ShortcutSummary, + mode: FooterMode::ComposerEmpty, esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, @@ -1185,7 +1186,7 @@ mod tests { snapshot_footer( "footer_shortcuts_context_running", FooterProps { - mode: FooterMode::ShortcutSummary, + mode: FooterMode::ComposerEmpty, esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: true, @@ -1200,7 +1201,7 @@ mod tests { snapshot_footer( "footer_context_tokens_used", FooterProps { - mode: FooterMode::ShortcutSummary, + mode: FooterMode::ComposerEmpty, esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, @@ -1243,7 +1244,7 @@ mod tests { ); let props = FooterProps { - mode: FooterMode::ShortcutSummary, + mode: FooterMode::ComposerEmpty, esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, @@ -1269,7 +1270,7 @@ mod tests { ); let props = FooterProps { - mode: FooterMode::ShortcutSummary, + mode: FooterMode::ComposerEmpty, esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: true, From b7a2510664772f86368699d5fca004507fbc1cbf Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 22:55:35 -0800 Subject: [PATCH 12/26] Reduce diff --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 5f5bf858facd..186b1a256e00 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2174,23 +2174,21 @@ impl ChatComposer { /// modes (Esc hint, overlay, quit reminder) can override that base when /// their conditions are active. fn footer_mode(&self) -> FooterMode { - let base = if self.is_empty() { + let base_mode = if self.is_empty() { FooterMode::ComposerEmpty } else { FooterMode::ComposerHasDraft }; - if matches!(self.footer_mode, FooterMode::EscHint) && self.is_empty() { - return FooterMode::EscHint; - } - if matches!(self.footer_mode, FooterMode::ShortcutOverlay) && self.is_empty() { - return FooterMode::ShortcutOverlay; - } - if self.quit_shortcut_hint_visible() { - return FooterMode::QuitShortcutReminder; + match self.footer_mode { + FooterMode::EscHint => FooterMode::EscHint, + FooterMode::ShortcutOverlay => FooterMode::ShortcutOverlay, + FooterMode::QuitShortcutReminder if self.quit_shortcut_hint_visible() => { + FooterMode::QuitShortcutReminder + } + FooterMode::QuitShortcutReminder => base_mode, + FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft => base_mode, } - - base } fn custom_footer_height(&self) -> Option { From 818126aa4cdc2547564caab8620835960fc88861 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 23:04:53 -0800 Subject: [PATCH 13/26] Fix bug --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 186b1a256e00..5183925a82b6 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2186,6 +2186,11 @@ impl ChatComposer { FooterMode::QuitShortcutReminder if self.quit_shortcut_hint_visible() => { FooterMode::QuitShortcutReminder } + FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft + if self.quit_shortcut_hint_visible() => + { + FooterMode::QuitShortcutReminder + } FooterMode::QuitShortcutReminder => base_mode, FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft => base_mode, } From f38ed5cd3f0d4f3b772776074a6b5a5ff9f9169a Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 23:14:13 -0800 Subject: [PATCH 14/26] Code quality --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 5183925a82b6..e18a14bc0a3f 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2522,7 +2522,7 @@ impl Renderable for ChatComposer { let footer_props = self.footer_props(); let show_cycle_hint = !footer_props.is_task_running; let show_shortcuts_hint = match footer_props.mode { - FooterMode::ComposerEmpty => self.is_empty() && !self.is_in_paste_burst(), + FooterMode::ComposerEmpty => !self.is_in_paste_burst(), FooterMode::QuitShortcutReminder | FooterMode::ShortcutOverlay | FooterMode::EscHint @@ -2556,7 +2556,6 @@ impl Renderable for ChatComposer { } else { popup_rect }; - let indicator = self.collaboration_mode_indicator; let left_width = if self.footer_flash_visible() { self.footer_flash .as_ref() @@ -2567,7 +2566,7 @@ impl Renderable for ChatComposer { } else { footer_line_width( footer_props, - indicator, + self.collaboration_mode_indicator, show_cycle_hint, show_shortcuts_hint, show_queue_hint, @@ -2589,7 +2588,7 @@ impl Renderable for ChatComposer { Some(single_line_footer_layout( hint_rect, context_width, - indicator, + self.collaboration_mode_indicator, show_cycle_hint, show_shortcuts_hint, show_queue_hint, @@ -2621,7 +2620,7 @@ impl Renderable for ChatComposer { hint_rect, buf, footer_props, - indicator, + self.collaboration_mode_indicator, show_cycle_hint, show_shortcuts_hint, show_queue_hint, @@ -2643,7 +2642,7 @@ impl Renderable for ChatComposer { hint_rect, buf, footer_props, - indicator, + self.collaboration_mode_indicator, show_cycle_hint, show_shortcuts_hint, show_queue_hint, From e1057c2a4b8ba2d272d504d1246c61473b8eab9d Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 23:27:43 -0800 Subject: [PATCH 15/26] Simplification --- .codespellrc | 2 +- codex-rs/tui/src/bottom_pane/chat_composer.rs | 20 +++---------------- codex-rs/tui/src/bottom_pane/footer.rs | 8 ++++---- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/.codespellrc b/.codespellrc index 84b4495e310d..a3f0cd501adc 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,6 +1,6 @@ [codespell] # Ref: https://github.com/codespell-project/codespell#using-a-config-file -skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt +skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt,*.snap,*.snap.new check-hidden = true ignore-regex = ^\s*"image/\S+": ".*|\b(afterAll)\b ignore-words-list = ratatui,ser,iTerm,iterm2,iterm diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index e18a14bc0a3f..76b8bffbcb07 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -150,8 +150,6 @@ use std::collections::VecDeque; use std::path::PathBuf; use std::time::Duration; use std::time::Instant; -use textwrap::Options as TextwrapOptions; -use unicode_width::UnicodeWidthStr; fn windows_degraded_sandbox_active() -> bool { cfg!(target_os = "windows") @@ -2682,21 +2680,9 @@ impl Renderable for ChatComposer { .to_string() }; if !textarea_rect.is_empty() { - let text_x = textarea_rect.x; - let text_y = textarea_rect.y; - let text_width = textarea_rect.width; - let max_width = text_width.max(1) as usize; - let text = if UnicodeWidthStr::width(text.as_str()) <= max_width { - text - } else { - let opts = TextwrapOptions::new(max_width).break_words(false); - textwrap::wrap(text.as_str(), opts) - .first() - .map(std::string::ToString::to_string) - .unwrap_or(text) - }; - let placeholder = text.dim(); - buf.set_span(text_x, text_y, &placeholder, text_width); + let placeholder = Span::from(text).dim(); + Line::from(vec![placeholder]) + .render_ref(textarea_rect.inner(Margin::new(0, 0)), buf); } } } diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index debf5fb8774e..814c59429485 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -94,15 +94,15 @@ pub(crate) enum FooterMode { QuitShortcutReminder, /// Base single-line footer when the composer is empty. ComposerEmpty, - /// Multi-line shortcut overlay shown after pressing `?`. - ShortcutOverlay, - /// Transient "press Esc again" hint shown after the first Esc while idle. - EscHint, /// Base single-line footer when the composer contains a draft. /// /// The shortcuts hint is suppressed here; when a task is running with /// steer enabled, this mode can show the queue hint instead. ComposerHasDraft, + /// Multi-line shortcut overlay shown after pressing `?`. + ShortcutOverlay, + /// Transient "press Esc again" hint shown after the first Esc while idle. + EscHint, } pub(crate) fn toggle_shortcut_mode(current: FooterMode, ctrl_c_hint: bool) -> FooterMode { From 5ba35b0392f792cdfbf748484d97766ffe4e6b18 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 23:30:59 -0800 Subject: [PATCH 16/26] Lint --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 76b8bffbcb07..129903308e2f 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -79,9 +79,11 @@ use crossterm::event::KeyModifiers; use ratatui::buffer::Buffer; use ratatui::layout::Constraint; use ratatui::layout::Layout; +use ratatui::layout::Margin; use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; +use ratatui::text::Span; use ratatui::widgets::Block; use ratatui::widgets::StatefulWidgetRef; use ratatui::widgets::WidgetRef; From adcba89379184aafc8df1fef1548c09919d12c3a Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 26 Jan 2026 23:39:58 -0800 Subject: [PATCH 17/26] Simplify waterfall --- codex-rs/tui/src/bottom_pane/footer.rs | 16 ++++++++++++++-- ...__tests__footer_collapse_empty_mode_only.snap | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 814c59429485..8ee7b7b91fd3 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -278,6 +278,10 @@ pub(crate) fn single_line_footer_layout( summary_line(indicator, spec) } }; + // When the mode cycle hint is applicable (idle, non-queue mode), + // show the right-side context indicator if the "(shift+tab to cycle)" + // variant can also fit. + let context_requires_cycle_hint = show_cycle_hint && !show_queue_hint; if show_queue_hint { // In queue mode, prefer dropping context before dropping the queue hint. @@ -342,7 +346,13 @@ pub(crate) fn single_line_footer_layout( show_cycle_hint: false, }; let mode_only_width = spec_width(mode_only_spec); - if mode_only_width > 0 && can_show_left_with_context(area, mode_only_width, context_width) { + // When the mode cycle hint is applicable, only show context % if the mode cycle hint + // itself can also fit. This prevents the right-side context indicator from + // reappearing after we've already dropped "(shift+tab to cycle)". + if !context_requires_cycle_hint + && mode_only_width > 0 + && can_show_left_with_context(area, mode_only_width, context_width) + { return (SummaryLeft::Custom(spec_line(mode_only_spec)), true); } if mode_only_width > 0 && left_fits(area, mode_only_width) { @@ -356,7 +366,9 @@ pub(crate) fn single_line_footer_layout( show_cycle_hint: false, }; let mode_only_width = summary_line(Some(indicator), mode_only_spec).width() as u16; - if can_show_left_with_context(area, mode_only_width, context_width) { + if !context_requires_cycle_hint + && can_show_left_with_context(area, mode_only_width, context_width) + { return ( SummaryLeft::Custom(summary_line(Some(indicator), mode_only_spec)), true, diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap index 7a1e21da275c..98c7c3388919 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_collapse_empty_mode_only.snap @@ -3,7 +3,7 @@ source: tui/src/bottom_pane/chat_composer.rs expression: terminal.backend() --- " " -"› Ask Codex to do " +"› Ask Codex to do anythin " " " " " " " From aeabe93f0042e7b57eb6da5cdaf6e369dcd86576 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 27 Jan 2026 00:10:36 -0800 Subject: [PATCH 18/26] Code quality --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 6 +- codex-rs/tui/src/bottom_pane/footer.rs | 58 +++++++++++++------ 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 129903308e2f..aa7fc2932156 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2147,7 +2147,11 @@ impl ChatComposer { return false; } - let next = toggle_shortcut_mode(self.footer_mode, self.quit_shortcut_hint_visible()); + let next = toggle_shortcut_mode( + self.footer_mode, + self.quit_shortcut_hint_visible(), + self.is_empty(), + ); let changed = next != self.footer_mode; self.footer_mode = next; changed diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 8ee7b7b91fd3..a81ff7bf63a2 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -92,6 +92,10 @@ impl CollaborationModeIndicator { pub(crate) enum FooterMode { /// Transient "press again to quit" reminder (Ctrl+C/Ctrl+D). QuitShortcutReminder, + /// Multi-line shortcut overlay shown after pressing `?`. + ShortcutOverlay, + /// Transient "press Esc again" hint shown after the first Esc while idle. + EscHint, /// Base single-line footer when the composer is empty. ComposerEmpty, /// Base single-line footer when the composer contains a draft. @@ -99,19 +103,25 @@ pub(crate) enum FooterMode { /// The shortcuts hint is suppressed here; when a task is running with /// steer enabled, this mode can show the queue hint instead. ComposerHasDraft, - /// Multi-line shortcut overlay shown after pressing `?`. - ShortcutOverlay, - /// Transient "press Esc again" hint shown after the first Esc while idle. - EscHint, } -pub(crate) fn toggle_shortcut_mode(current: FooterMode, ctrl_c_hint: bool) -> FooterMode { +pub(crate) fn toggle_shortcut_mode( + current: FooterMode, + ctrl_c_hint: bool, + is_empty: bool, +) -> FooterMode { if ctrl_c_hint && matches!(current, FooterMode::QuitShortcutReminder) { return current; } + let base_mode = if is_empty { + FooterMode::ComposerEmpty + } else { + FooterMode::ComposerHasDraft + }; + match current { - FooterMode::ShortcutOverlay | FooterMode::QuitShortcutReminder => FooterMode::ComposerEmpty, + FooterMode::ShortcutOverlay | FooterMode::QuitShortcutReminder => base_mode, _ => FooterMode::ShortcutOverlay, } } @@ -152,6 +162,25 @@ pub(crate) fn footer_height(props: FooterProps) -> u16 { footer_lines(props, None, false, show_shortcuts_hint, show_queue_hint).len() as u16 } +/// Render a single precomputed footer line. +/// +/// Collapse/fallback logic (for example, `single_line_footer_layout`) may +/// choose a specific line that fits the current width (for example, after +/// dropping the cycle hint). +pub(crate) fn render_footer_line(area: Rect, buf: &mut Buffer, line: Line<'static>) { + Paragraph::new(prefix_lines( + vec![line], + " ".repeat(FOOTER_INDENT_COLS).into(), + " ".repeat(FOOTER_INDENT_COLS).into(), + )) + .render(area, buf); +} + +/// Render default footer content derived from `FooterProps`. +/// +/// This uses `footer_lines` to build a nicely formatted footer line +/// for varying screen widths. Use this when the caller has not already +/// selected a specific line to render. pub(crate) fn render_footer( area: Rect, buf: &mut Buffer, @@ -175,15 +204,6 @@ pub(crate) fn render_footer( .render(area, buf); } -pub(crate) fn render_footer_line(area: Rect, buf: &mut Buffer, line: Line<'static>) { - Paragraph::new(prefix_lines( - vec![line], - " ".repeat(FOOTER_INDENT_COLS).into(), - " ".repeat(FOOTER_INDENT_COLS).into(), - )) - .render(area, buf); -} - pub(crate) fn left_fits(area: Rect, left_width: u16) -> bool { let max_width = area.width.saturating_sub(FOOTER_INDENT_COLS as u16); left_width <= max_width @@ -459,6 +479,12 @@ pub(crate) fn render_footer_hint_items(area: Rect, buf: &mut Buffer, items: &[(S footer_hint_items_line(items).render(inset_footer_hint_area(area), buf); } +/// Build the default left-side footer lines for the current mode. +/// +/// This is the mode-driven baseline. Collapse/fallback logic may choose a +/// specific single-line variant that better fits the current width (see +/// `single_line_footer_layout`). The right-side context indicator is rendered +/// separately by the caller. fn footer_lines( props: FooterProps, indicator: Option, @@ -466,8 +492,6 @@ fn footer_lines( show_shortcuts_hint: bool, show_queue_hint: bool, ) -> Vec> { - // Render left-side hints only. The context indicator is rendered - // separately, right-aligned by the caller. match props.mode { FooterMode::QuitShortcutReminder => { vec![quit_shortcut_reminder_line(props.quit_shortcut_key)] From bc84910e448d287f0e5ada7c55569c0b7b927ee2 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 27 Jan 2026 00:25:27 -0800 Subject: [PATCH 19/26] Code quality --- codex-rs/tui/src/bottom_pane/footer.rs | 132 +++++++++++++------------ 1 file changed, 71 insertions(+), 61 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index a81ff7bf63a2..c16e5072a5b4 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -185,7 +185,7 @@ pub(crate) fn render_footer( area: Rect, buf: &mut Buffer, props: FooterProps, - indicator: Option, + collaboration_mode_indicator: Option, show_cycle_hint: bool, show_shortcuts_hint: bool, show_queue_hint: bool, @@ -193,7 +193,7 @@ pub(crate) fn render_footer( Paragraph::new(prefix_lines( footer_lines( props, - indicator, + collaboration_mode_indicator, show_cycle_hint, show_shortcuts_hint, show_queue_hint, @@ -218,14 +218,17 @@ enum SummaryHintKind { } #[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct SummarySpec { +struct LeftSideState { hint: SummaryHintKind, show_cycle_hint: bool, } -fn summary_line(indicator: Option, spec: SummarySpec) -> Line<'static> { +fn left_side_line( + collaboration_mode_indicator: Option, + state: LeftSideState, +) -> Line<'static> { let mut line = Line::from(""); - match spec.hint { + match state.hint { SummaryHintKind::None => {} SummaryHintKind::Shortcuts => { line.push_span(key_hint::plain(KeyCode::Char('?'))); @@ -241,11 +244,11 @@ fn summary_line(indicator: Option, spec: SummarySpec } }; - if let Some(indicator) = indicator { - if !matches!(spec.hint, SummaryHintKind::None) { + if let Some(collaboration_mode_indicator) = collaboration_mode_indicator { + if !matches!(state.hint, SummaryHintKind::None) { line.push_span(" · ".dim()); } - line.push_span(indicator.styled_span(spec.show_cycle_hint)); + line.push_span(collaboration_mode_indicator.styled_span(state.show_cycle_hint)); } line @@ -262,7 +265,7 @@ pub(crate) enum SummaryLeft { pub(crate) fn single_line_footer_layout( area: Rect, context_width: u16, - indicator: Option, + collaboration_mode_indicator: Option, show_cycle_hint: bool, show_shortcuts_hint: bool, show_queue_hint: bool, @@ -274,28 +277,28 @@ pub(crate) fn single_line_footer_layout( } else { SummaryHintKind::None }; - let default_spec = SummarySpec { + let default_state = LeftSideState { hint: hint_kind, show_cycle_hint, }; - let default_line = summary_line(indicator, default_spec); + let default_line = left_side_line(collaboration_mode_indicator, default_state); let default_width = default_line.width() as u16; if default_width > 0 && can_show_left_with_context(area, default_width, context_width) { return (SummaryLeft::Default, true); } - let spec_width = |spec: SummarySpec| -> u16 { - if spec == default_spec { + let state_width = |state: LeftSideState| -> u16 { + if state == default_state { default_width } else { - summary_line(indicator, spec).width() as u16 + left_side_line(collaboration_mode_indicator, state).width() as u16 } }; - let spec_line = |spec: SummarySpec| -> Line<'static> { - if spec == default_spec { + let state_line = |state: LeftSideState| -> Line<'static> { + if state == default_state { default_line.clone() } else { - summary_line(indicator, spec) + left_side_line(collaboration_mode_indicator, state) } }; // When the mode cycle hint is applicable (idle, non-queue mode), @@ -305,67 +308,67 @@ pub(crate) fn single_line_footer_layout( if show_queue_hint { // In queue mode, prefer dropping context before dropping the queue hint. - let queue_specs = [ - default_spec, - SummarySpec { + let queue_states = [ + default_state, + LeftSideState { hint: SummaryHintKind::QueueMessage, show_cycle_hint: false, }, - SummarySpec { + LeftSideState { hint: SummaryHintKind::QueueShort, show_cycle_hint: false, }, ]; - let mut previous_spec: Option = None; - for spec in queue_specs { - if previous_spec == Some(spec) { + let mut previous_state: Option = None; + for state in queue_states { + if previous_state == Some(state) { continue; } - previous_spec = Some(spec); - let width = spec_width(spec); + previous_state = Some(state); + let width = state_width(state); if width > 0 && can_show_left_with_context(area, width, context_width) { - if spec == default_spec { + if state == default_state { return (SummaryLeft::Default, true); } - return (SummaryLeft::Custom(spec_line(spec)), true); + return (SummaryLeft::Custom(state_line(state)), true); } } - let mut previous_spec: Option = None; - for spec in queue_specs { - if previous_spec == Some(spec) { + let mut previous_state: Option = None; + for state in queue_states { + if previous_state == Some(state) { continue; } - previous_spec = Some(spec); - let width = spec_width(spec); + previous_state = Some(state); + let width = state_width(state); if width > 0 && left_fits(area, width) { - if spec == default_spec { + if state == default_state { return (SummaryLeft::Default, false); } - return (SummaryLeft::Custom(spec_line(spec)), false); + return (SummaryLeft::Custom(state_line(state)), false); } } - } else if indicator.is_some() { + } else if collaboration_mode_indicator.is_some() { if show_cycle_hint { - let cycle_spec = SummarySpec { + let cycle_state = LeftSideState { hint: SummaryHintKind::None, show_cycle_hint: true, }; - let cycle_width = spec_width(cycle_spec); + let cycle_width = state_width(cycle_state); if cycle_width > 0 && can_show_left_with_context(area, cycle_width, context_width) { - return (SummaryLeft::Custom(spec_line(cycle_spec)), true); + return (SummaryLeft::Custom(state_line(cycle_state)), true); } if cycle_width > 0 && left_fits(area, cycle_width) { - return (SummaryLeft::Custom(spec_line(cycle_spec)), false); + return (SummaryLeft::Custom(state_line(cycle_state)), false); } } - let mode_only_spec = SummarySpec { + let mode_only_state = LeftSideState { hint: SummaryHintKind::None, show_cycle_hint: false, }; - let mode_only_width = spec_width(mode_only_spec); + let mode_only_width = state_width(mode_only_state); // When the mode cycle hint is applicable, only show context % if the mode cycle hint // itself can also fit. This prevents the right-side context indicator from // reappearing after we've already dropped "(shift+tab to cycle)". @@ -373,30 +376,37 @@ pub(crate) fn single_line_footer_layout( && mode_only_width > 0 && can_show_left_with_context(area, mode_only_width, context_width) { - return (SummaryLeft::Custom(spec_line(mode_only_spec)), true); + return (SummaryLeft::Custom(state_line(mode_only_state)), true); } if mode_only_width > 0 && left_fits(area, mode_only_width) { - return (SummaryLeft::Custom(spec_line(mode_only_spec)), false); + return (SummaryLeft::Custom(state_line(mode_only_state)), false); } } - if let Some(indicator) = indicator { - let mode_only_spec = SummarySpec { + if let Some(collaboration_mode_indicator) = collaboration_mode_indicator { + let mode_only_state = LeftSideState { hint: SummaryHintKind::None, show_cycle_hint: false, }; - let mode_only_width = summary_line(Some(indicator), mode_only_spec).width() as u16; + let mode_only_width = + left_side_line(Some(collaboration_mode_indicator), mode_only_state).width() as u16; if !context_requires_cycle_hint && can_show_left_with_context(area, mode_only_width, context_width) { return ( - SummaryLeft::Custom(summary_line(Some(indicator), mode_only_spec)), + SummaryLeft::Custom(left_side_line( + Some(collaboration_mode_indicator), + mode_only_state, + )), true, ); } if left_fits(area, mode_only_width) { return ( - SummaryLeft::Custom(summary_line(Some(indicator), mode_only_spec)), + SummaryLeft::Custom(left_side_line( + Some(collaboration_mode_indicator), + mode_only_state, + )), false, ); } @@ -487,7 +497,7 @@ pub(crate) fn render_footer_hint_items(area: Rect, buf: &mut Buffer, items: &[(S /// separately by the caller. fn footer_lines( props: FooterProps, - indicator: Option, + collaboration_mode_indicator: Option, show_cycle_hint: bool, show_shortcuts_hint: bool, show_queue_hint: bool, @@ -497,7 +507,7 @@ fn footer_lines( vec![quit_shortcut_reminder_line(props.quit_shortcut_key)] } FooterMode::ComposerEmpty => { - let spec = SummarySpec { + let state = LeftSideState { hint: if show_shortcuts_hint { SummaryHintKind::Shortcuts } else { @@ -505,7 +515,7 @@ fn footer_lines( }, show_cycle_hint, }; - vec![summary_line(indicator, spec)] + vec![left_side_line(collaboration_mode_indicator, state)] } FooterMode::ShortcutOverlay => { // Intentionally suppress the mode indicator here: the overlay is a @@ -526,7 +536,7 @@ fn footer_lines( } FooterMode::EscHint => vec![esc_hint_line(props.esc_backtrack_hint)], FooterMode::ComposerHasDraft => { - let spec = SummarySpec { + let state = LeftSideState { hint: if show_queue_hint { SummaryHintKind::QueueMessage } else { @@ -534,21 +544,21 @@ fn footer_lines( }, show_cycle_hint, }; - vec![summary_line(indicator, spec)] + vec![left_side_line(collaboration_mode_indicator, state)] } } } pub(crate) fn footer_line_width( props: FooterProps, - indicator: Option, + collaboration_mode_indicator: Option, show_cycle_hint: bool, show_shortcuts_hint: bool, show_queue_hint: bool, ) -> u16 { footer_lines( props, - indicator, + collaboration_mode_indicator, show_cycle_hint, show_shortcuts_hint, show_queue_hint, @@ -1018,7 +1028,7 @@ mod tests { name: &str, width: u16, props: FooterProps, - indicator: Option, + collaboration_mode_indicator: Option, ) { let height = footer_height(props).max(1); let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); @@ -1047,7 +1057,7 @@ mod tests { }; let left_width = footer_line_width( props, - indicator, + collaboration_mode_indicator, show_cycle_hint, show_shortcuts_hint, show_queue_hint, @@ -1061,7 +1071,7 @@ mod tests { let (summary_left, show_context) = single_line_footer_layout( area, context_width, - indicator, + collaboration_mode_indicator, show_cycle_hint, show_shortcuts_hint, show_queue_hint, @@ -1072,7 +1082,7 @@ mod tests { area, f.buffer_mut(), props, - indicator, + collaboration_mode_indicator, show_cycle_hint, show_shortcuts_hint, show_queue_hint, @@ -1091,7 +1101,7 @@ mod tests { area, f.buffer_mut(), props, - indicator, + collaboration_mode_indicator, show_cycle_hint, show_shortcuts_hint, show_queue_hint, From 78e03e0d7d69ed5e5b1d0049f59457b919e74122 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 27 Jan 2026 00:39:37 -0800 Subject: [PATCH 20/26] Comments --- codex-rs/tui/src/bottom_pane/footer.rs | 45 ++++++++++++++++---------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index c16e5072a5b4..29875c71a05f 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -287,13 +287,6 @@ pub(crate) fn single_line_footer_layout( return (SummaryLeft::Default, true); } - let state_width = |state: LeftSideState| -> u16 { - if state == default_state { - default_width - } else { - left_side_line(collaboration_mode_indicator, state).width() as u16 - } - }; let state_line = |state: LeftSideState| -> Line<'static> { if state == default_state { default_line.clone() @@ -301,9 +294,10 @@ pub(crate) fn single_line_footer_layout( left_side_line(collaboration_mode_indicator, state) } }; - // When the mode cycle hint is applicable (idle, non-queue mode), - // show the right-side context indicator if the "(shift+tab to cycle)" - // variant can also fit. + let state_width = |state: LeftSideState| -> u16 { state_line(state).width() as u16 }; + // When the mode cycle hint is applicable (idle, non-queue mode), only show + // the right-side context indicator if the "(shift+tab to cycle)" variant + // can also fit. let context_requires_cycle_hint = show_cycle_hint && !show_queue_hint; if show_queue_hint { @@ -320,6 +314,9 @@ pub(crate) fn single_line_footer_layout( }, ]; + // Pass 1: keep the right-side context indicator if any queue variant + // can fit alongside it. We skip adjacent duplicates because + // `default_state` can already be the no-cycle queue variant. let mut previous_state: Option = None; for state in queue_states { if previous_state == Some(state) { @@ -335,6 +332,8 @@ pub(crate) fn single_line_footer_layout( } } + // Pass 2: if context cannot fit, drop it before dropping the queue + // hint. Reuse the same dedupe so we do not try equivalent states twice. let mut previous_state: Option = None; for state in queue_states { if previous_state == Some(state) { @@ -351,6 +350,8 @@ pub(crate) fn single_line_footer_layout( } } else if collaboration_mode_indicator.is_some() { if show_cycle_hint { + // First fallback: drop shortcut hint but keep the cycle + // hint on the mode label if it can fit. let cycle_state = LeftSideState { hint: SummaryHintKind::None, show_cycle_hint: true, @@ -364,30 +365,40 @@ pub(crate) fn single_line_footer_layout( } } + // Next fallback: mode label only. If the cycle hint is applicable but + // cannot fit, we also suppress context so the right side does not + // outlive "(shift+tab to cycle)" on the left. let mode_only_state = LeftSideState { hint: SummaryHintKind::None, show_cycle_hint: false, }; let mode_only_width = state_width(mode_only_state); - // When the mode cycle hint is applicable, only show context % if the mode cycle hint - // itself can also fit. This prevents the right-side context indicator from - // reappearing after we've already dropped "(shift+tab to cycle)". if !context_requires_cycle_hint && mode_only_width > 0 && can_show_left_with_context(area, mode_only_width, context_width) { - return (SummaryLeft::Custom(state_line(mode_only_state)), true); + return ( + SummaryLeft::Custom(state_line(mode_only_state)), + true, // show_context + ); } if mode_only_width > 0 && left_fits(area, mode_only_width) { - return (SummaryLeft::Custom(state_line(mode_only_state)), false); + return ( + SummaryLeft::Custom(state_line(mode_only_state)), + false, // show_context + ); } } + // Final fallback: if queue variants (or other earlier states) could not fit + // at all, drop every hint and try to show just the mode label. if let Some(collaboration_mode_indicator) = collaboration_mode_indicator { let mode_only_state = LeftSideState { hint: SummaryHintKind::None, show_cycle_hint: false, }; + // Compute the width without going through `state_line` so we do not + // depend on `default_state` (which may still be a queue variant). let mode_only_width = left_side_line(Some(collaboration_mode_indicator), mode_only_state).width() as u16; if !context_requires_cycle_hint @@ -398,7 +409,7 @@ pub(crate) fn single_line_footer_layout( Some(collaboration_mode_indicator), mode_only_state, )), - true, + true, // show_context ); } if left_fits(area, mode_only_width) { @@ -407,7 +418,7 @@ pub(crate) fn single_line_footer_layout( Some(collaboration_mode_indicator), mode_only_state, )), - false, + false, // show_context ); } } From 72be1631608b4296aaf479cdfcfcf34e88a6a2e7 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 27 Jan 2026 01:08:13 -0800 Subject: [PATCH 21/26] Code quality --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 6 +-- codex-rs/tui/src/bottom_pane/footer.rs | 49 +++++++++---------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index aa7fc2932156..aadb8629087f 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -105,7 +105,7 @@ use super::footer::footer_hint_items_width; use super::footer::footer_line_width; use super::footer::inset_footer_hint_area; use super::footer::render_context_right; -use super::footer::render_footer; +use super::footer::render_footer_from_props; use super::footer::render_footer_hint_items; use super::footer::render_footer_line; use super::footer::reset_mode_after_activity; @@ -2620,7 +2620,7 @@ impl Renderable for ChatComposer { if let Some((summary_left, _)) = single_line_layout { match summary_left { SummaryLeft::Default => { - render_footer( + render_footer_from_props( hint_rect, buf, footer_props, @@ -2642,7 +2642,7 @@ impl Renderable for ChatComposer { } else if let Some(items) = self.footer_hint_override.as_ref() { render_footer_hint_items(hint_rect, buf, items); } else { - render_footer( + render_footer_from_props( hint_rect, buf, footer_props, diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 29875c71a05f..4a92da0669d1 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -27,9 +27,10 @@ use ratatui::widgets::Widget; /// The rendering inputs for the footer area under the composer. /// /// Callers are expected to construct `FooterProps` from higher-level state (`ChatComposer`, -/// `BottomPane`, and `ChatWidget`) and pass it to `render_footer`. The footer treats these values as -/// authoritative and does not attempt to infer missing state (for example, it does not query -/// whether a task is running). +/// `BottomPane`, and `ChatWidget`) and pass it to the footer render helpers +/// (`render_footer_from_props` or the single-line collapse logic). The footer +/// treats these values as authoritative and does not attempt to infer missing +/// state (for example, it does not query whether a task is running). #[derive(Clone, Copy, Debug)] pub(crate) struct FooterProps { pub(crate) mode: FooterMode, @@ -159,14 +160,10 @@ pub(crate) fn footer_height(props: FooterProps) -> u16 { | FooterMode::ShortcutOverlay | FooterMode::EscHint => false, }; - footer_lines(props, None, false, show_shortcuts_hint, show_queue_hint).len() as u16 + footer_from_props_lines(props, None, false, show_shortcuts_hint, show_queue_hint).len() as u16 } /// Render a single precomputed footer line. -/// -/// Collapse/fallback logic (for example, `single_line_footer_layout`) may -/// choose a specific line that fits the current width (for example, after -/// dropping the cycle hint). pub(crate) fn render_footer_line(area: Rect, buf: &mut Buffer, line: Line<'static>) { Paragraph::new(prefix_lines( vec![line], @@ -176,12 +173,14 @@ pub(crate) fn render_footer_line(area: Rect, buf: &mut Buffer, line: Line<'stati .render(area, buf); } -/// Render default footer content derived from `FooterProps`. +/// Render footer content directly from `FooterProps`. /// -/// This uses `footer_lines` to build a nicely formatted footer line -/// for varying screen widths. Use this when the caller has not already -/// selected a specific line to render. -pub(crate) fn render_footer( +/// This is intentionally not part of the width-based collapse/fallback logic. +/// Transient instructional states (shortcut overlay, Esc hint, quit reminder) +/// prioritize "what to do next" instructions and currently suppress the +/// collaboration mode label entirely. When collapse logic has already chosen a +/// specific single line, prefer `render_footer_line`. +pub(crate) fn render_footer_from_props( area: Rect, buf: &mut Buffer, props: FooterProps, @@ -191,7 +190,7 @@ pub(crate) fn render_footer( show_queue_hint: bool, ) { Paragraph::new(prefix_lines( - footer_lines( + footer_from_props_lines( props, collaboration_mode_indicator, show_cycle_hint, @@ -500,13 +499,13 @@ pub(crate) fn render_footer_hint_items(area: Rect, buf: &mut Buffer, items: &[(S footer_hint_items_line(items).render(inset_footer_hint_area(area), buf); } -/// Build the default left-side footer lines for the current mode. +/// Map `FooterProps` to one or more footer lines without dynamic collapse. /// -/// This is the mode-driven baseline. Collapse/fallback logic may choose a -/// specific single-line variant that better fits the current width (see -/// `single_line_footer_layout`). The right-side context indicator is rendered -/// separately by the caller. -fn footer_lines( +/// This is the straightforward FooterMode-to-text mapping used by instructional, +/// transient footer states (shortcut overlay, Esc hint, quit reminder), and as +/// a fallback for base states when we are not running the single-line collapse +/// logic. Width-based collapse decisions live in `single_line_footer_layout`. +fn footer_from_props_lines( props: FooterProps, collaboration_mode_indicator: Option, show_cycle_hint: bool, @@ -567,7 +566,7 @@ pub(crate) fn footer_line_width( show_shortcuts_hint: bool, show_queue_hint: bool, ) -> u16 { - footer_lines( + footer_from_props_lines( props, collaboration_mode_indicator, show_cycle_hint, @@ -991,7 +990,7 @@ mod tests { ); match summary_left { SummaryLeft::Default => { - render_footer( + render_footer_from_props( area, f.buffer_mut(), props, @@ -1010,7 +1009,7 @@ mod tests { render_context_right(area, f.buffer_mut(), &context_line); } } else { - render_footer( + render_footer_from_props( area, f.buffer_mut(), props, @@ -1089,7 +1088,7 @@ mod tests { ); match summary_left { SummaryLeft::Default => { - render_footer( + render_footer_from_props( area, f.buffer_mut(), props, @@ -1108,7 +1107,7 @@ mod tests { render_context_right(area, f.buffer_mut(), &context_line); } } else { - render_footer( + render_footer_from_props( area, f.buffer_mut(), props, From 791aa190027253b4e76cb3e3b9db2ae669fc25dc Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 27 Jan 2026 01:20:02 -0800 Subject: [PATCH 22/26] Comments --- codex-rs/tui/src/bottom_pane/footer.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 4a92da0669d1..b77341a43c5f 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -8,6 +8,20 @@ //! Some footer content is time-based rather than event-based, such as the "press again to quit" //! hint. The owning widgets schedule redraws so time-based hints can expire even if the UI is //! otherwise idle. +//! +//! Single-line collapse overview: +//! 1. The composer decides the current `FooterMode` and hint flags, then calls +//! `single_line_footer_layout` for the base single-line modes. +//! 2. That layout helper applies the width-based fallback rules: prefer the +//! fullest left-side hint that still allows the right-side context indicator +//! to fit, then progressively drop hint text and finally context as needed. +//! 3. When collapse chooses a specific line, callers render it via +//! `render_footer_line`. Otherwise, callers render the straightforward +//! mode-to-text mapping via `render_footer_from_props`. +//! +//! In short: `single_line_footer_layout` chooses *what* best fits, and the two +//! render helpers choose whether to draw the chosen line or the default +//! `FooterProps` mapping. #[cfg(target_os = "linux")] use crate::clipboard_paste::is_probably_wsl; use crate::key_hint; @@ -499,12 +513,14 @@ pub(crate) fn render_footer_hint_items(area: Rect, buf: &mut Buffer, items: &[(S footer_hint_items_line(items).render(inset_footer_hint_area(area), buf); } -/// Map `FooterProps` to one or more footer lines without dynamic collapse. +/// Map `FooterProps` to footer lines without width-based collapse. /// -/// This is the straightforward FooterMode-to-text mapping used by instructional, -/// transient footer states (shortcut overlay, Esc hint, quit reminder), and as -/// a fallback for base states when we are not running the single-line collapse -/// logic. Width-based collapse decisions live in `single_line_footer_layout`. +/// This is the canonical FooterMode-to-text mapping. It powers transient, +/// instructional states (shortcut overlay, Esc hint, quit reminder) and also +/// the default rendering for base states when collapse is not applied (or when +/// `single_line_footer_layout` returns `SummaryLeft::Default`). Collapse and +/// fallback decisions live in `single_line_footer_layout`; this function only +/// formats the chosen/default content. fn footer_from_props_lines( props: FooterProps, collaboration_mode_indicator: Option, From 1ab40e80b4d939a81d611f2baed805fd5165702d Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 27 Jan 2026 01:28:29 -0800 Subject: [PATCH 23/26] Comments --- codex-rs/tui/src/bottom_pane/footer.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index b77341a43c5f..1c1ddcd21ab7 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -12,9 +12,19 @@ //! Single-line collapse overview: //! 1. The composer decides the current `FooterMode` and hint flags, then calls //! `single_line_footer_layout` for the base single-line modes. -//! 2. That layout helper applies the width-based fallback rules: prefer the -//! fullest left-side hint that still allows the right-side context indicator -//! to fit, then progressively drop hint text and finally context as needed. +//! 2. `single_line_footer_layout` applies the width-based fallback rules: +//! (If this description is hard to follow, just try it out by resizing +//! your terminal width; these rules were built out of trial and error.) +//! - Start with the fullest left-side hint plus the right-side context. +//! - When the queue hint is active, prefer keeping that queue hint visible, +//! even if it means dropping the right-side context earlier; the queue +//! hint may also be shortened before it is removed. +//! - When the queue hint is not active but the mode cycle hint is applicable, +//! drop "? for shortcuts" before dropping "(shift+tab to cycle)". +//! - If "(shift+tab to cycle)" cannot fit, also hide the right-side +//! context to avoid too many state transitions in quick succession. +//! - Finally, try a mode-only line (with and without context), and fall +//! back to no left-side footer if nothing can fit. //! 3. When collapse chooses a specific line, callers render it via //! `render_footer_line`. Otherwise, callers render the straightforward //! mode-to-text mapping via `render_footer_from_props`. @@ -544,9 +554,6 @@ fn footer_from_props_lines( vec![left_side_line(collaboration_mode_indicator, state)] } FooterMode::ShortcutOverlay => { - // Intentionally suppress the mode indicator here: the overlay is a - // transient "what can I do next?" state and we want the next action - // hints to take full priority. #[cfg(target_os = "linux")] let is_wsl = is_probably_wsl(); #[cfg(not(target_os = "linux"))] From 0c97ae546eaed8f2a0272448222c8f68a5afc148 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 27 Jan 2026 01:33:53 -0800 Subject: [PATCH 24/26] Reduce duplication --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 5 +- codex-rs/tui/src/bottom_pane/footer.rs | 112 ++++-------------- 2 files changed, 22 insertions(+), 95 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index aadb8629087f..7404d4a45339 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -98,7 +98,7 @@ use super::footer::FooterMode; use super::footer::FooterProps; use super::footer::SummaryLeft; use super::footer::can_show_left_with_context; -use super::footer::context_window_line; +use super::footer::context_window_line_and_width; use super::footer::esc_hint_mode; use super::footer::footer_height; use super::footer::footer_hint_items_width; @@ -2541,11 +2541,10 @@ impl Renderable for ChatComposer { | FooterMode::ShortcutOverlay | FooterMode::EscHint => false, }; - let context_line = context_window_line( + let (context_line, context_width) = context_window_line_and_width( footer_props.context_window_percent, footer_props.context_window_used_tokens, ); - let context_width = context_line.width() as u16; let custom_height = self.custom_footer_height(); let footer_hint_height = custom_height.unwrap_or_else(|| footer_height(footer_props)); diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 1c1ddcd21ab7..b9dd2b2103de 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -760,6 +760,15 @@ pub(crate) fn context_window_line(percent: Option, used_tokens: Option Line::from(vec![Span::from("100% context left").dim()]) } +pub(crate) fn context_window_line_and_width( + percent: Option, + used_tokens: Option, +) -> (Line<'static>, u16) { + let line = context_window_line(percent, used_tokens); + let width = line.width() as u16; + (line, width) +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ShortcutId { Commands, @@ -965,96 +974,7 @@ mod tests { use ratatui::backend::TestBackend; fn snapshot_footer(name: &str, props: FooterProps) { - let height = footer_height(props).max(1); - let mut terminal = Terminal::new(TestBackend::new(80, height)).unwrap(); - terminal - .draw(|f| { - let area = Rect::new(0, 0, f.area().width, height); - let context_line = context_window_line( - props.context_window_percent, - props.context_window_used_tokens, - ); - let context_width = context_line.width() as u16; - let show_cycle_hint = !props.is_task_running; - let show_shortcuts_hint = match props.mode { - FooterMode::ComposerEmpty => true, - FooterMode::QuitShortcutReminder - | FooterMode::ShortcutOverlay - | FooterMode::EscHint - | FooterMode::ComposerHasDraft => false, - }; - let show_queue_hint = match props.mode { - FooterMode::ComposerHasDraft => props.is_task_running && props.steer_enabled, - FooterMode::QuitShortcutReminder - | FooterMode::ComposerEmpty - | FooterMode::ShortcutOverlay - | FooterMode::EscHint => false, - }; - let left_width = footer_line_width( - props, - None, - show_cycle_hint, - show_shortcuts_hint, - show_queue_hint, - ); - let can_show_left_and_context = - can_show_left_with_context(area, left_width, context_width); - if matches!( - props.mode, - FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft - ) { - let (summary_left, show_context) = single_line_footer_layout( - area, - context_width, - None, - show_cycle_hint, - show_shortcuts_hint, - show_queue_hint, - ); - match summary_left { - SummaryLeft::Default => { - render_footer_from_props( - area, - f.buffer_mut(), - props, - None, - show_cycle_hint, - show_shortcuts_hint, - show_queue_hint, - ); - } - SummaryLeft::Custom(line) => { - render_footer_line(area, f.buffer_mut(), line); - } - SummaryLeft::None => {} - } - if show_context { - render_context_right(area, f.buffer_mut(), &context_line); - } - } else { - render_footer_from_props( - area, - f.buffer_mut(), - props, - None, - show_cycle_hint, - show_shortcuts_hint, - show_queue_hint, - ); - let show_context = can_show_left_and_context - && !matches!( - props.mode, - FooterMode::EscHint - | FooterMode::QuitShortcutReminder - | FooterMode::ShortcutOverlay - ); - if show_context { - render_context_right(area, f.buffer_mut(), &context_line); - } - } - }) - .unwrap(); - assert_snapshot!(name, terminal.backend()); + snapshot_footer_impl(name, 80, props, None); } fn snapshot_footer_with_indicator( @@ -1062,17 +982,25 @@ mod tests { width: u16, props: FooterProps, collaboration_mode_indicator: Option, + ) { + snapshot_footer_impl(name, width, props, collaboration_mode_indicator); + } + + fn snapshot_footer_impl( + name: &str, + width: u16, + props: FooterProps, + collaboration_mode_indicator: Option, ) { let height = footer_height(props).max(1); let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); terminal .draw(|f| { let area = Rect::new(0, 0, f.area().width, height); - let context_line = context_window_line( + let (context_line, context_width) = context_window_line_and_width( props.context_window_percent, props.context_window_used_tokens, ); - let context_width = context_line.width() as u16; let show_cycle_hint = !props.is_task_running; let show_shortcuts_hint = match props.mode { FooterMode::ComposerEmpty => true, From 2d47b352a7559340d1d88af42b81ac35bd043dc4 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 27 Jan 2026 01:38:38 -0800 Subject: [PATCH 25/26] Code quality --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 5 +-- codex-rs/tui/src/bottom_pane/footer.rs | 31 +++++-------------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 7404d4a45339..aadb8629087f 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -98,7 +98,7 @@ use super::footer::FooterMode; use super::footer::FooterProps; use super::footer::SummaryLeft; use super::footer::can_show_left_with_context; -use super::footer::context_window_line_and_width; +use super::footer::context_window_line; use super::footer::esc_hint_mode; use super::footer::footer_height; use super::footer::footer_hint_items_width; @@ -2541,10 +2541,11 @@ impl Renderable for ChatComposer { | FooterMode::ShortcutOverlay | FooterMode::EscHint => false, }; - let (context_line, context_width) = context_window_line_and_width( + let context_line = context_window_line( footer_props.context_window_percent, footer_props.context_window_used_tokens, ); + let context_width = context_line.width() as u16; let custom_height = self.custom_footer_height(); let footer_hint_height = custom_height.unwrap_or_else(|| footer_height(footer_props)); diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index b9dd2b2103de..9efee4d572de 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -760,15 +760,6 @@ pub(crate) fn context_window_line(percent: Option, used_tokens: Option Line::from(vec![Span::from("100% context left").dim()]) } -pub(crate) fn context_window_line_and_width( - percent: Option, - used_tokens: Option, -) -> (Line<'static>, u16) { - let line = context_window_line(percent, used_tokens); - let width = line.width() as u16; - (line, width) -} - #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ShortcutId { Commands, @@ -974,19 +965,10 @@ mod tests { use ratatui::backend::TestBackend; fn snapshot_footer(name: &str, props: FooterProps) { - snapshot_footer_impl(name, 80, props, None); - } - - fn snapshot_footer_with_indicator( - name: &str, - width: u16, - props: FooterProps, - collaboration_mode_indicator: Option, - ) { - snapshot_footer_impl(name, width, props, collaboration_mode_indicator); + snapshot_footer_with_mode_indicator(name, 80, props, None); } - fn snapshot_footer_impl( + fn snapshot_footer_with_mode_indicator( name: &str, width: u16, props: FooterProps, @@ -997,10 +979,11 @@ mod tests { terminal .draw(|f| { let area = Rect::new(0, 0, f.area().width, height); - let (context_line, context_width) = context_window_line_and_width( + let context_line = context_window_line( props.context_window_percent, props.context_window_used_tokens, ); + let context_width = context_line.width() as u16; let show_cycle_hint = !props.is_task_running; let show_shortcuts_hint = match props.mode { FooterMode::ComposerEmpty => true, @@ -1262,14 +1245,14 @@ mod tests { context_window_used_tokens: None, }; - snapshot_footer_with_indicator( + snapshot_footer_with_mode_indicator( "footer_mode_indicator_wide", 120, props, Some(CollaborationModeIndicator::Plan), ); - snapshot_footer_with_indicator( + snapshot_footer_with_mode_indicator( "footer_mode_indicator_narrow_overlap_hides", 50, props, @@ -1288,7 +1271,7 @@ mod tests { context_window_used_tokens: None, }; - snapshot_footer_with_indicator( + snapshot_footer_with_mode_indicator( "footer_mode_indicator_running_hides_hint", 120, props, From c2669106cc767c14bf68b7262f9ce621c907f097 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 27 Jan 2026 09:26:24 -0800 Subject: [PATCH 26/26] Tweak color of code mode --- codex-rs/tui/src/bottom_pane/footer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 9efee4d572de..69c5e7bb0078 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -102,7 +102,7 @@ impl CollaborationModeIndicator { let label = self.label(show_cycle_hint); match self { CollaborationModeIndicator::Plan => Span::from(label).magenta(), - CollaborationModeIndicator::Code => Span::from(label).cyan(), + CollaborationModeIndicator::Code => Span::from(label).dim(), CollaborationModeIndicator::PairProgramming => Span::from(label).cyan(), CollaborationModeIndicator::Execute => Span::from(label).dim(), }