TUI footer: right-align context and degrade shortcut summary + mode cleanly (#9944)

## Summary
Refines the bottom footer layout to keep `% context left` right-aligned
while making the left side degrade cleanly

## Behavior with empty textarea
Full width:
<img width="607" height="62" alt="Screenshot 2026-01-26 at 2 59 59 PM"
src="https://github.com/user-attachments/assets/854f33b7-d714-40be-8840-a52eb3bda442"
/>
Less:
<img width="412" height="66" alt="Screenshot 2026-01-26 at 2 59 48 PM"
src="https://github.com/user-attachments/assets/9c501788-c3a2-4b34-8f0b-8ec4395b44fe"
/>
Min width:
<img width="218" height="77" alt="Screenshot 2026-01-26 at 2 59 33 PM"
src="https://github.com/user-attachments/assets/0bed2385-bdbf-4254-8ae4-ab3452243628"
/>

## Behavior with message in textarea and agent running (steer enabled)
Full width:
<img width="753" height="63" alt="Screenshot 2026-01-26 at 4 33 54 PM"
src="https://github.com/user-attachments/assets/1856b352-914a-44cf-813d-1cb50c7f183b"
/>

Less:
<img width="353" height="61" alt="Screenshot 2026-01-26 at 4 30 12 PM"
src="https://github.com/user-attachments/assets/d951c4d5-f3e7-4116-8fe1-6a6c712b3d48"
/>

Less:
<img width="304" height="64" alt="Screenshot 2026-01-26 at 4 30 51 PM"
src="https://github.com/user-attachments/assets/1433e994-5cbc-4e20-a98a-79eee13c8699"
/>

Less:
<img width="235" height="61" alt="Screenshot 2026-01-26 at 4 30 56 PM"
src="https://github.com/user-attachments/assets/e216c3c6-84cd-40fc-ae4d-83bf28947f0e"
/>

Less:
<img width="165" height="59" alt="Screenshot 2026-01-26 at 4 31 08 PM"
src="https://github.com/user-attachments/assets/027de5de-7185-47ce-b1cc-5363ea33d9b1"
/>

## Notes / Edge Cases
- In steer mode while typing, the queue hint no longer replaces the mode
label; it renders as `tab to queue message · {Mode}`.
- Collapse priorities differ by state:
- With the queue hint active, `% context left` is hidden before
shortening or dropping the queue hint.
- In the empty + non-running state, `? for shortcuts` is dropped first,
and `% context left` is only shown if `(shift+tab to
cycle)` can also fit.
- Transient instructional states (`?` overlay, Esc hint, Ctrl+C/D
reminders, and flash/override hints) intentionally suppress the
mode label (and context) to focus the next action.

## Implementation Notes
- Renamed the base footer modes to make the state explicit:
`ComposerEmpty` and `ComposerHasDraft`, and compute the base mode
directly from emptiness.
- Unified collapse behavior in `single_line_footer_layout` for both base
modes, with:
- Queue-hint behavior that prefers keeping the queue hint over context.
- A cycle-hint guard that prevents context from reappearing after
`(shift+tab to cycle)` is dropped.
- Kept rendering responsibilities explicit:
  - `single_line_footer_layout` decides what fits.
  - `render_footer_line` renders a chosen line.
- `render_footer_from_props` renders the canonical mode-to-text mapping.
- Expanded snapshot coverage:
- Added `footer_collapse_snapshots` in `chat_composer.rs` to lock the
distinct collapse states across widths.
- Consolidated the width-aware snapshot helper usage (e.g.,
`snapshot_composer_state_with_width`,
`snapshot_footer_with_mode_indicator`).
This commit is contained in:
Charley Cunningham
2026-01-27 09:43:09 -08:00
committed by GitHub
parent 067922a734
commit 538e1059a3
40 changed files with 949 additions and 167 deletions

View File

@@ -96,15 +96,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_footer;
use super::footer::render_context_right;
use super::footer::render_footer_from_props;
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::single_line_footer_layout;
use super::footer::toggle_shortcut_mode;
use super::paste_burst::CharDecision;
use super::paste_burst::PasteBurst;
@@ -341,7 +346,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,
@@ -2234,7 +2239,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
@@ -2254,19 +2263,32 @@ impl ChatComposer {
}
}
/// Resolve the effective footer mode via a small priority waterfall.
///
/// 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 {
let base_mode = if self.is_empty() {
FooterMode::ComposerEmpty
} else {
FooterMode::ComposerHasDraft
};
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::ComposerEmpty | FooterMode::ComposerHasDraft
if self.quit_shortcut_hint_visible() =>
{
FooterMode::QuitShortcutReminder
}
FooterMode::ShortcutSummary if !self.is_empty() => FooterMode::ContextOnly,
other => other,
FooterMode::QuitShortcutReminder => base_mode,
FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft => base_mode,
}
}
@@ -2584,6 +2606,28 @@ 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 = match footer_props.mode {
FooterMode::ComposerEmpty => !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::ComposerEmpty
| FooterMode::ShortcutOverlay
| FooterMode::EscHint => false,
};
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));
@@ -2598,26 +2642,102 @@ impl Renderable for ChatComposer {
} else {
popup_rect
};
let mut left_content_width = None;
if self.footer_flash_visible() {
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,
self.collaboration_mode_indicator,
show_cycle_hint,
show_shortcuts_hint,
show_queue_hint,
)
};
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();
let single_line_layout = if has_override {
None
} else {
match footer_props.mode {
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
// the context indicator on narrow widths.
Some(single_line_footer_layout(
hint_rect,
context_width,
self.collaboration_mode_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,
FooterMode::EscHint
| FooterMode::QuitShortcutReminder
| FooterMode::ShortcutOverlay
) {
false
} else {
single_line_layout
.as_ref()
.map(|(_, show_context)| *show_context)
.unwrap_or(can_show_left_and_context)
};
if let Some((summary_left, _)) = single_line_layout {
match summary_left {
SummaryLeft::Default => {
render_footer_from_props(
hint_rect,
buf,
footer_props,
self.collaboration_mode_indicator,
show_cycle_hint,
show_shortcuts_hint,
show_queue_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_from_props(
hint_rect,
buf,
footer_props,
self.collaboration_mode_indicator,
show_cycle_hint,
show_shortcuts_hint,
show_queue_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();
@@ -2647,8 +2767,11 @@ 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 placeholder = Span::from(text).dim();
Line::from(vec![placeholder])
.render_ref(textarea_rect.inner(Margin::new(0, 0)), buf);
}
}
}
}
@@ -2861,14 +2984,17 @@ mod tests {
);
}
fn snapshot_composer_state<F>(name: &str, enhanced_keys_supported: bool, setup: F)
where
fn snapshot_composer_state_with_width<F>(
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::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
@@ -2890,6 +3016,13 @@ mod tests {
insta::assert_snapshot!(name, terminal.backend());
}
fn snapshot_composer_state<F>(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;
@@ -2942,6 +3075,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;
@@ -2962,15 +3189,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::<AppEvent>();
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::<AppEvent>();
@@ -3031,11 +3283,11 @@ 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");
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));
@@ -3043,8 +3295,8 @@ 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::ContextOnly);
assert_eq!(composer.footer_mode, FooterMode::ComposerEmpty);
assert_eq!(composer.footer_mode(), FooterMode::ComposerHasDraft);
}
/// Behavior: while a paste-like burst is being captured, `?` must not toggle the shortcut

View File

@@ -8,6 +8,30 @@
//! 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. `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`.
//!
//! 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;
@@ -27,9 +51,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,
@@ -77,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(),
}
@@ -92,21 +117,36 @@ impl CollaborationModeIndicator {
pub(crate) enum FooterMode {
/// Transient "press again to quit" reminder (Ctrl+C/Ctrl+D).
QuitShortcutReminder,
ShortcutSummary,
/// Multi-line shortcut overlay shown after pressing `?`.
ShortcutOverlay,
/// Transient "press Esc again" hint shown after the first Esc while idle.
EscHint,
ContextOnly,
/// Base single-line footer when the composer is empty.
ComposerEmpty,
/// 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,
}
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::ShortcutSummary
}
FooterMode::ShortcutOverlay | FooterMode::QuitShortcutReminder => base_mode,
_ => FooterMode::ShortcutOverlay,
}
}
@@ -124,57 +164,347 @@ pub(crate) fn reset_mode_after_activity(current: FooterMode) -> FooterMode {
FooterMode::EscHint
| FooterMode::ShortcutOverlay
| FooterMode::QuitShortcutReminder
| FooterMode::ContextOnly => FooterMode::ShortcutSummary,
| FooterMode::ComposerHasDraft => FooterMode::ComposerEmpty,
other => other,
}
}
pub(crate) fn footer_height(props: FooterProps) -> u16 {
footer_lines(props).len() as u16
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,
};
footer_from_props_lines(props, None, false, show_shortcuts_hint, show_queue_hint).len() as u16
}
pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps) {
/// Render a single precomputed footer line.
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(
/// Render footer content directly from `FooterProps`.
///
/// 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,
indicator: Option<CollaborationModeIndicator>,
props: FooterProps,
collaboration_mode_indicator: Option<CollaborationModeIndicator>,
show_cycle_hint: bool,
left_content_width: Option<u16>,
show_shortcuts_hint: bool,
show_queue_hint: bool,
) {
let Some(indicator) = indicator else {
return;
Paragraph::new(prefix_lines(
footer_from_props_lines(
props,
collaboration_mode_indicator,
show_cycle_hint,
show_shortcuts_hint,
show_queue_hint,
),
" ".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
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SummaryHintKind {
None,
Shortcuts,
QueueMessage,
QueueShort,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct LeftSideState {
hint: SummaryHintKind,
show_cycle_hint: bool,
}
fn left_side_line(
collaboration_mode_indicator: Option<CollaborationModeIndicator>,
state: LeftSideState,
) -> Line<'static> {
let mut line = Line::from("");
match state.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(collaboration_mode_indicator) = collaboration_mode_indicator {
if !matches!(state.hint, SummaryHintKind::None) {
line.push_span(" · ".dim());
}
line.push_span(collaboration_mode_indicator.styled_span(state.show_cycle_hint));
}
line
}
pub(crate) enum SummaryLeft {
Default,
Custom(Line<'static>),
None,
}
/// 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,
collaboration_mode_indicator: Option<CollaborationModeIndicator>,
show_cycle_hint: bool,
show_shortcuts_hint: bool,
show_queue_hint: bool,
) -> (SummaryLeft, bool) {
let hint_kind = if show_queue_hint {
SummaryHintKind::QueueMessage
} else if show_shortcuts_hint {
SummaryHintKind::Shortcuts
} else {
SummaryHintKind::None
};
let default_state = LeftSideState {
hint: hint_kind,
show_cycle_hint,
};
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 state_line = |state: LeftSideState| -> Line<'static> {
if state == default_state {
default_line.clone()
} else {
left_side_line(collaboration_mode_indicator, state)
}
};
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 {
// In queue mode, prefer dropping context before dropping the queue hint.
let queue_states = [
default_state,
LeftSideState {
hint: SummaryHintKind::QueueMessage,
show_cycle_hint: false,
},
LeftSideState {
hint: SummaryHintKind::QueueShort,
show_cycle_hint: false,
},
];
// 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<LeftSideState> = None;
for state in queue_states {
if previous_state == Some(state) {
continue;
}
previous_state = Some(state);
let width = state_width(state);
if width > 0 && can_show_left_with_context(area, width, context_width) {
if state == default_state {
return (SummaryLeft::Default, true);
}
return (SummaryLeft::Custom(state_line(state)), true);
}
}
// 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<LeftSideState> = None;
for state in queue_states {
if previous_state == Some(state) {
continue;
}
previous_state = Some(state);
let width = state_width(state);
if width > 0 && left_fits(area, width) {
if state == default_state {
return (SummaryLeft::Default, false);
}
return (SummaryLeft::Custom(state_line(state)), false);
}
}
} 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,
};
let cycle_width = state_width(cycle_state);
if cycle_width > 0 && can_show_left_with_context(area, cycle_width, context_width) {
return (SummaryLeft::Custom(state_line(cycle_state)), true);
}
if cycle_width > 0 && left_fits(area, cycle_width) {
return (SummaryLeft::Custom(state_line(cycle_state)), false);
}
}
// 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);
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, // show_context
);
}
if mode_only_width > 0 && left_fits(area, mode_only_width) {
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
&& can_show_left_with_context(area, mode_only_width, context_width)
{
return (
SummaryLeft::Custom(left_side_line(
Some(collaboration_mode_indicator),
mode_only_state,
)),
true, // show_context
);
}
if left_fits(area, mode_only_width) {
return (
SummaryLeft::Custom(left_side_line(
Some(collaboration_mode_indicator),
mode_only_state,
)),
false, // show_context
);
}
}
(SummaryLeft::None, true)
}
fn right_aligned_x(area: Rect, content_width: u16) -> Option<u16> {
if area.is_empty() {
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;
}
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 span = indicator.styled_span(show_cycle_hint);
let label_width = span.width() as u16;
if label_width == 0 || label_width > area.width {
let context_width = line.width() as u16;
let Some(mut x) = right_aligned_x(area, context_width) else {
return;
}
let x = area
.x
.saturating_add(area.width)
.saturating_sub(label_width)
.saturating_sub(FOOTER_INDENT_COLS as u16);
};
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,26 +523,35 @@ 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<Line<'static>> {
// 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.
/// Map `FooterProps` to footer lines without width-based collapse.
///
/// 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<CollaborationModeIndicator>,
show_cycle_hint: bool,
show_shortcuts_hint: bool,
show_queue_hint: bool,
) -> Vec<Line<'static>> {
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![
key_hint::plain(KeyCode::Char('?')).into(),
" for shortcuts".dim(),
]);
vec![line]
FooterMode::ComposerEmpty => {
let state = LeftSideState {
hint: if show_shortcuts_hint {
SummaryHintKind::Shortcuts
} else {
SummaryHintKind::None
},
show_cycle_hint,
};
vec![left_side_line(collaboration_mode_indicator, state)]
}
FooterMode::ShortcutOverlay => {
#[cfg(target_os = "linux")]
@@ -229,26 +568,37 @@ fn footer_lines(props: FooterProps) -> Vec<Line<'static>> {
shortcut_overlay_lines(state)
}
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,
);
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());
}
vec![line]
FooterMode::ComposerHasDraft => {
let state = LeftSideState {
hint: if show_queue_hint {
SummaryHintKind::QueueMessage
} else {
SummaryHintKind::None
},
show_cycle_hint,
};
vec![left_side_line(collaboration_mode_indicator, state)]
}
}
}
pub(crate) fn footer_line_width(props: FooterProps) -> u16 {
footer_lines(props)
.last()
.map(|line| line.width() as u16)
.unwrap_or(0)
pub(crate) fn footer_line_width(
props: FooterProps,
collaboration_mode_indicator: Option<CollaborationModeIndicator>,
show_cycle_hint: bool,
show_shortcuts_hint: bool,
show_queue_hint: bool,
) -> u16 {
footer_from_props_lines(
props,
collaboration_mode_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 {
@@ -396,7 +746,7 @@ fn build_columns(entries: Vec<Line<'static>>) -> Vec<Line<'static>> {
.collect()
}
fn context_window_line(percent: Option<i64>, used_tokens: Option<i64>) -> Line<'static> {
pub(crate) fn context_window_line(percent: Option<i64>, used_tokens: Option<i64>) -> Line<'static> {
if let Some(percent) = percent {
let percent = percent.clamp(0, 100);
return Line::from(vec![Span::from(format!("{percent}% context left")).dim()]);
@@ -615,36 +965,102 @@ 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);
render_footer(area, f.buffer_mut(), props);
})
.unwrap();
assert_snapshot!(name, terminal.backend());
snapshot_footer_with_mode_indicator(name, 80, props, None);
}
fn snapshot_footer_with_indicator(
fn snapshot_footer_with_mode_indicator(
name: &str,
width: u16,
props: FooterProps,
indicator: Option<CollaborationModeIndicator>,
collaboration_mode_indicator: Option<CollaborationModeIndicator>,
) {
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);
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 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,
collaboration_mode_indicator,
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,
collaboration_mode_indicator,
show_cycle_hint,
show_shortcuts_hint,
show_queue_hint,
);
match summary_left {
SummaryLeft::Default => {
render_footer_from_props(
area,
f.buffer_mut(),
props,
collaboration_mode_indicator,
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,
collaboration_mode_indicator,
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());
@@ -655,7 +1071,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,
@@ -760,7 +1176,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,
@@ -775,7 +1191,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,
@@ -788,9 +1204,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,
@@ -803,9 +1219,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,
@@ -818,7 +1234,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,
@@ -829,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,
@@ -844,7 +1260,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,
@@ -855,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,

View File

@@ -11,4 +11,4 @@ expression: terminal.backend()
" "
" "
" "
" 100% context left "
" 100% context left "

View File

@@ -11,4 +11,4 @@ expression: terminal.backend()
" "
" "
" "
" 100% context left · ? for shortcuts "
" ? for shortcuts 100% context left "

View File

@@ -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 "

View File

@@ -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 "

View File

@@ -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) "

View File

@@ -0,0 +1,13 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
" "
" Ask Codex to do anythin "
" "
" "
" "
" "
" "
" "
" Code mode "

View File

@@ -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 "

View File

@@ -0,0 +1,13 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
" "
" Test "
" "
" "
" "
" "
" "
" "
" tab to queue message · Code mode "

View File

@@ -0,0 +1,13 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
" "
" Test "
" "
" "
" "
" "
" "
" "
" Code mode "

View File

@@ -0,0 +1,13 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
" "
" Test "
" "
" "
" "
" "
" "
" "
" tab to queue · Code mode 98% context left "

View File

@@ -0,0 +1,13 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
" "
" Test "
" "
" "
" "
" "
" "
" "
" tab to queue · Code mode "

View File

@@ -10,4 +10,4 @@ expression: terminal.backend()
" "
" "
" "
" 100% context left "
" 100% context left "

View File

@@ -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 "

View File

@@ -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 "

View File

@@ -11,4 +11,4 @@ expression: terminal.backend()
" "
" "
" "
" 100% context left "
" 100% context left "

View File

@@ -11,4 +11,4 @@ expression: terminal.backend()
" "
" "
" "
" 100% context left "
" 100% context left "

View File

@@ -11,4 +11,4 @@ expression: terminal.backend()
" "
" "
" "
" 100% context left "
" 100% context left "

View File

@@ -0,0 +1,5 @@
---
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
" 100% context left "

View File

@@ -0,0 +1,5 @@
---
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
" tab to queue message 100% context left "

View File

@@ -1,5 +0,0 @@
---
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
" 100% context left "

View File

@@ -1,5 +0,0 @@
---
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
" 100% context left · tab to queue message "

View File

@@ -2,4 +2,4 @@
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
" 123K used · ? for shortcuts "
" ? for shortcuts 123K used "

View File

@@ -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) "

View File

@@ -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 "

View File

@@ -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 "

View File

@@ -2,4 +2,4 @@
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
" 72% context left · ? for shortcuts "
" ? for shortcuts 72% context left "

View File

@@ -2,4 +2,4 @@
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
" 100% context left · ? for shortcuts "
" ? for shortcuts 100% context left "

View File

@@ -7,4 +7,4 @@ expression: "render_snapshot(&pane, area)"
Ask Codex to do anything
100% context left · ? for shortcuts
? for shortcuts 100% context left

View File

@@ -7,4 +7,4 @@ expression: "render_snapshot(&pane, area)"
Ask Codex to do anything
100% context left · ? for sh
100% context left

View File

@@ -9,4 +9,4 @@ expression: "render_snapshot(&pane, area)"
Ask Codex to do anything
100% context left · ? for shortcuts
? for shortcuts 100% context left

View File

@@ -7,4 +7,4 @@ expression: "render_snapshot(&pane, area)"
Ask Codex to do anything
100% context left · ? for shortcuts
? for shortcuts 100% context left

View File

@@ -11,4 +11,4 @@ expression: "render_snapshot(&pane, area)"
Ask Codex to do anything
100% context left · ? for shortcuts
? for shortcuts 100% context left

View File

@@ -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

View File

@@ -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

View File

@@ -8,4 +8,4 @@ expression: terminal.backend()
" "
" Ask Codex to do anything "
" "
" 100% context left · ? for shortcuts "
" ? for shortcuts 100% context left "

View File

@@ -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

View File

@@ -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 "