Refactor the footer logic to a new file (#4259)

This will help us have more control over the footer

---------

Co-authored-by: pakrym-oai <pakrym@openai.com>
This commit is contained in:
Ahmed Ibrahim
2025-09-26 07:13:13 -07:00
committed by GitHub
parent 1fc3413a46
commit 02609184be
7 changed files with 428 additions and 74 deletions

View File

@@ -1,5 +1,4 @@
use codex_core::protocol::TokenUsageInfo;
use codex_protocol::num_format::format_si_suffix;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
@@ -25,6 +24,8 @@ use super::chat_composer_history::ChatComposerHistory;
use super::command_popup::CommandItem;
use super::command_popup::CommandPopup;
use super::file_search_popup::FileSearchPopup;
use super::footer::FooterProps;
use super::footer::render_footer;
use super::paste_burst::CharDecision;
use super::paste_burst::PasteBurst;
use crate::bottom_pane::paste_burst::FlushResult;
@@ -37,7 +38,6 @@ use crate::bottom_pane::textarea::TextArea;
use crate::bottom_pane::textarea::TextAreaState;
use crate::clipboard_paste::normalize_pasted_path;
use crate::clipboard_paste::pasted_image_format;
use crate::key_hint;
use crate::ui_consts::LIVE_PREFIX_COLS;
use codex_file_search::FileMatch;
use std::cell::RefCell;
@@ -1263,78 +1263,17 @@ impl WidgetRef for ChatComposer {
} else {
popup_rect
};
let mut hint: Vec<Span<'static>> = if self.ctrl_c_quit_hint {
let ctrl_c_followup = if self.is_task_running {
" to interrupt"
} else {
" to quit"
};
vec![
" ".into(),
key_hint::ctrl('C'),
" again".into(),
ctrl_c_followup.into(),
]
} else {
let newline_hint_key = if self.use_shift_enter_hint {
key_hint::shift('⏎')
} else {
key_hint::ctrl('J')
};
vec![
key_hint::plain('⏎'),
" send ".into(),
newline_hint_key,
" newline ".into(),
key_hint::ctrl('T'),
" transcript ".into(),
key_hint::ctrl('C'),
" quit".into(),
]
};
if !self.ctrl_c_quit_hint && self.esc_backtrack_hint {
hint.push(" ".into());
hint.push(key_hint::plain("Esc"));
hint.push(" edit prev".into());
}
// Append token/context usage info to the footer hints when available.
if let Some(token_usage_info) = &self.token_usage_info {
let token_usage = &token_usage_info.total_token_usage;
hint.push(" ".into());
hint.push(
Span::from(format!(
"{} tokens used",
format_si_suffix(token_usage.blended_total())
))
.style(Style::default().add_modifier(Modifier::DIM)),
);
let last_token_usage = &token_usage_info.last_token_usage;
if let Some(context_window) = token_usage_info.model_context_window {
let percent_remaining: u8 = if context_window > 0 {
last_token_usage.percent_of_context_window_remaining(context_window)
} else {
100
};
let context_style = if percent_remaining < 20 {
Style::default().fg(Color::Yellow)
} else {
Style::default().add_modifier(Modifier::DIM)
};
hint.push(" ".into());
hint.push(Span::styled(
format!("{percent_remaining}% context left"),
context_style,
));
}
}
let hint = hint
.into_iter()
.map(|span| span.patch_style(Style::default().dim()))
.collect::<Vec<_>>();
Line::from(hint).render_ref(hint_rect, buf);
render_footer(
hint_rect,
buf,
FooterProps {
ctrl_c_quit_hint: self.ctrl_c_quit_hint,
is_task_running: self.is_task_running,
esc_backtrack_hint: self.esc_backtrack_hint,
use_shift_enter_hint: self.use_shift_enter_hint,
token_usage_info: self.token_usage_info.as_ref(),
},
);
}
}
let border_style = if self.has_focus {

View File

@@ -0,0 +1,386 @@
use codex_core::protocol::TokenUsageInfo;
use codex_protocol::num_format::format_si_suffix;
use crossterm::event::KeyCode;
use crossterm::event::KeyModifiers;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::WidgetRef;
use crate::key_hint;
#[derive(Clone, Copy, Debug)]
pub(crate) struct FooterProps<'a> {
pub(crate) ctrl_c_quit_hint: bool,
pub(crate) is_task_running: bool,
pub(crate) esc_backtrack_hint: bool,
pub(crate) use_shift_enter_hint: bool,
pub(crate) token_usage_info: Option<&'a TokenUsageInfo>,
}
#[derive(Clone, Copy, Debug)]
struct CtrlCReminderState {
pub(crate) is_task_running: bool,
}
#[derive(Clone, Copy, Debug)]
struct ShortcutsState {
pub(crate) use_shift_enter_hint: bool,
pub(crate) esc_backtrack_hint: bool,
}
#[derive(Clone, Copy, Debug)]
enum FooterContent {
Shortcuts(ShortcutsState),
CtrlCReminder(CtrlCReminderState),
}
pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps<'_>) {
let content = if props.ctrl_c_quit_hint {
FooterContent::CtrlCReminder(CtrlCReminderState {
is_task_running: props.is_task_running,
})
} else {
FooterContent::Shortcuts(ShortcutsState {
use_shift_enter_hint: props.use_shift_enter_hint,
esc_backtrack_hint: props.esc_backtrack_hint,
})
};
let mut spans = footer_spans(content);
if let Some(token_usage_info) = props.token_usage_info {
append_token_usage_spans(&mut spans, token_usage_info);
}
let spans = spans
.into_iter()
.map(|span| span.patch_style(Style::default().dim()))
.collect::<Vec<_>>();
Line::from(spans).render_ref(area, buf);
}
fn footer_spans(content: FooterContent) -> Vec<Span<'static>> {
match content {
FooterContent::Shortcuts(state) => shortcuts_spans(state),
FooterContent::CtrlCReminder(state) => ctrl_c_reminder_spans(state),
}
}
fn append_token_usage_spans(spans: &mut Vec<Span<'static>>, token_usage_info: &TokenUsageInfo) {
let token_usage = &token_usage_info.total_token_usage;
spans.push(" ".into());
spans.push(
Span::from(format!(
"{} tokens used",
format_si_suffix(token_usage.blended_total())
))
.style(Style::default().add_modifier(Modifier::DIM)),
);
let last_token_usage = &token_usage_info.last_token_usage;
if let Some(context_window) = token_usage_info.model_context_window {
let percent_remaining: u8 = if context_window > 0 {
last_token_usage.percent_of_context_window_remaining(context_window)
} else {
100
};
let context_style = if percent_remaining < 20 {
Style::default().fg(Color::Yellow)
} else {
Style::default().add_modifier(Modifier::DIM)
};
spans.push(" ".into());
spans.push(Span::styled(
format!("{percent_remaining}% context left"),
context_style,
));
}
}
fn shortcuts_spans(state: ShortcutsState) -> Vec<Span<'static>> {
let mut spans = Vec::new();
for descriptor in SHORTCUTS {
if let Some(segment) = descriptor.footer_segment(state) {
if !segment.prefix.is_empty() {
spans.push(segment.prefix.into());
}
spans.push(segment.binding.span());
spans.push(segment.label.into());
}
}
spans
}
fn ctrl_c_reminder_spans(state: CtrlCReminderState) -> Vec<Span<'static>> {
let followup = if state.is_task_running {
" to interrupt"
} else {
" to quit"
};
vec![
" ".into(),
key_hint::ctrl('C'),
" again".into(),
followup.into(),
]
}
#[derive(Clone, Copy, Debug)]
struct FooterSegment {
prefix: &'static str,
binding: ShortcutBinding,
label: &'static str,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
enum ShortcutId {
Send,
InsertNewline,
ShowTranscript,
Quit,
EditPrevious,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ShortcutBinding {
code: KeyCode,
modifiers: KeyModifiers,
display: ShortcutDisplay,
condition: DisplayCondition,
}
impl ShortcutBinding {
fn span(&self) -> Span<'static> {
self.display.into_span()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ShortcutDisplay {
Plain(&'static str),
Ctrl(char),
Shift(char),
}
impl ShortcutDisplay {
fn into_span(self) -> Span<'static> {
match self {
ShortcutDisplay::Plain(text) => key_hint::plain(text),
ShortcutDisplay::Ctrl(ch) => key_hint::ctrl(ch),
ShortcutDisplay::Shift(ch) => key_hint::shift(ch),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum DisplayCondition {
Always,
WhenShiftEnterHint,
WhenNotShiftEnterHint,
}
impl DisplayCondition {
fn matches(self, state: ShortcutsState) -> bool {
match self {
DisplayCondition::Always => true,
DisplayCondition::WhenShiftEnterHint => state.use_shift_enter_hint,
DisplayCondition::WhenNotShiftEnterHint => !state.use_shift_enter_hint,
}
}
}
struct ShortcutDescriptor {
id: ShortcutId,
bindings: &'static [ShortcutBinding],
footer_label: &'static str,
footer_prefix: &'static str,
}
impl ShortcutDescriptor {
fn binding_for(&self, state: ShortcutsState) -> Option<ShortcutBinding> {
self.bindings
.iter()
.find(|binding| binding.condition.matches(state))
.copied()
}
fn should_show(&self, state: ShortcutsState) -> bool {
match self.id {
ShortcutId::EditPrevious => state.esc_backtrack_hint,
_ => true,
}
}
fn footer_segment(&self, state: ShortcutsState) -> Option<FooterSegment> {
if !self.should_show(state) {
return None;
}
let binding = self.binding_for(state)?;
Some(FooterSegment {
prefix: self.footer_prefix,
binding,
label: self.footer_label,
})
}
}
const SHORTCUTS: &[ShortcutDescriptor] = &[
ShortcutDescriptor {
id: ShortcutId::Send,
bindings: &[ShortcutBinding {
code: KeyCode::Enter,
modifiers: KeyModifiers::NONE,
display: ShortcutDisplay::Plain(""),
condition: DisplayCondition::Always,
}],
footer_label: " send ",
footer_prefix: "",
},
ShortcutDescriptor {
id: ShortcutId::InsertNewline,
bindings: &[
ShortcutBinding {
code: KeyCode::Enter,
modifiers: KeyModifiers::SHIFT,
display: ShortcutDisplay::Shift('⏎'),
condition: DisplayCondition::WhenShiftEnterHint,
},
ShortcutBinding {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
display: ShortcutDisplay::Ctrl('J'),
condition: DisplayCondition::WhenNotShiftEnterHint,
},
],
footer_label: " newline ",
footer_prefix: "",
},
ShortcutDescriptor {
id: ShortcutId::ShowTranscript,
bindings: &[ShortcutBinding {
code: KeyCode::Char('t'),
modifiers: KeyModifiers::CONTROL,
display: ShortcutDisplay::Ctrl('T'),
condition: DisplayCondition::Always,
}],
footer_label: " transcript ",
footer_prefix: "",
},
ShortcutDescriptor {
id: ShortcutId::Quit,
bindings: &[ShortcutBinding {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
display: ShortcutDisplay::Ctrl('C'),
condition: DisplayCondition::Always,
}],
footer_label: " quit",
footer_prefix: "",
},
ShortcutDescriptor {
id: ShortcutId::EditPrevious,
bindings: &[ShortcutBinding {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE,
display: ShortcutDisplay::Plain("Esc"),
condition: DisplayCondition::Always,
}],
footer_label: " edit prev",
footer_prefix: " ",
},
];
#[cfg(test)]
mod tests {
use super::*;
use codex_core::protocol::TokenUsage;
use insta::assert_snapshot;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
fn snapshot_footer(name: &str, props: FooterProps<'_>) {
let mut terminal = Terminal::new(TestBackend::new(80, 3)).unwrap();
terminal
.draw(|f| {
let area = Rect::new(0, 0, f.area().width, 1);
render_footer(area, f.buffer_mut(), props);
})
.unwrap();
assert_snapshot!(name, terminal.backend());
}
fn token_usage(total_tokens: u64, last_tokens: u64, context_window: u64) -> TokenUsageInfo {
let usage = TokenUsage {
input_tokens: total_tokens,
cached_input_tokens: 0,
output_tokens: 0,
reasoning_output_tokens: 0,
total_tokens,
};
let last = TokenUsage {
input_tokens: last_tokens,
cached_input_tokens: 0,
output_tokens: 0,
reasoning_output_tokens: 0,
total_tokens: last_tokens,
};
TokenUsageInfo {
total_token_usage: usage,
last_token_usage: last,
model_context_window: Some(context_window),
}
}
#[test]
fn footer_snapshots() {
snapshot_footer(
"footer_shortcuts_default",
FooterProps {
ctrl_c_quit_hint: false,
is_task_running: false,
esc_backtrack_hint: false,
use_shift_enter_hint: false,
token_usage_info: None,
},
);
snapshot_footer(
"footer_shortcuts_shift_and_esc",
FooterProps {
ctrl_c_quit_hint: false,
is_task_running: false,
esc_backtrack_hint: true,
use_shift_enter_hint: true,
token_usage_info: Some(&token_usage(4_200, 900, 8_000)),
},
);
snapshot_footer(
"footer_ctrl_c_quit_idle",
FooterProps {
ctrl_c_quit_hint: true,
is_task_running: false,
esc_backtrack_hint: false,
use_shift_enter_hint: false,
token_usage_info: None,
},
);
snapshot_footer(
"footer_ctrl_c_quit_running",
FooterProps {
ctrl_c_quit_hint: true,
is_task_running: true,
esc_backtrack_hint: false,
use_shift_enter_hint: false,
token_usage_info: None,
},
);
}
}

View File

@@ -23,6 +23,7 @@ mod chat_composer_history;
mod command_popup;
pub mod custom_prompt_view;
mod file_search_popup;
mod footer;
mod list_selection_view;
pub(crate) use list_selection_view::SelectionViewParams;
mod paste_burst;

View File

@@ -0,0 +1,7 @@
---
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
" ⌃C again to quit "
" "
" "

View File

@@ -0,0 +1,7 @@
---
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
" ⌃C again to interrupt "
" "
" "

View File

@@ -0,0 +1,7 @@
---
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
"⏎ send ⌃J newline ⌃T transcript ⌃C quit "
" "
" "

View File

@@ -0,0 +1,7 @@
---
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
"⏎ send ⇧⏎ newline ⌃T transcript ⌃C quit Esc edit prev 4.20K tokens use"
" "
" "