From ec98445abf602e54c0399b7dd61da14de286b8a6 Mon Sep 17 00:00:00 2001
From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com>
Date: Thu, 2 Oct 2025 11:34:47 -0700
Subject: [PATCH] normalize key hints (#4586)
render key hints the same everywhere.
| Before | After |
|--------|-------|
|
|
|
|
|
|
|
|
|
|
|
|
---
.../tui/src/bottom_pane/approval_overlay.rs | 9 +-
.../tui/src/bottom_pane/custom_prompt_view.rs | 4 +-
codex-rs/tui/src/bottom_pane/footer.rs | 153 ++++++++----------
.../src/bottom_pane/list_selection_view.rs | 12 +-
codex-rs/tui/src/bottom_pane/popup_consts.rs | 15 +-
...__tests__footer_mode_shortcut_overlay.snap | 1 -
...tests__footer_shortcuts_shift_and_esc.snap | 1 -
..._list_selection_spacing_with_subtitle.snap | 2 +-
...st_selection_spacing_without_subtitle.snap | 2 +-
codex-rs/tui/src/chatwidget.rs | 14 +-
...hatwidget__tests__approval_modal_exec.snap | 2 +-
..._tests__approval_modal_exec_no_reason.snap | 2 +-
...atwidget__tests__approval_modal_patch.snap | 2 +-
...twidget__tests__chat_small_running_h2.snap | 2 +-
...exec_and_status_layout_vt100_snapshot.snap | 2 +-
...dget__tests__exec_approval_modal_exec.snap | 4 +-
...atwidget__tests__status_widget_active.snap | 2 +-
...sts__status_widget_and_approval_modal.snap | 2 +-
codex-rs/tui/src/key_hint.rs | 95 +++++++++--
codex-rs/tui/src/pager_overlay.rs | 115 +++++--------
codex-rs/tui/src/resume_picker.rs | 23 +--
..._tests__static_overlay_snapshot_basic.snap | 4 +-
...ript_overlay_apply_patch_scroll_vt100.snap | 4 +-
...ts__transcript_overlay_snapshot_basic.snap | 4 +-
...ator_widget__tests__renders_truncated.snap | 2 +-
...__tests__renders_with_queued_messages.snap | 4 +-
...t__tests__renders_with_working_header.snap | 2 +-
codex-rs/tui/src/status_indicator_widget.rs | 13 +-
28 files changed, 270 insertions(+), 227 deletions(-)
diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs
index 511039df4..d85e5db57 100644
--- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs
+++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs
@@ -11,6 +11,7 @@ use crate::bottom_pane::list_selection_view::SelectionViewParams;
use crate::diff_render::DiffSummary;
use crate::exec_command::strip_bash_lc_and_escape;
use crate::history_cell;
+use crate::key_hint;
use crate::render::highlight::highlight_bash_to_lines;
use crate::render::renderable::ColumnRenderable;
use crate::render::renderable::Renderable;
@@ -116,7 +117,13 @@ impl ApprovalOverlay {
.collect();
let params = SelectionViewParams {
- footer_hint: Some("Press Enter to confirm or Esc to cancel".to_string()),
+ footer_hint: Some(Line::from(vec![
+ "Press ".into(),
+ key_hint::plain(KeyCode::Enter).into(),
+ " to confirm or ".into(),
+ key_hint::plain(KeyCode::Esc).into(),
+ " to cancel".into(),
+ ])),
items,
header,
..Default::default()
diff --git a/codex-rs/tui/src/bottom_pane/custom_prompt_view.rs b/codex-rs/tui/src/bottom_pane/custom_prompt_view.rs
index 07bd92038..54f474fb1 100644
--- a/codex-rs/tui/src/bottom_pane/custom_prompt_view.rs
+++ b/codex-rs/tui/src/bottom_pane/custom_prompt_view.rs
@@ -14,7 +14,7 @@ use std::cell::RefCell;
use crate::render::renderable::Renderable;
-use super::popup_consts::STANDARD_POPUP_HINT_LINE;
+use super::popup_consts::standard_popup_hint_line;
use super::CancellationEvent;
use super::bottom_pane_view::BottomPaneView;
@@ -221,7 +221,7 @@ impl Renderable for CustomPromptView {
let hint_y = hint_blank_y.saturating_add(1);
if hint_y < area.y.saturating_add(area.height) {
- Paragraph::new(STANDARD_POPUP_HINT_LINE).render(
+ Paragraph::new(standard_popup_hint_line()).render(
Rect {
x: area.x,
y: hint_y,
diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs
index 133c0b34b..3fc9b2e1f 100644
--- a/codex-rs/tui/src/bottom_pane/footer.rs
+++ b/codex-rs/tui/src/bottom_pane/footer.rs
@@ -1,13 +1,15 @@
+use crate::key_hint;
+use crate::key_hint::KeyBinding;
+use crate::render::line_utils::prefix_lines;
use crate::ui_consts::FOOTER_INDENT_COLS;
use crossterm::event::KeyCode;
-use crossterm::event::KeyModifiers;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::text::Span;
-use ratatui::widgets::WidgetRef;
-use std::iter;
+use ratatui::widgets::Paragraph;
+use ratatui::widgets::Widget;
#[derive(Clone, Copy, Debug)]
pub(crate) struct FooterProps {
@@ -61,15 +63,12 @@ pub(crate) fn footer_height(props: FooterProps) -> u16 {
}
pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps) {
- let lines = footer_lines(props);
- for (idx, line) in lines.into_iter().enumerate() {
- let y = area.y + idx as u16;
- if y >= area.y + area.height {
- break;
- }
- let row = Rect::new(area.x, y, area.width, 1);
- line.render_ref(row, buf);
- }
+ Paragraph::new(prefix_lines(
+ footer_lines(props),
+ " ".repeat(FOOTER_INDENT_COLS).into(),
+ " ".repeat(FOOTER_INDENT_COLS).into(),
+ ))
+ .render(area, buf);
}
fn footer_lines(props: FooterProps) -> Vec> {
@@ -81,7 +80,10 @@ fn footer_lines(props: FooterProps) -> Vec> {
if props.is_task_running {
vec![context_window_line(props.context_window_percent)]
} else {
- vec![dim_line(indent_text("? for shortcuts"))]
+ vec![Line::from(vec![
+ key_hint::plain(KeyCode::Char('?')).into(),
+ " for shortcuts".dim(),
+ ])]
}
}
FooterMode::ShortcutOverlay => shortcut_overlay_lines(ShortcutsState {
@@ -110,27 +112,36 @@ fn ctrl_c_reminder_line(state: CtrlCReminderState) -> Line<'static> {
} else {
"quit"
};
- let text = format!("ctrl + c again to {action}");
- dim_line(indent_text(&text))
+ Line::from(vec![
+ key_hint::ctrl(KeyCode::Char('c')).into(),
+ format!(" again to {action}").into(),
+ ])
+ .dim()
}
fn esc_hint_line(esc_backtrack_hint: bool) -> Line<'static> {
- let text = if esc_backtrack_hint {
- "esc again to edit previous message"
+ let esc = key_hint::plain(KeyCode::Esc);
+ if esc_backtrack_hint {
+ Line::from(vec![esc.into(), " again to edit previous message".into()]).dim()
} else {
- "esc esc to edit previous message"
- };
- dim_line(indent_text(text))
+ Line::from(vec![
+ esc.into(),
+ " ".into(),
+ esc.into(),
+ " to edit previous message".into(),
+ ])
+ .dim()
+ }
}
fn shortcut_overlay_lines(state: ShortcutsState) -> Vec> {
- let mut commands = String::new();
- let mut newline = String::new();
- let mut file_paths = String::new();
- let mut paste_image = String::new();
- let mut edit_previous = String::new();
- let mut quit = String::new();
- let mut show_transcript = String::new();
+ let mut commands = Line::from("");
+ let mut newline = Line::from("");
+ let mut file_paths = Line::from("");
+ let mut paste_image = Line::from("");
+ let mut edit_previous = Line::from("");
+ let mut quit = Line::from("");
+ let mut show_transcript = Line::from("");
for descriptor in SHORTCUTS {
if let Some(text) = descriptor.overlay_entry(state) {
@@ -153,14 +164,14 @@ fn shortcut_overlay_lines(state: ShortcutsState) -> Vec> {
paste_image,
edit_previous,
quit,
- String::new(),
+ Line::from(""),
show_transcript,
];
build_columns(ordered)
}
-fn build_columns(entries: Vec) -> Vec> {
+fn build_columns(entries: Vec>) -> Vec> {
if entries.is_empty() {
return Vec::new();
}
@@ -174,7 +185,7 @@ fn build_columns(entries: Vec) -> Vec> {
let mut entries = entries;
if entries.len() < target_len {
entries.extend(std::iter::repeat_n(
- String::new(),
+ Line::from(""),
target_len - entries.len(),
));
}
@@ -183,7 +194,7 @@ fn build_columns(entries: Vec) -> Vec> {
for (idx, entry) in entries.iter().enumerate() {
let column = idx % COLUMNS;
- column_widths[column] = column_widths[column].max(entry.len());
+ column_widths[column] = column_widths[column].max(entry.width());
}
for (idx, width) in column_widths.iter_mut().enumerate() {
@@ -193,42 +204,30 @@ fn build_columns(entries: Vec) -> Vec> {
entries
.chunks(COLUMNS)
.map(|chunk| {
- let mut line = String::new();
+ let mut line = Line::from("");
for (col, entry) in chunk.iter().enumerate() {
- line.push_str(entry);
+ line.extend(entry.spans.clone());
if col < COLUMNS - 1 {
let target_width = column_widths[col];
- let padding = target_width.saturating_sub(entry.len()) + COLUMN_GAP;
- line.push_str(&" ".repeat(padding));
+ let padding = target_width.saturating_sub(entry.width()) + COLUMN_GAP;
+ line.push_span(Span::from(" ".repeat(padding)));
}
}
- let indented = indent_text(&line);
- dim_line(indented)
+ line.dim()
})
.collect()
}
-fn indent_text(text: &str) -> String {
- let mut indented = String::with_capacity(FOOTER_INDENT_COLS + text.len());
- indented.extend(iter::repeat_n(' ', FOOTER_INDENT_COLS));
- indented.push_str(text);
- indented
-}
-
-fn dim_line(text: String) -> Line<'static> {
- Line::from(text).dim()
-}
-
fn context_window_line(percent: Option) -> Line<'static> {
let mut spans: Vec> = Vec::new();
- spans.push(indent_text("").into());
match percent {
Some(percent) => {
spans.push(format!("{percent}%").bold());
spans.push(" context left".dim());
}
None => {
- spans.push("? for shortcuts".dim());
+ spans.push(key_hint::plain(KeyCode::Char('?')).into());
+ spans.push(" for shortcuts".dim());
}
}
Line::from(spans)
@@ -247,9 +246,7 @@ enum ShortcutId {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ShortcutBinding {
- code: KeyCode,
- modifiers: KeyModifiers,
- overlay_text: &'static str,
+ key: KeyBinding,
condition: DisplayCondition,
}
@@ -288,20 +285,24 @@ impl ShortcutDescriptor {
self.bindings.iter().find(|binding| binding.matches(state))
}
- fn overlay_entry(&self, state: ShortcutsState) -> Option {
+ fn overlay_entry(&self, state: ShortcutsState) -> Option> {
let binding = self.binding_for(state)?;
- let label = match self.id {
+ let mut line = Line::from(vec![self.prefix.into(), binding.key.into()]);
+ match self.id {
ShortcutId::EditPrevious => {
if state.esc_backtrack_hint {
- " again to edit previous message"
+ line.push_span(" again to edit previous message");
} else {
- " esc to edit previous message"
+ line.extend(vec![
+ " ".into(),
+ key_hint::plain(KeyCode::Esc).into(),
+ " to edit previous message".into(),
+ ]);
}
}
- _ => self.label,
+ _ => line.push_span(self.label),
};
- let text = format!("{}{}{}", self.prefix, binding.overlay_text, label);
- Some(text)
+ Some(line)
}
}
@@ -309,9 +310,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
ShortcutDescriptor {
id: ShortcutId::Commands,
bindings: &[ShortcutBinding {
- code: KeyCode::Char('/'),
- modifiers: KeyModifiers::NONE,
- overlay_text: "/",
+ key: key_hint::plain(KeyCode::Char('/')),
condition: DisplayCondition::Always,
}],
prefix: "",
@@ -321,15 +320,11 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
id: ShortcutId::InsertNewline,
bindings: &[
ShortcutBinding {
- code: KeyCode::Enter,
- modifiers: KeyModifiers::SHIFT,
- overlay_text: "shift + enter",
+ key: key_hint::shift(KeyCode::Enter),
condition: DisplayCondition::WhenShiftEnterHint,
},
ShortcutBinding {
- code: KeyCode::Char('j'),
- modifiers: KeyModifiers::CONTROL,
- overlay_text: "ctrl + j",
+ key: key_hint::ctrl(KeyCode::Char('j')),
condition: DisplayCondition::WhenNotShiftEnterHint,
},
],
@@ -339,9 +334,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
ShortcutDescriptor {
id: ShortcutId::FilePaths,
bindings: &[ShortcutBinding {
- code: KeyCode::Char('@'),
- modifiers: KeyModifiers::NONE,
- overlay_text: "@",
+ key: key_hint::plain(KeyCode::Char('@')),
condition: DisplayCondition::Always,
}],
prefix: "",
@@ -350,9 +343,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
ShortcutDescriptor {
id: ShortcutId::PasteImage,
bindings: &[ShortcutBinding {
- code: KeyCode::Char('v'),
- modifiers: KeyModifiers::CONTROL,
- overlay_text: "ctrl + v",
+ key: key_hint::ctrl(KeyCode::Char('v')),
condition: DisplayCondition::Always,
}],
prefix: "",
@@ -361,9 +352,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
ShortcutDescriptor {
id: ShortcutId::EditPrevious,
bindings: &[ShortcutBinding {
- code: KeyCode::Esc,
- modifiers: KeyModifiers::NONE,
- overlay_text: "esc",
+ key: key_hint::plain(KeyCode::Esc),
condition: DisplayCondition::Always,
}],
prefix: "",
@@ -372,9 +361,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
ShortcutDescriptor {
id: ShortcutId::Quit,
bindings: &[ShortcutBinding {
- code: KeyCode::Char('c'),
- modifiers: KeyModifiers::CONTROL,
- overlay_text: "ctrl + c",
+ key: key_hint::ctrl(KeyCode::Char('c')),
condition: DisplayCondition::Always,
}],
prefix: "",
@@ -383,9 +370,7 @@ const SHORTCUTS: &[ShortcutDescriptor] = &[
ShortcutDescriptor {
id: ShortcutId::ShowTranscript,
bindings: &[ShortcutBinding {
- code: KeyCode::Char('t'),
- modifiers: KeyModifiers::CONTROL,
- overlay_text: "ctrl + t",
+ key: key_hint::ctrl(KeyCode::Char('t')),
condition: DisplayCondition::Always,
}],
prefix: "",
diff --git a/codex-rs/tui/src/bottom_pane/list_selection_view.rs b/codex-rs/tui/src/bottom_pane/list_selection_view.rs
index e2614885a..cb3de2122 100644
--- a/codex-rs/tui/src/bottom_pane/list_selection_view.rs
+++ b/codex-rs/tui/src/bottom_pane/list_selection_view.rs
@@ -43,7 +43,7 @@ pub(crate) struct SelectionItem {
pub(crate) struct SelectionViewParams {
pub title: Option,
pub subtitle: Option,
- pub footer_hint: Option,
+ pub footer_hint: Option>,
pub items: Vec,
pub is_searchable: bool,
pub search_placeholder: Option,
@@ -65,7 +65,7 @@ impl Default for SelectionViewParams {
}
pub(crate) struct ListSelectionView {
- footer_hint: Option,
+ footer_hint: Option>,
items: Vec,
state: ScrollState,
complete: bool,
@@ -416,7 +416,7 @@ impl Renderable for ListSelectionView {
width: footer_area.width.saturating_sub(2),
height: footer_area.height,
};
- Line::from(hint.clone().dim()).render(hint_area, buf);
+ hint.clone().dim().render(hint_area, buf);
}
}
}
@@ -425,7 +425,7 @@ impl Renderable for ListSelectionView {
mod tests {
use super::*;
use crate::app_event::AppEvent;
- use crate::bottom_pane::popup_consts::STANDARD_POPUP_HINT_LINE;
+ use crate::bottom_pane::popup_consts::standard_popup_hint_line;
use insta::assert_snapshot;
use ratatui::layout::Rect;
use tokio::sync::mpsc::unbounded_channel;
@@ -455,7 +455,7 @@ mod tests {
SelectionViewParams {
title: Some("Select Approval Mode".to_string()),
subtitle: subtitle.map(str::to_string),
- footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
+ footer_hint: Some(standard_popup_hint_line()),
items,
..Default::default()
},
@@ -517,7 +517,7 @@ mod tests {
let mut view = ListSelectionView::new(
SelectionViewParams {
title: Some("Select Approval Mode".to_string()),
- footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
+ footer_hint: Some(standard_popup_hint_line()),
items,
is_searchable: true,
search_placeholder: Some("Type to search branches".to_string()),
diff --git a/codex-rs/tui/src/bottom_pane/popup_consts.rs b/codex-rs/tui/src/bottom_pane/popup_consts.rs
index 5147b2ee5..2cabe389b 100644
--- a/codex-rs/tui/src/bottom_pane/popup_consts.rs
+++ b/codex-rs/tui/src/bottom_pane/popup_consts.rs
@@ -1,8 +1,21 @@
//! Shared popup-related constants for bottom pane widgets.
+use crossterm::event::KeyCode;
+use ratatui::text::Line;
+
+use crate::key_hint;
+
/// Maximum number of rows any popup should attempt to display.
/// Keep this consistent across all popups for a uniform feel.
pub(crate) const MAX_POPUP_ROWS: usize = 8;
/// Standard footer hint text used by popups.
-pub(crate) const STANDARD_POPUP_HINT_LINE: &str = "Press Enter to confirm or Esc to go back";
+pub(crate) fn standard_popup_hint_line() -> Line<'static> {
+ Line::from(vec![
+ "Press ".into(),
+ key_hint::plain(KeyCode::Enter).into(),
+ " to confirm or ".into(),
+ key_hint::plain(KeyCode::Esc).into(),
+ " to go back".into(),
+ ])
+}
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 170dedc01..3b6782d06 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
@@ -1,6 +1,5 @@
---
source: tui/src/bottom_pane/chat_composer.rs
-assertion_line: 1497
expression: terminal.backend()
---
" "
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 ffa4c5b04..264515a6c 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
@@ -1,6 +1,5 @@
---
source: tui/src/bottom_pane/footer.rs
-assertion_line: 389
expression: terminal.backend()
---
" / for commands shift + enter for newline "
diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap
index 9cbfe88f7..512f6bbca 100644
--- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap
+++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_with_subtitle.snap
@@ -9,4 +9,4 @@ expression: render_lines(&view)
› 1. Read Only (current) Codex can read files
2. Full Access Codex can edit files
- Press Enter to confirm or Esc to go back
+ Press enter to confirm or esc to go back
diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap
index 5e3cf2c6e..ddd0f90cd 100644
--- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap
+++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__list_selection_view__tests__list_selection_spacing_without_subtitle.snap
@@ -8,4 +8,4 @@ expression: render_lines(&view)
› 1. Read Only (current) Codex can read files
2. Full Access Codex can edit files
- Press Enter to confirm or Esc to go back
+ Press enter to confirm or esc to go back
diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs
index 9940c8f7d..32566b3ec 100644
--- a/codex-rs/tui/src/chatwidget.rs
+++ b/codex-rs/tui/src/chatwidget.rs
@@ -68,7 +68,7 @@ use crate::bottom_pane::SelectionAction;
use crate::bottom_pane::SelectionItem;
use crate::bottom_pane::SelectionViewParams;
use crate::bottom_pane::custom_prompt_view::CustomPromptView;
-use crate::bottom_pane::popup_consts::STANDARD_POPUP_HINT_LINE;
+use crate::bottom_pane::popup_consts::standard_popup_hint_line;
use crate::clipboard_paste::paste_image_to_temp_png;
use crate::diff_render::display_path_for;
use crate::exec_cell::CommandOutput;
@@ -1625,7 +1625,7 @@ impl ChatWidget {
subtitle: Some(
"Switch between OpenAI models for this and future Codex CLI session".to_string(),
),
- footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
+ footer_hint: Some(standard_popup_hint_line()),
items,
..Default::default()
});
@@ -1668,7 +1668,7 @@ impl ChatWidget {
self.bottom_pane.show_selection_view(SelectionViewParams {
title: Some("Select Approval Mode".to_string()),
- footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
+ footer_hint: Some(standard_popup_hint_line()),
items,
..Default::default()
});
@@ -1843,7 +1843,7 @@ impl ChatWidget {
self.bottom_pane.show_selection_view(SelectionViewParams {
title: Some("Select a review preset".into()),
- footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
+ footer_hint: Some(standard_popup_hint_line()),
items,
..Default::default()
});
@@ -1879,7 +1879,7 @@ impl ChatWidget {
self.bottom_pane.show_selection_view(SelectionViewParams {
title: Some("Select a base branch".to_string()),
- footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
+ footer_hint: Some(standard_popup_hint_line()),
items,
is_searchable: true,
search_placeholder: Some("Type to search branches".to_string()),
@@ -1920,7 +1920,7 @@ impl ChatWidget {
self.bottom_pane.show_selection_view(SelectionViewParams {
title: Some("Select a commit to review".to_string()),
- footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
+ footer_hint: Some(standard_popup_hint_line()),
items,
is_searchable: true,
search_placeholder: Some("Type to search commits".to_string()),
@@ -2145,7 +2145,7 @@ pub(crate) fn show_review_commit_picker_with_entries(
chat.bottom_pane.show_selection_view(SelectionViewParams {
title: Some("Select a commit to review".to_string()),
- footer_hint: Some(STANDARD_POPUP_HINT_LINE.to_string()),
+ footer_hint: Some(standard_popup_hint_line()),
items,
is_searchable: true,
search_placeholder: Some("Type to search commits".to_string()),
diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap
index 10ded3d33..558728bd3 100644
--- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap
+++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec.snap
@@ -13,4 +13,4 @@ expression: terminal.backend().vt100().screen().contents()
rest of the session
3. Cancel Do not run the command
- Press Enter to confirm or Esc to cancel
+ Press enter to confirm or esc to cancel
diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_no_reason.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_no_reason.snap
index c3e04bd15..53348af15 100644
--- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_no_reason.snap
+++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_exec_no_reason.snap
@@ -13,5 +13,5 @@ expression: terminal.backend()
" rest of the session "
" 3. Cancel Do not run the command "
" "
-" Press Enter to confirm or Esc to cancel "
+" Press enter to confirm or esc to cancel "
" "
diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_patch.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_patch.snap
index fd1860caf..64fbada3b 100644
--- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_patch.snap
+++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approval_modal_patch.snap
@@ -15,5 +15,5 @@ expression: terminal.backend()
"› 1. Approve Apply the proposed changes "
" 2. Cancel Do not apply the changes "
" "
-" Press Enter to confirm or Esc to cancel "
+" Press enter to confirm or esc to cancel "
" "
diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_running_h2.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_running_h2.snap
index 5164cd455..3b236fff1 100644
--- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_running_h2.snap
+++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_running_h2.snap
@@ -2,5 +2,5 @@
source: tui/src/chatwidget/tests.rs
expression: terminal.backend()
---
-" Thinking (0s • Esc to interrupt) "
+" Thinking (0s • esc to interrupt) "
"› Ask Codex to do anything "
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 e3f7f532a..a05ef2799 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
@@ -9,7 +9,7 @@ expression: term.backend().vt100().screen().contents()
└ Search Change Approved
Read diff_render.rs
- Investigating rendering code (0s • Esc to interrupt)
+ Investigating rendering code (0s • esc to interrupt)
› Summarize recent commits
diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__exec_approval_modal_exec.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__exec_approval_modal_exec.snap
index 5beb5323a..8b7c5838c 100644
--- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__exec_approval_modal_exec.snap
+++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__exec_approval_modal_exec.snap
@@ -18,7 +18,7 @@ Buffer {
" rest of the session ",
" 3. Cancel Do not run the command ",
" ",
- " Press Enter to confirm or Esc to cancel ",
+ " Press enter to confirm or esc to cancel ",
" ",
],
styles: [
@@ -37,6 +37,6 @@ Buffer {
x: 34, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 56, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 2, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
- x: 41, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
+ x: 0, y: 14, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}
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 e4ae90ba9..465774602 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
@@ -3,7 +3,7 @@ source: tui/src/chatwidget/tests.rs
expression: terminal.backend()
---
" "
-" Analyzing (0s • Esc to interrupt) "
+" Analyzing (0s • esc to interrupt) "
" "
" "
"› Ask Codex to do anything "
diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_and_approval_modal.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_and_approval_modal.snap
index 86d89b80f..96b51f7a3 100644
--- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_and_approval_modal.snap
+++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_and_approval_modal.snap
@@ -15,5 +15,5 @@ expression: terminal.backend()
" rest of the session "
" 3. Cancel Do not run the command "
" "
-" Press Enter to confirm or Esc to cancel "
+" Press enter to confirm or esc to cancel "
" "
diff --git a/codex-rs/tui/src/key_hint.rs b/codex-rs/tui/src/key_hint.rs
index f68a493e9..b79c2a270 100644
--- a/codex-rs/tui/src/key_hint.rs
+++ b/codex-rs/tui/src/key_hint.rs
@@ -1,23 +1,86 @@
+use crossterm::event::KeyCode;
+use crossterm::event::KeyEvent;
+use crossterm::event::KeyEventKind;
+use crossterm::event::KeyModifiers;
use ratatui::style::Style;
use ratatui::style::Stylize;
use ratatui::text::Span;
-use std::fmt::Display;
-#[cfg(test)]
-const ALT_PREFIX: &str = "⌥";
-#[cfg(all(not(test), target_os = "macos"))]
-const ALT_PREFIX: &str = "⌥";
-#[cfg(all(not(test), not(target_os = "macos")))]
-const ALT_PREFIX: &str = "Alt+";
+const ALT_PREFIX: &str = "alt + ";
+const CTRL_PREFIX: &str = "ctrl + ";
+const SHIFT_PREFIX: &str = "shift + ";
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub(crate) struct KeyBinding {
+ key: KeyCode,
+ modifiers: KeyModifiers,
+}
+
+impl KeyBinding {
+ pub(crate) const fn new(key: KeyCode, modifiers: KeyModifiers) -> Self {
+ Self { key, modifiers }
+ }
+
+ pub fn is_press(&self, event: KeyEvent) -> bool {
+ self.key == event.code
+ && self.modifiers == event.modifiers
+ && (event.kind == KeyEventKind::Press || event.kind == KeyEventKind::Repeat)
+ }
+}
+
+pub(crate) const fn plain(key: KeyCode) -> KeyBinding {
+ KeyBinding::new(key, KeyModifiers::NONE)
+}
+
+pub(crate) const fn alt(key: KeyCode) -> KeyBinding {
+ KeyBinding::new(key, KeyModifiers::ALT)
+}
+
+pub(crate) const fn shift(key: KeyCode) -> KeyBinding {
+ KeyBinding::new(key, KeyModifiers::SHIFT)
+}
+
+pub(crate) const fn ctrl(key: KeyCode) -> KeyBinding {
+ KeyBinding::new(key, KeyModifiers::CONTROL)
+}
+
+fn modifiers_to_string(modifiers: KeyModifiers) -> String {
+ let mut result = String::new();
+ if modifiers.contains(KeyModifiers::CONTROL) {
+ result.push_str(CTRL_PREFIX);
+ }
+ if modifiers.contains(KeyModifiers::SHIFT) {
+ result.push_str(SHIFT_PREFIX);
+ }
+ if modifiers.contains(KeyModifiers::ALT) {
+ result.push_str(ALT_PREFIX);
+ }
+ result
+}
+
+impl From for Span<'static> {
+ fn from(binding: KeyBinding) -> Self {
+ (&binding).into()
+ }
+}
+impl From<&KeyBinding> for Span<'static> {
+ fn from(binding: &KeyBinding) -> Self {
+ let KeyBinding { key, modifiers } = binding;
+ let modifiers = modifiers_to_string(*modifiers);
+ let key = match key {
+ KeyCode::Enter => "enter".to_string(),
+ KeyCode::Up => "↑".to_string(),
+ KeyCode::Down => "↓".to_string(),
+ KeyCode::Left => "←".to_string(),
+ KeyCode::Right => "→".to_string(),
+ KeyCode::PageUp => "pgup".to_string(),
+ KeyCode::PageDown => "pgdn".to_string(),
+ _ => format!("{key}").to_ascii_lowercase(),
+ };
+ Span::styled(format!("{modifiers}{key}"), key_hint_style())
+ }
+}
fn key_hint_style() -> Style {
- Style::default().bold()
-}
-
-fn modifier_span(prefix: &str, key: impl Display) -> Span<'static> {
- Span::styled(format!("{prefix}{key}"), key_hint_style())
-}
-
-pub(crate) fn alt(key: impl Display) -> Span<'static> {
- modifier_span(ALT_PREFIX, key)
+ Style::default().dim()
}
diff --git a/codex-rs/tui/src/pager_overlay.rs b/codex-rs/tui/src/pager_overlay.rs
index 6a0562fe8..7997625aa 100644
--- a/codex-rs/tui/src/pager_overlay.rs
+++ b/codex-rs/tui/src/pager_overlay.rs
@@ -3,18 +3,16 @@ use std::sync::Arc;
use std::time::Duration;
use crate::history_cell::HistoryCell;
+use crate::key_hint;
+use crate::key_hint::KeyBinding;
use crate::render::renderable::Renderable;
use crate::tui;
use crate::tui::TuiEvent;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
-use crossterm::event::KeyEventKind;
use ratatui::buffer::Buffer;
use ratatui::buffer::Cell;
use ratatui::layout::Rect;
-use ratatui::style::Color;
-use ratatui::style::Style;
-use ratatui::style::Styled;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::text::Span;
@@ -61,23 +59,40 @@ impl Overlay {
}
}
+const KEY_UP: KeyBinding = key_hint::plain(KeyCode::Up);
+const KEY_DOWN: KeyBinding = key_hint::plain(KeyCode::Down);
+const KEY_PAGE_UP: KeyBinding = key_hint::plain(KeyCode::PageUp);
+const KEY_PAGE_DOWN: KeyBinding = key_hint::plain(KeyCode::PageDown);
+const KEY_SPACE: KeyBinding = key_hint::plain(KeyCode::Char(' '));
+const KEY_HOME: KeyBinding = key_hint::plain(KeyCode::Home);
+const KEY_END: KeyBinding = key_hint::plain(KeyCode::End);
+const KEY_Q: KeyBinding = key_hint::plain(KeyCode::Char('q'));
+const KEY_ESC: KeyBinding = key_hint::plain(KeyCode::Esc);
+const KEY_ENTER: KeyBinding = key_hint::plain(KeyCode::Enter);
+const KEY_CTRL_T: KeyBinding = key_hint::ctrl(KeyCode::Char('t'));
+const KEY_CTRL_C: KeyBinding = key_hint::ctrl(KeyCode::Char('c'));
+
// Common pager navigation hints rendered on the first line
-const PAGER_KEY_HINTS: &[(&str, &str)] = &[
- ("↑/↓", "scroll"),
- ("PgUp/PgDn", "page"),
- ("Home/End", "jump"),
+const PAGER_KEY_HINTS: &[(&[KeyBinding], &str)] = &[
+ (&[KEY_UP, KEY_DOWN], "to scroll"),
+ (&[KEY_PAGE_UP, KEY_PAGE_DOWN], "to page"),
+ (&[KEY_HOME, KEY_END], "to jump"),
];
-// Render a single line of key hints from (key, description) pairs.
-fn render_key_hints(area: Rect, buf: &mut Buffer, pairs: &[(&str, &str)]) {
- let key_hint_style = Style::default().fg(Color::Cyan);
+// Render a single line of key hints from (key(s), description) pairs.
+fn render_key_hints(area: Rect, buf: &mut Buffer, pairs: &[(&[KeyBinding], &str)]) {
let mut spans: Vec> = vec![" ".into()];
let mut first = true;
- for (key, desc) in pairs {
+ for (keys, desc) in pairs {
if !first {
spans.push(" ".into());
}
- spans.push(Span::from(key.to_string()).set_style(key_hint_style));
+ for (i, key) in keys.iter().enumerate() {
+ if i > 0 {
+ spans.push("/".into());
+ }
+ spans.push(Span::from(key));
+ }
spans.push(" ".into());
spans.push(Span::from(desc.to_string()));
first = false;
@@ -214,48 +229,24 @@ impl PagerView {
fn handle_key_event(&mut self, tui: &mut tui::Tui, key_event: KeyEvent) -> Result<()> {
match key_event {
- KeyEvent {
- code: KeyCode::Up,
- kind: KeyEventKind::Press | KeyEventKind::Repeat,
- ..
- } => {
+ e if KEY_UP.is_press(e) => {
self.scroll_offset = self.scroll_offset.saturating_sub(1);
}
- KeyEvent {
- code: KeyCode::Down,
- kind: KeyEventKind::Press | KeyEventKind::Repeat,
- ..
- } => {
+ e if KEY_DOWN.is_press(e) => {
self.scroll_offset = self.scroll_offset.saturating_add(1);
}
- KeyEvent {
- code: KeyCode::PageUp,
- kind: KeyEventKind::Press | KeyEventKind::Repeat,
- ..
- } => {
+ e if KEY_PAGE_UP.is_press(e) => {
let area = self.content_area(tui.terminal.viewport_area);
self.scroll_offset = self.scroll_offset.saturating_sub(area.height as usize);
}
- KeyEvent {
- code: KeyCode::PageDown | KeyCode::Char(' '),
- kind: KeyEventKind::Press | KeyEventKind::Repeat,
- ..
- } => {
+ e if KEY_PAGE_DOWN.is_press(e) || KEY_SPACE.is_press(e) => {
let area = self.content_area(tui.terminal.viewport_area);
self.scroll_offset = self.scroll_offset.saturating_add(area.height as usize);
}
- KeyEvent {
- code: KeyCode::Home,
- kind: KeyEventKind::Press | KeyEventKind::Repeat,
- ..
- } => {
+ e if KEY_HOME.is_press(e) => {
self.scroll_offset = 0;
}
- KeyEvent {
- code: KeyCode::End,
- kind: KeyEventKind::Press | KeyEventKind::Repeat,
- ..
- } => {
+ e if KEY_END.is_press(e) => {
self.scroll_offset = usize::MAX;
}
_ => {
@@ -434,9 +425,11 @@ impl TranscriptOverlay {
let line1 = Rect::new(area.x, area.y, area.width, 1);
let line2 = Rect::new(area.x, area.y.saturating_add(1), area.width, 1);
render_key_hints(line1, buf, PAGER_KEY_HINTS);
- let mut pairs: Vec<(&str, &str)> = vec![("q", "quit"), ("Esc", "edit prev")];
+
+ let mut pairs: Vec<(&[KeyBinding], &str)> =
+ vec![(&[KEY_Q], "to quit"), (&[KEY_ESC], "to edit prev")];
if self.highlight_cell.is_some() {
- pairs.push(("⏎", "edit message"));
+ pairs.push((&[KEY_ENTER], "to edit message"));
}
render_key_hints(line2, buf, &pairs);
}
@@ -454,23 +447,7 @@ impl TranscriptOverlay {
pub(crate) fn handle_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()> {
match event {
TuiEvent::Key(key_event) => match key_event {
- KeyEvent {
- code: KeyCode::Char('q'),
- kind: KeyEventKind::Press,
- ..
- }
- | KeyEvent {
- code: KeyCode::Char('t'),
- modifiers: crossterm::event::KeyModifiers::CONTROL,
- kind: KeyEventKind::Press,
- ..
- }
- | KeyEvent {
- code: KeyCode::Char('c'),
- modifiers: crossterm::event::KeyModifiers::CONTROL,
- kind: KeyEventKind::Press,
- ..
- } => {
+ e if KEY_Q.is_press(e) || KEY_CTRL_C.is_press(e) || KEY_CTRL_T.is_press(e) => {
self.is_done = true;
Ok(())
}
@@ -516,7 +493,7 @@ impl StaticOverlay {
let line1 = Rect::new(area.x, area.y, area.width, 1);
let line2 = Rect::new(area.x, area.y.saturating_add(1), area.width, 1);
render_key_hints(line1, buf, PAGER_KEY_HINTS);
- let pairs = [("q", "quit")];
+ let pairs: Vec<(&[KeyBinding], &str)> = vec![(&[KEY_Q], "to quit")];
render_key_hints(line2, buf, &pairs);
}
@@ -533,17 +510,7 @@ impl StaticOverlay {
pub(crate) fn handle_event(&mut self, tui: &mut tui::Tui, event: TuiEvent) -> Result<()> {
match event {
TuiEvent::Key(key_event) => match key_event {
- KeyEvent {
- code: KeyCode::Char('q'),
- kind: KeyEventKind::Press,
- ..
- }
- | KeyEvent {
- code: KeyCode::Char('c'),
- modifiers: crossterm::event::KeyModifiers::CONTROL,
- kind: KeyEventKind::Press,
- ..
- } => {
+ e if KEY_Q.is_press(e) || KEY_CTRL_C.is_press(e) => {
self.is_done = true;
Ok(())
}
diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs
index 9a8e8f408..ded7d41af 100644
--- a/codex-rs/tui/src/resume_picker.rs
+++ b/codex-rs/tui/src/resume_picker.rs
@@ -24,6 +24,7 @@ use tokio_stream::StreamExt;
use tokio_stream::wrappers::UnboundedReceiverStream;
use unicode_width::UnicodeWidthStr;
+use crate::key_hint;
use crate::text_formatting::truncate_text;
use crate::tui::FrameRequester;
use crate::tui::Tui;
@@ -678,16 +679,18 @@ fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
// Hint line
let hint_line: Line = vec![
- "Enter".bold(),
- " to resume ".into(),
- "• ".dim(),
- "Esc".bold(),
- " to start new ".into(),
- "• ".dim(),
- "Ctrl+C".into(),
- " to quit ".into(),
- "• ".dim(),
- "↑/↓".into(),
+ key_hint::plain(KeyCode::Enter).into(),
+ " to resume ".dim(),
+ " ".dim(),
+ key_hint::plain(KeyCode::Esc).into(),
+ " to start new ".dim(),
+ " ".dim(),
+ key_hint::ctrl(KeyCode::Char('c')).into(),
+ " to quit ".dim(),
+ " ".dim(),
+ key_hint::plain(KeyCode::Up).into(),
+ "/".dim(),
+ key_hint::plain(KeyCode::Down).into(),
" to browse".dim(),
]
.into();
diff --git a/codex-rs/tui/src/snapshots/codex_tui__pager_overlay__tests__static_overlay_snapshot_basic.snap b/codex-rs/tui/src/snapshots/codex_tui__pager_overlay__tests__static_overlay_snapshot_basic.snap
index ee65b04d7..bc7818aff 100644
--- a/codex-rs/tui/src/snapshots/codex_tui__pager_overlay__tests__static_overlay_snapshot_basic.snap
+++ b/codex-rs/tui/src/snapshots/codex_tui__pager_overlay__tests__static_overlay_snapshot_basic.snap
@@ -9,6 +9,6 @@ expression: term.backend()
"~ "
"~ "
"───────────────────────────────── 100% ─"
-" ↑/↓ scroll PgUp/PgDn page Home/End "
-" q quit "
+" ↑/↓ to scroll pgup/pgdn to page hom"
+" q to quit "
" "
diff --git a/codex-rs/tui/src/snapshots/codex_tui__pager_overlay__tests__transcript_overlay_apply_patch_scroll_vt100.snap b/codex-rs/tui/src/snapshots/codex_tui__pager_overlay__tests__transcript_overlay_apply_patch_scroll_vt100.snap
index df5f17adc..7beca4a4c 100644
--- a/codex-rs/tui/src/snapshots/codex_tui__pager_overlay__tests__transcript_overlay_apply_patch_scroll_vt100.snap
+++ b/codex-rs/tui/src/snapshots/codex_tui__pager_overlay__tests__transcript_overlay_apply_patch_scroll_vt100.snap
@@ -11,5 +11,5 @@ expression: snapshot
1 +hello
2 +world
─────────────────────────────────────────────────────────────────────────── 0% ─
- ↑/↓ scroll PgUp/PgDn page Home/End jump
- q quit Esc edit prev
+ ↑/↓ to scroll pgup/pgdn to page home/end to jump
+ q to quit esc to edit prev
diff --git a/codex-rs/tui/src/snapshots/codex_tui__pager_overlay__tests__transcript_overlay_snapshot_basic.snap b/codex-rs/tui/src/snapshots/codex_tui__pager_overlay__tests__transcript_overlay_snapshot_basic.snap
index 0c03edef4..61fcf7d2e 100644
--- a/codex-rs/tui/src/snapshots/codex_tui__pager_overlay__tests__transcript_overlay_snapshot_basic.snap
+++ b/codex-rs/tui/src/snapshots/codex_tui__pager_overlay__tests__transcript_overlay_snapshot_basic.snap
@@ -9,6 +9,6 @@ expression: term.backend()
" "
"gamma "
"───────────────────────────────── 100% ─"
-" ↑/↓ scroll PgUp/PgDn page Home/End "
-" q quit Esc edit prev "
+" ↑/↓ to scroll pgup/pgdn to page hom"
+" q to quit esc to edit prev "
" "
diff --git a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_truncated.snap b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_truncated.snap
index 6aa340177..95dbc33bb 100644
--- a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_truncated.snap
+++ b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_truncated.snap
@@ -2,5 +2,5 @@
source: tui/src/status_indicator_widget.rs
expression: terminal.backend()
---
-" Working (0s • Esc "
+" Working (0s • esc "
" "
diff --git a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_queued_messages.snap b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_queued_messages.snap
index 061f3a139..7df7e5de7 100644
--- a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_queued_messages.snap
+++ b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_queued_messages.snap
@@ -2,11 +2,11 @@
source: tui/src/status_indicator_widget.rs
expression: terminal.backend()
---
-" Working (0s • Esc to interrupt) "
+" Working (0s • esc to interrupt) "
" "
" ↳ first "
" ↳ second "
-" ⌥↑ edit "
+" alt + ↑ edit "
" "
" "
" "
diff --git a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_working_header.snap b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_working_header.snap
index debf28211..c02e83263 100644
--- a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_working_header.snap
+++ b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_working_header.snap
@@ -2,5 +2,5 @@
source: tui/src/status_indicator_widget.rs
expression: terminal.backend()
---
-" Working (0s • Esc to interrupt) "
+" Working (0s • esc to interrupt) "
" "
diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs
index afb795206..caf15b288 100644
--- a/codex-rs/tui/src/status_indicator_widget.rs
+++ b/codex-rs/tui/src/status_indicator_widget.rs
@@ -5,6 +5,7 @@ use std::time::Duration;
use std::time::Instant;
use codex_core::protocol::Op;
+use crossterm::event::KeyCode;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Stylize;
@@ -164,7 +165,7 @@ impl WidgetRef for StatusIndicatorWidget {
spans.extend(vec![
" ".into(),
format!("({pretty_elapsed} • ").dim(),
- "Esc".dim().bold(),
+ key_hint::plain(KeyCode::Esc).into(),
" to interrupt)".dim(),
]);
@@ -188,8 +189,14 @@ impl WidgetRef for StatusIndicatorWidget {
}
}
if !self.queued_messages.is_empty() {
- let shortcut = key_hint::alt("↑");
- lines.push(Line::from(vec![" ".into(), shortcut, " edit".into()]).dim());
+ lines.push(
+ Line::from(vec![
+ " ".into(),
+ key_hint::alt(KeyCode::Up).into(),
+ " edit".into(),
+ ])
+ .dim(),
+ );
}
let paragraph = Paragraph::new(lines);