Keep TUI status content separate from composer hints (#60680)

## Why

Composer hints could replace configured status content, and mode indicators and warnings reduced the width available to status extensions.

## What changed

- Give enabled status content a dedicated area below the composer in both inline and fullscreen layouts, preserving it during transient hint states.
- Keep shortcuts, queue hints, mode indicators, and warnings in the hint row.
- Measure extension status content at the full available width, allowing up to four rows while yielding space to the composer when height is limited.
- Suppress composer hints behind centered dialogs.

## Testing

Update layout assertions and snapshots for status persistence, extension resizing and clipping, warning placement, and reasoning-effort transitions.

GitOrigin-RevId: f11c2e4fb53c35b69c2fbdbe80cef31bb28faca4
This commit is contained in:
Joe Savona
2026-11-10 23:33:26 +00:00
committed by copyberry
parent ad3d3521ed
commit 8a7a723dcd
130 changed files with 405 additions and 382 deletions
@@ -7,4 +7,5 @@ terminal title: project
footer:
› Ask Codex to do anything
gpt-5.2 default · /tmp/project · ← for agents
gpt-5.2 default · /tmp/project
← for agents · ? for shortcuts
@@ -5,4 +5,5 @@ expression: normalize_snapshot_paths(popup)
---
› Ask Codex to do anything
gpt-5.2 default · /tmp/project · Keep this title · ← for agents
gpt-5.2 default · /tmp/project · Keep this title
← for agents · ? for shortcuts
@@ -662,7 +662,10 @@ async fn reconnect_exhaustion_and_unknown_initial_thread_stay_offline() -> Resul
"reconnect_failed",
format!(
"{error}\n{}",
render_bottom_popup(&app.chat_widget, /*width*/ 80)
super::disconnect::normalize_reconnect_status(&render_bottom_popup(
&app.chat_widget,
/*width*/ 80
))
)
);
tokio::time::resume();
@@ -30,7 +30,7 @@ async fn agents_navigation_opens_the_command_center_on_every_target() -> Result<
let composer = render_bottom_popup(&app.chat_widget, /*width*/ 96);
let footer = composer.lines().last().expect("composer footer").trim();
insta::allow_duplicates! {
insta::assert_snapshot!(normalize_snapshot_paths(footer), @"GPT-5.6-Sol default · /tmp/project · ← for agents");
insta::assert_snapshot!(normalize_snapshot_paths(footer), @"← for agents · ? for shortcuts");
}
app.handle_tui_event(
&mut tui,
@@ -17,10 +17,10 @@ use serde_json::json;
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::Message;
fn normalize_reconnect_status(popup: &str) -> String {
pub(super) fn normalize_reconnect_status(popup: &str) -> String {
let status =
regex_lite::Regex::new(r"(?m)^[•◦] (Reconnecting to server… )\([\dhms ]+\)").unwrap();
status.replace(popup, "• ${1}(<elapsed>)").into_owned()
regex_lite::Regex::new(r"(?m)^[•◦] (Reconnecting to server…|Reconnect failed — check the endpoint, then relaunch) \([\dhms ]+\)").unwrap();
normalize_snapshot_paths(status.replace(popup, "• ${1} (<elapsed>)").into_owned())
}
#[tokio::test]
@@ -500,10 +500,11 @@ async fn replacement_uses_server_defaults_and_preserves_explicit_launch_settings
if explicit == "saved" {
let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 140)
.replace(&server_config.cwd.display().to_string(), "<PROJECT>");
insta::assert_snapshot!(rendered, @r"
insta::assert_snapshot!(rendered, @"
› Ask Codex to do anything
server-model high · <PROJECT> · ← for agents
server-model high · <PROJECT>
← for agents · ? for shortcuts
");
}
if explicit.starts_with("profile_summary") {
@@ -3,9 +3,10 @@ source: tui/src/app/tests/active_reconnect_tests.rs
expression: "format!(\"{error}\\n{}\", render_bottom_popup(&app.chat_widget, 80))"
---
Automatic reconnect timed out while connecting to the server.
• Reconnect failed — check the endpoint, then relaunch (0s)
• Reconnect failed — check the endpoint, then relaunch (<elapsed>)
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
⌃c quit
@@ -4,4 +4,5 @@ expression: "render_bottom_popup(&app.chat_widget, 80)"
---
› kept draft
gpt-test default Plan mode (⇧tab to cycle)
gpt-test default
Plan mode (⇧tab to cycle)
@@ -12,3 +12,4 @@ expression: normalize_snapshot_paths(rendered)
› Ask Codex to do anything
gpt-5.6-terra medium · /tmp/project
? for shortcuts
@@ -5,3 +5,4 @@ expression: normalize_snapshot_paths(rendered)
› Ask Codex to do anything
gpt-5.5 default · /tmp/project
? for shortcuts
@@ -11,16 +11,16 @@ startup:
attached:
› draft during startup
GPT-6.1-Sol default · /tmp/project
owned=true
startup:
› draft during startup
› draft during startup
attached:
@@ -16,3 +16,4 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&app.chat_widget, 80))
› Ask Codex to do anything
gpt-test default · /tmp/project
? for shortcuts
@@ -14,4 +14,5 @@ expression: "format!(\"{}\\n{}\",\nlines_to_single_string(&app.transcript_cells.
› keep this draft!
gpt-test default · /tmp/project
⌃c quit
@@ -7,4 +7,5 @@ expression: "reconnect_status.replace(&render_bottom_popup(&app.chat_widget, 80)
› startup draft
GPT-5.6-Sol default · /tmp/project
⌃c quit
@@ -6,6 +6,7 @@ Other task:
› Ask Codex to do anything
gpt-test default · /tmp/project
? for shortcuts
Restored owner:
voice ● listening f9 mute /voice stop
mic ▁▁▁▁▁▁ codex ▁▁▁▁▁▁
@@ -13,6 +14,7 @@ Restored owner:
› Ask Codex to do anything
gpt-test default · /tmp/project
? for shortcuts
>_ OpenAI Codex (v0.0.0)
/tmp/project
@@ -13,6 +13,7 @@ expression: "rendered_receipts.join(\"\\n\\n\")"
› Ask Codex to do anything
gpt-test default · /tmp/project
? for shortcuts
── Steered ──
■ Conversation interrupted - use /feedback if something went wrong
@@ -31,6 +32,7 @@ expression: "rendered_receipts.join(\"\\n\\n\")"
› Ask Codex to do anything
gpt-test default · /tmp/project
? for shortcuts
── Rejected ──
■ reply stream closed before submission
@@ -39,7 +41,8 @@ expression: "rendered_receipts.join(\"\\n\\n\")"
› follow up
tab to queue message 100% context left
gpt-test default · /tmp/project
tab to queue message
── Duplicate ──
■ Conversation interrupted - use /feedback if something went wrong
@@ -50,3 +53,4 @@ expression: "rendered_receipts.join(\"\\n\\n\")"
› Ask Codex to do anything
gpt-test default · /tmp/project
? for shortcuts
@@ -729,10 +729,11 @@ async fn fresh_startup_reads_destination_and_cleared_model_uses_catalog() -> Res
if !remote {
let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 80)
.replace(&destination.path().display().to_string(), "<PROJECT>");
insta::assert_snapshot!(rendered, @r"
insta::assert_snapshot!(rendered, @"
› Ask Codex to do anything
GPT-6.1-Sol high · <PROJECT>
? for shortcuts
");
}
let expected_cwd = if override_cwd {
+22 -25
View File
@@ -360,7 +360,6 @@ 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::max_left_width_for_right;
use super::footer::passive_footer_status_line;
use super::footer::render_context_right;
use super::footer::render_footer_from_props;
@@ -371,7 +370,6 @@ use super::footer::side_conversation_context_line;
use super::footer::single_line_footer_layout;
use super::footer::status_line_right_indicator_line;
use super::footer::toggle_shortcut_mode;
use super::footer::uses_passive_footer_status_layout;
use super::mention_usage::MentionUsage;
use super::mentions_v2::MentionV2Popup;
use super::mentions_v2::MentionV2Selection;
@@ -397,7 +395,6 @@ use crate::render::RectExt;
use crate::render::renderable::Renderable;
use crate::slash_command::SlashCommand;
use crate::style::user_message_style;
use crate::ui_consts::FOOTER_INDENT_COLS;
use codex_protocol::ThreadId;
use codex_protocol::user_input::ByteRange;
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
@@ -911,12 +908,13 @@ impl ChatComposer {
let style = IgnitionStyle::random(self.effort_animation_style);
self.effort_ignition = Some(EffortIgnition::new(tier, style));
self.effort_animation_style = Some(style);
if self.footer.status_line_enabled
&& let Some(FooterContent::HyperlinkLine(previous)) =
passive_footer_status_line(&self.footer_props())
if let Some(previous) = self.status_surface_content()
&& !matches!(previous.as_ref(), FooterContent::Extension { .. })
{
self.effort_status_line_transition =
Some(EffortStatusLineTransition::new(tier, previous));
self.effort_status_line_transition = Some(EffortStatusLineTransition::new(
tier,
previous.into_owned().into_hyperlink_line(),
));
}
if let Some(frame_requester) = &self.frame_requester {
frame_requester.schedule_frame();
@@ -4727,7 +4725,7 @@ impl ChatComposer {
self.render_transcript_footer(transcript_hint_area, buf, footer, warning_area);
}
None if self.shortcuts_above_composer(options) => {
let props = self.hint_footer_props(options);
let props = self.hint_footer_props();
super::shortcut_overlay::render(&props, inset_footer_hint_area(popup_rect), buf);
super::shortcut_overlay::close_hint(&props, transcript_hint_area.width)
.render(transcript_hint_area, buf);
@@ -4738,24 +4736,23 @@ impl ChatComposer {
input.render(inset_footer_hint_area(hint_rect), buf);
} else if let Some(line) = self.history_search_footer_line() {
render_footer_line(hint_rect, buf, line);
} else if self.shows_shared_status(options) {
self.render_status_surface(hint_rect, buf, options);
} else {
let footer_props = self.hint_footer_props(options);
let show_cycle_hint = !self.has_separate_status(options)
let footer_props = self.hint_footer_props();
let show_cycle_hint = !self.popup_active()
&& !self.blocks_direct_input
&& self.draft.input_enabled
&& !footer_props.is_task_running
&& self.footer.collaboration_mode_indicator.is_some();
let show_shortcuts_hint =
footer_props.mode == FooterMode::ComposerEmpty && !self.is_in_paste_burst();
let show_queue_hint = footer_props.mode == FooterMode::ComposerHasDraft
&& footer_props.is_task_running;
let status_line_active = uses_passive_footer_status_layout(&footer_props);
let left_mode_indicator =
if status_line_active || self.has_separate_status(options) {
None
} else {
self.footer.collaboration_mode_indicator
};
let status_line_active = self.footer.status_line_enabled;
let left_mode_indicator = if status_line_active {
None
} else {
self.footer.collaboration_mode_indicator
};
let active_footer_hint_override = self.footer.hint_override.as_ref();
let left_width = if self.footer.flash_visible() {
self.footer
@@ -4779,8 +4776,6 @@ impl ChatComposer {
Some(side_conversation_context_line(label))
} else if let Some(line) = self.shell_mode_footer_line() {
Some(line)
} else if self.has_separate_status(options) {
None
} else if status_line_active {
let full = self.mode_indicator_line(show_cycle_hint);
let compact = self.mode_indicator_line(/*show_cycle_hint*/ false);
@@ -4798,7 +4793,7 @@ impl ChatComposer {
can_show_left_with_context(hint_rect, left_width, right_width);
let has_override =
self.footer.flash_visible() || active_footer_hint_override.is_some();
let single_line_layout = if has_override || status_line_active {
let single_line_layout = if has_override {
None
} else {
match footer_props.mode {
@@ -4823,7 +4818,9 @@ impl ChatComposer {
| FooterMode::ShortcutOverlay => None,
}
};
let show_right = if matches!(
let show_right = if has_override && status_line_active {
can_show_left_and_context
} else if matches!(
footer_props.mode,
FooterMode::EscHint
| FooterMode::HistorySearch
@@ -4867,7 +4864,7 @@ impl ChatComposer {
hint_rect,
buf,
&footer_props,
self.footer.collaboration_mode_indicator,
left_mode_indicator,
show_cycle_hint,
show_shortcuts_hint,
show_queue_hint,
@@ -41,26 +41,16 @@ impl ChatComposer {
area: Rect,
options: ComposerRenderOptions<'_>,
) -> ComposerLayout {
let mut footer_hint_height = self.footer_hint_height(area.width, options);
let mut status_height = self.status_surface_height(area.width, options);
let shared = footer_hint_height > 1 && self.shows_shared_status(options);
if shared || status_height > 1 {
// Extra status rows yield to the draft, voice strip and other composer surfaces.
let desired =
self.height_with_rows(area.width, options, footer_hint_height, status_height);
let rows = if shared {
&mut footer_hint_height
} else {
&mut status_height
};
*rows = rows
.saturating_sub(desired.saturating_sub(area.height))
.max(1);
}
let status_height = status_height.min(
area.height
.saturating_sub(footer_hint_height.saturating_add(/*rhs*/ 3)),
);
let footer_hint_height = self.footer_hint_height(area.width, options);
let wanted = self.status_surface_height(area.width, /*available*/ 4, options);
let desired = self.height_with_rows(area.width, options, footer_hint_height, wanted);
let status_height = wanted
.saturating_sub(desired.saturating_sub(area.height))
.max(wanted.min(1))
.min(
area.height
.saturating_sub(footer_hint_height.saturating_add(3)),
);
let status = Rect {
y: area
.bottom()
@@ -90,7 +80,7 @@ impl ChatComposer {
let shortcuts_above = self.shortcuts_above_composer(options);
let (composer_rect, popup_rect, mut footer_rect) = if shortcuts_above {
let [shortcuts, composer, footer] = Layout::vertical([
Constraint::Max(footer_height(&self.hint_footer_props(options), area.width)),
Constraint::Max(footer_height(&self.hint_footer_props(), area.width)),
Constraint::Min(3 + voice_rows),
Constraint::Length(footer_hint_height),
])
@@ -213,7 +203,7 @@ impl ChatComposer {
width,
options,
self.footer_hint_height(width, options),
self.status_surface_height(width, options),
self.status_surface_height(width, /*available*/ 4, options),
)
}
@@ -241,7 +231,7 @@ impl ChatComposer {
remote_images_separator,
status_height,
if self.shortcuts_above_composer(options) {
footer_height(&self.hint_footer_props(options), width)
footer_height(&self.hint_footer_props(), width)
} else {
0
},
@@ -80,8 +80,10 @@ pub(crate) struct ComposerRenderOptions<'a> {
pub(crate) working_tip: Option<&'a crate::turn_tip::TurnTip>,
pub(crate) warning_count: usize,
pub(crate) textarea_right_reserve: u16,
/// Keep configured status below the composer while hints occupy the final row.
/// The host is using the separate fullscreen transcript layout.
pub(crate) separate_status_line: bool,
/// A centered dialog owns input while the composer is painted behind it.
pub(crate) is_dialog_backdrop: bool,
/// Topmost frame row available for suggestions overlaying the transcript.
pub(crate) overlay_top: u16,
/// The workspace paints the open Presets menu over the completed frame.
@@ -284,8 +286,8 @@ impl super::ChatComposer {
}
pub(super) fn footer_hint_height(&self, width: u16, options: ComposerRenderOptions<'_>) -> u16 {
if self.shows_shared_status(options) {
return self.status_line_height(width, options);
if options.is_dialog_backdrop {
return 0;
}
if self.show_warning_notice(options) || self.shortcuts_above_composer(options) {
return 1;
@@ -295,7 +297,7 @@ impl super::ChatComposer {
.map_or_else(
|| {
self.custom_footer_height()
.unwrap_or_else(|| footer_height(&self.hint_footer_props(options), width))
.unwrap_or_else(|| footer_height(&self.hint_footer_props(), width))
},
|footer| footer.text.height().try_into().unwrap_or(u16::MAX),
)
@@ -310,7 +312,11 @@ impl super::ChatComposer {
warning_area: Option<Rect>,
) {
let hint_area = warning_notice::before_warning(hint_area, warning_area);
if hint_area.is_empty() {
return;
}
let mut text = footer.text.clone();
text.lines.truncate(usize::from(hint_area.height));
if footer.cursor_column.is_none()
&& self.footer.flash_visible()
&& let Some(flash) = &self.footer.flash
@@ -335,6 +341,17 @@ impl super::ChatComposer {
line.extend(shortcuts.spans);
}
}
if self.footer.status_line_enabled
&& let Some(mode) = self.mode_indicator_line(/*show_cycle_hint*/ false)
&& let Some(row) = text
.lines
.get(usize::from(hint_area.height.saturating_sub(1)))
&& crate::line_truncation::line_width(row) + 1 + mode.width()
<= usize::from(hint_area.width)
&& let Some(bottom_row) = hint_area.rows().next_back()
{
mode.right_aligned().render(bottom_row, buf);
}
Paragraph::new(text).render(hint_area, buf);
}
@@ -365,8 +382,8 @@ pub(super) struct FooterState {
pub(super) goal_status_indicator: Option<GoalStatusIndicator>,
pub(super) ide_context_active: bool,
pub(super) status_line_value: Option<FooterContent>,
// Available width and occupied height, shared by layout and cursor placement.
pub(super) measured_status_line: std::cell::Cell<Option<(u16, u16)>>,
// Measurement bounds and occupied height, shared by layout and cursor placement.
pub(super) measured_status_line: std::cell::Cell<Option<(ratatui::layout::Size, u16)>>,
pub(super) status_line_enabled: bool,
pub(super) side_conversation_context_label: Option<String>,
pub(super) active_agent_label: Option<String>,
@@ -176,10 +176,10 @@ fn mention_menu_reserves_height_above_composer_and_owns_cursor_over_interactive_
composer: surface,
textarea,
popup,
footer,
status,
..
} = composer.layout_with_options(area, options);
assert_eq!((popup.bottom(), surface.bottom()), (surface.y, footer.y));
assert_eq!((popup.bottom(), surface.bottom()), (surface.y, status.y));
assert_eq!(
composer.cursor_pos_with_options(area, options),
Some((3, textarea.y))
@@ -1,6 +1,5 @@
---
source: tui/src/bottom_pane/chat_composer/footer_state_tests.rs
assertion_line: 231
expression: "frames.join(\"\\n\\n\")"
---
60x8, cursor Some((10, 6))
@@ -9,7 +8,7 @@ expression: "frames.join(\"\\n\\n\")"
MODEL STATUS
Find: needle
Enter next · Esc close
@@ -19,7 +18,7 @@ expression: "frames.join(\"\\n\\n\")"
MODEL STATUS
Find: needle
Enter next · Esc close
@@ -5,5 +5,6 @@ expression: text(&buffer)
› Ask Codex
MODEL STATUS
Find: needle
Copied selection to clipboard
@@ -12,7 +12,6 @@ Transcript row 06: behind the menu
Transcript row 07: behind the menu
Transcript row 08: behind the menu
Transcript row 09: behind the menu
Transcript row 10: behind the menu
Mentions
Files, directories, plugins, skills and tasks
@@ -33,4 +32,5 @@ Transcript row 10: behind the menu
› @
MODEL STATUS
TRANSCRIPT STATUS
@@ -1,10 +0,0 @@
---
source: tui/src/bottom_pane/chat_composer/status_surface_tests.rs
expression: shared
---
16 | Ready PR 12345
15 | Ready
15 | PR 12345
16 | Ready PR 12345
25 | Ready
25 | PR 12345 Plan mode
@@ -13,5 +13,5 @@ expression: wide
› Ask Codex
MODEL · ~/project · Context 20% used Plan mode (⇧tab to cycle)
? for shortcuts
MODEL · ~/project · Context 20% used
? for shortcuts Plan mode (⇧tab to cycle)
@@ -13,8 +13,8 @@ expression: "states.join(\"\\n---\\n\")"
› Ask Codex
MODEL · ~/project · Context 20% used Plan mode
? for shortcuts
MODEL · ~/project · Context 20% used
? for shortcuts Plan mode (⇧tab to cycle)
---
@@ -27,8 +27,8 @@ expression: "states.join(\"\\n---\\n\")"
› Ask Codex
MODEL · ~/project · Context 20% used Plan mode
Copied selection
MODEL · ~/project · Context 20% used
Copied selection Plan mode (⇧tab to cycle)
---
@@ -41,8 +41,8 @@ expression: "states.join(\"\\n---\\n\")"
› Ask Codex
MODEL · ~/project · Context 20% used Plan mode
Ctrl+X waiting for chord
MODEL · ~/project · Context 20% used
Ctrl+X waiting for chord Plan mode (⇧tab to cycle)
---
@@ -55,5 +55,5 @@ expression: "states.join(\"\\n---\\n\")"
› Continue the task
MODEL · ~/project · Context 20% used Plan mode
tab to queue message
MODEL · ~/project · Context 20% used
tab to queue message Plan mode
@@ -1,10 +1,9 @@
---
source: tui/src/bottom_pane/chat_composer/status_surface_tests.rs
assertion_line: 344
expression: "format!(\"{buf:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 80, height: 8 },
area: Rect { x: 0, y: 0, width: 80, height: 9 },
content: [
" /model choose what model and reasoning effort to use ",
"› /memories configure memory use and generation ",
@@ -14,6 +13,7 @@ Buffer {
"› /m ",
" ",
" model · high · fast ",
" ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
@@ -3,9 +3,8 @@
//! Shortcut help grows above the input, preserving status and close rows below it even when clipped.
use super::*;
use ratatui::layout::Size;
const MAX_FOOTER_ROWS: u16 = 4;
use std::borrow::Cow;
impl ChatComposer {
pub(crate) fn set_status_line(&mut self, status_line: Option<FooterContent>) -> bool {
@@ -36,61 +35,45 @@ impl ChatComposer {
true
}
pub(super) fn has_separate_status(&self, options: ComposerRenderOptions<'_>) -> bool {
options.separate_status_line && self.footer.status_line_enabled
}
pub(super) fn shows_shared_status(&self, options: ComposerRenderOptions<'_>) -> bool {
!options.separate_status_line
&& options.footer.is_none()
&& self.custom_footer_height().is_none()
&& self.footer.status_line_enabled
&& self.footer_mode().shows_passive_line(self.is_task_running)
}
pub(super) fn status_surface_height(
&self,
width: u16,
available: u16,
options: ComposerRenderOptions<'_>,
) -> u16 {
if self.has_separate_status(options) {
self.status_line_height(width, options)
// The unfocused composer paints only the raw status, as one dim line.
let (available, status) = if options.unfocused {
let raw = self
.status_line_content()
.filter(|_| self.footer.status_line_enabled);
(available.min(1), raw.map(Cow::Borrowed))
} else {
0
}
}
pub(super) fn status_line_height(&self, width: u16, options: ComposerRenderOptions<'_>) -> u16 {
// The unfocused composer paints one dim status line.
if options.unfocused {
return 1;
}
let Some(FooterContent::Extension { content }) = &self.footer.status_line_value else {
return 1;
(available, self.status_surface_content())
};
let mut area = Rect::new(/*x*/ 0, /*y*/ 0, width, /*height*/ 1);
if !options.separate_status_line {
let warning = self.warning_notice_layout(inset_footer_hint_area(area), options);
area = warning_notice::before_warning(area, warning.map(|(rect, _)| rect));
let bounds = inset_footer_hint_area(Rect::new(0, 0, width, available)).as_size();
if bounds.width == 0 || bounds.height == 0 {
return 0;
}
let (_, width) = self.status_line_layout(area, options, content.measure_natural().width);
let cache = &self.footer.measured_status_line;
if let Some((previous_width, height)) = cache.get()
&& previous_width == width
{
return height;
match status.as_deref() {
Some(FooterContent::Extension { content }) => {
let cache = &self.footer.measured_status_line;
if let Some((previous, height)) = cache.get()
&& previous == bounds
{
return height;
}
let height = content.measure(bounds).height;
cache.set(Some((bounds, height)));
height
}
Some(line) => u16::from(line.width() > 0),
None => 0,
}
let height = content
.measure(Size::new(width, MAX_FOOTER_ROWS))
.height
.max(1);
cache.set(Some((width, height)));
height
}
pub(super) fn hint_footer_props(&self, options: ComposerRenderOptions<'_>) -> FooterProps {
pub(super) fn hint_footer_props(&self) -> FooterProps {
let mut props = self.footer_props();
if self.has_separate_status(options) {
if props.status_line_enabled {
props.status_line_enabled = false;
props.status_line_value = None;
props.active_agent_label = None;
@@ -98,64 +81,21 @@ impl ChatComposer {
props
}
fn status_line_layout(
&self,
area: Rect,
options: ComposerRenderOptions<'_>,
left_width: u16,
) -> (Option<Line<'static>>, u16) {
let show_cycle_hint = !self.is_task_running
&& if options.separate_status_line {
!self.blocks_direct_input
&& self.draft.input_enabled
&& !self.popup_active()
&& matches!(
self.footer_mode(),
FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft
)
&& options.footer.is_none_or(|footer| !footer.is_interactive)
} else {
self.footer.collaboration_mode_indicator.is_some()
};
let right = if !options.separate_status_line
&& let Some(label) = &self.footer.side_conversation_context_label
pub(super) fn status_surface_content(&self) -> Option<Cow<'_, FooterContent>> {
if !self.footer.status_line_enabled {
return None;
}
let status = self.footer.status_line_value.as_ref();
if matches!(status, Some(FooterContent::Extension { .. }))
|| self.footer.active_agent_label.is_none()
{
Some(side_conversation_context_line(label))
} else if !options.separate_status_line
&& let Some(shell) = self.shell_mode_footer_line()
{
Some(shell)
} else if !options.separate_status_line
&& self
.effort_status_line_transition
.as_ref()
.is_some_and(|transition| !transition.is_finished())
{
None
} else {
let full = self.mode_indicator_line(show_cycle_hint);
let full_fits = full.as_ref().is_none_or(|right| {
let right_width = right.width() as u16;
(!options.separate_status_line || right_width <= inset_footer_hint_area(area).width)
&& can_show_left_with_context(area, left_width, right_width)
});
if full_fits {
full
} else {
self.mode_indicator_line(/*show_cycle_hint*/ false)
}
};
let right_width = right
.as_ref()
.map_or(/*default*/ 0, |line| line.width() as u16);
let width = max_left_width_for_right(area, right_width).unwrap_or_else(|| {
if options.separate_status_line {
inset_footer_hint_area(area).width
} else {
area.width.saturating_sub(FOOTER_INDENT_COLS as u16)
}
});
(right, width)
return status.map(Cow::Borrowed);
}
let mut props = self.footer_props();
// The viewed agent belongs to native status even during a transient composer hint.
props.mode = FooterMode::ComposerHasDraft;
props.is_task_running = false;
passive_footer_status_line(&props).map(Cow::Owned)
}
pub(super) fn render_status_surface(
@@ -177,42 +117,24 @@ impl ChatComposer {
}
return;
}
let mut props = self.footer_props();
if options.separate_status_line {
// Persistent context is independent of help, queue, search, and quit modes.
props.mode = FooterMode::ComposerHasDraft;
props.is_task_running = false;
}
let line = passive_footer_status_line(&props);
let left_width = line.as_ref().map_or(/*default*/ 0, FooterContent::width);
let (right, width) = self.status_line_layout(area, options, left_width);
let transition = self
let area = inset_footer_hint_area(area);
let line = self.status_surface_content();
if let Some(transition) = self
.effort_status_line_transition
.as_ref()
.filter(|transition| !transition.is_finished());
let content = if let Some(transition) = transition {
transition
.render_line(line.map(FooterContent::into_hyperlink_line), width)
.map(FooterContent::HyperlinkLine)
} else {
line
};
if let Some(content) = content {
content.render(
Rect {
width,
..inset_footer_hint_area(area)
},
buf,
);
}
if let Some(right) = right {
render_context_right(area, buf, &right);
}
if transition.is_some()
&& let Some(frame_requester) = &self.frame_requester
.filter(|transition| !transition.is_finished())
{
frame_requester.schedule_frame_in(EFFORT_STATUS_LINE_FRAME_TICK);
if let Some(frame) = transition.render_line(
line.map(|line| line.into_owned().into_hyperlink_line()),
area.width,
) {
FooterContent::HyperlinkLine(frame).render(area, buf);
}
if let Some(requester) = &self.frame_requester {
requester.schedule_frame_in(EFFORT_STATUS_LINE_FRAME_TICK);
}
} else if let Some(line) = line {
line.into_owned().render(area, buf);
}
}
}
@@ -78,22 +78,29 @@ fn extension_status_line_clips_links_to_footer_layout() {
let (output, _) = render(&composer, /*width*/ 40, /*footer*/ None);
assert!(output.contains("plain"), "{output}");
assert!(!output.contains("bot"), "{output}");
composer.set_status_line(/*status_line*/ None);
let agent_rows = [false, true].map(|unfocused| {
let options = ComposerRenderOptions {
separate_status_line: true,
unfocused,
..Default::default()
};
render_status(&composer, Rect::new(0, 0, 40, 8), options).1
});
assert_eq!(agent_rows, [vec!["bot".to_string()], vec![]]);
}
fn status_rows(
fn render_status(
composer: &ChatComposer,
area: Rect,
options: ComposerRenderOptions<'_>,
) -> Vec<String> {
) -> (Buffer, Vec<String>) {
let layout = composer.layout_with_options(area, options);
let status = if options.separate_status_line {
layout.status
} else {
layout.footer
};
let status = layout.status;
let mut buf = Buffer::empty(area);
composer.render_with_options(area, &mut buf, /*mask_char*/ None, options);
(status.y..status.bottom())
let rows = (status.y..status.bottom())
.map(|y| {
(status.x..status.right())
.map(|x| buf[(x, y)].symbol())
@@ -101,11 +108,12 @@ fn status_rows(
.trim()
.to_owned()
})
.collect()
.collect();
(buf, rows)
}
#[test]
fn extension_layout_uses_the_width_left_by_the_mode_indicator_after_resize() {
fn extension_uses_the_full_width_after_resize() {
let mut composer = composer();
composer.footer.show_warnings_key = None;
let tree: crate::ui_tree::UiTree =
@@ -120,46 +128,31 @@ fn extension_layout_uses_the_width_left_by_the_mode_indicator_after_resize() {
.chars()
.map(|digit| osc8_hyperlink("https://example.com/pr/12345", &digit.to_string()))
.collect();
let [shared, separate] = [false, true].map(|separate_status_line| {
let mut frames = Vec::new();
for (width, mode, warning_count) in [
(16, None, 0),
(15, None, 0),
(16, None, 0),
(25, Some(CollaborationModeIndicator::Plan), 0),
(19, None, 1),
for fullscreen in [false, true] {
for (width, mode, expected_rows) in [
(16, None, 1),
(15, None, 2),
(16, None, 1),
(27, Some(CollaborationModeIndicator::Plan), 1),
(19, Some(CollaborationModeIndicator::Plan), 1),
] {
composer.set_collaboration_mode_indicator(mode);
let options = ComposerRenderOptions {
separate_status_line,
warning_count,
separate_status_line: fullscreen,
warning_count: 1,
..Default::default()
};
let height = composer.desired_height_with_options(width, options);
let area = Rect::new(/*x*/ 0, /*y*/ 0, width, height);
let rows = status_rows(&composer, area, options);
let (buffer, rows) = render_status(&composer, Rect::new(0, 0, width, height), options);
assert_eq!(rows.len(), expected_rows);
for row in &rows {
let text = strip_osc8(row);
assert_eq!(*row, text.replace("12345", &linked_number));
if warning_count == 0 {
frames.push(format!("{width} | {text}"));
}
}
if warning_count > 0 {
assert_eq!(
composer.footer.warning_notice_area.get().unwrap().bottom(),
height
);
assert_eq!(rows.len(), if separate_status_line { 1 } else { 2 });
let last = strip_osc8(rows.last().unwrap());
assert!(last.contains("PR 12345"));
assert_eq!(last.contains("⚠ 1"), !separate_status_line);
assert_eq!(*row, strip_osc8(row).replace("12345", &linked_number));
}
let warning = composer.footer.warning_notice_area.get().unwrap();
assert_eq!(buffer[(warning.x, warning.y)].symbol(), "⚠");
assert_eq!((warning.right(), warning.y), (width - 1, height - 1));
}
frames.join("\n")
});
assert_eq!(shared, separate);
insta::assert_snapshot!(shared);
}
}
#[test]
@@ -173,17 +166,11 @@ fn status_rows_yield_to_the_composer_and_shared_footer_overrides() {
separate_status_line,
..Default::default()
};
let short = if separate_status_line {
"one…"
} else {
"one\ntwo…"
};
for (height, expected) in [(12, "one\ntwo\nthree\nfour…"), (7, short)] {
for (height, expected) in [(12, "one\ntwo\nthree\nfour…"), (7, "one…")] {
let area = Rect::new(/*x*/ 0, /*y*/ 0, /*width*/ 7, height);
assert_eq!(status_rows(&composer, area, options).join("\n"), expected);
let (buffer, rows) = render_status(&composer, area, options);
assert_eq!(rows.join("\n"), expected);
let input = composer.layout_with_options(area, options).textarea;
let mut buffer = Buffer::empty(area);
composer.render_with_options(area, &mut buffer, /*mask_char*/ None, options);
assert!(input.height >= 3);
let draft = [0, 1, 2].map(|dy| buffer[(input.x, input.y + dy)].symbol());
assert_eq!(draft, ["a", "b", "c"]);
@@ -205,7 +192,7 @@ fn status_rows_yield_to_the_composer_and_shared_footer_overrides() {
composer.set_status_line(Some(FooterContent::from(tree)));
assert_eq!(
composer.footer_hint_height(/*width*/ 7, ComposerRenderOptions::default()),
2
1
);
composer.show_footer_flash("Copied".into(), Duration::from_secs(/*secs*/ 3));
assert_eq!(
@@ -227,7 +214,9 @@ fn sidebar_focus_collapses_extension_status_to_its_dim_line() {
..Default::default()
};
let height = composer.desired_height_with_options(/*width*/ 40, options);
status_rows(&composer, Rect::new(0, 0, 40, height), options).join("\n")
render_status(&composer, Rect::new(0, 0, 40, height), options)
.1
.join("\n")
});
insta::assert_snapshot!(frames.join("\n---\n"), @"
one
@@ -307,7 +296,7 @@ fn extension_status_keeps_its_rows_when_effort_changes() {
/*x*/ 0, /*y*/ 0, /*width*/ 7, /*height*/ 12,
);
assert_eq!(
status_rows(&composer, area, options),
render_status(&composer, area, options).1,
["one", "two", "three"]
);
}
@@ -401,8 +390,14 @@ fn fullscreen_plan_indicator_keeps_the_cycle_hint_when_it_fits() {
composer.set_status_line(Some(status.into()));
composer.set_active_agent_label(Some("reviewer".to_string()));
let (narrow, _) = render(&composer, /*width*/ 37, /*footer*/ None);
let row = narrow.lines().rev().nth(1).expect("persistent status row");
insta::assert_snapshot!(strip_osc8(row).trim(), @"project · PR #42 · rev… Plan mode");
assert!(
strip_osc8(&narrow).contains("project · PR #42 · reviewer"),
"{narrow}"
);
assert!(
narrow.lines().last().unwrap().contains("Plan mode"),
"{narrow}"
);
composer.set_active_agent_label(/*active_agent_label*/ None);
composer.set_task_running(/*running*/ true);
@@ -417,15 +412,16 @@ fn fullscreen_plan_indicator_keeps_the_cycle_hint_when_it_fits() {
is_interactive: true,
};
let (search, _) = render(&composer, /*width*/ 100, Some(&footer));
assert!(search.contains("Search: verify"), "{search}");
assert!(search.contains("Plan mode"), "{search}");
assert!(!search.contains("⇧tab"), "{search}");
// Configured items such as git-branch can have no value outside a repository.
composer.set_status_line(/*status_line*/ None);
let (empty_narrow, _) = render(&composer, /*width*/ 24, /*footer*/ None);
assert_eq!(
empty_narrow.lines().rev().nth(1).map(str::trim),
Some("Plan mode")
assert!(
empty_narrow.lines().last().unwrap().contains("Plan mode"),
"{empty_narrow}"
);
let (empty_wide, _) = render(&composer, /*width*/ 100, /*footer*/ None);
assert!(
@@ -435,16 +431,16 @@ fn fullscreen_plan_indicator_keeps_the_cycle_hint_when_it_fits() {
composer.set_input_enabled(/*enabled*/ false, /*placeholder*/ None);
let (read_only, _) = render(&composer, /*width*/ 100, /*footer*/ None);
assert_eq!(
read_only.lines().rev().nth(1).map(str::trim),
Some("Plan mode")
assert!(
read_only.lines().last().unwrap().contains("Plan mode"),
"{read_only}"
);
composer.set_input_enabled(/*enabled*/ true, /*placeholder*/ None);
composer.set_parent_owned_thread();
let (parent_owned, _) = render(&composer, /*width*/ 100, /*footer*/ None);
assert_eq!(
parent_owned.lines().rev().nth(1).map(str::trim),
Some("Plan mode")
assert!(
parent_owned.lines().last().unwrap().contains("Plan mode"),
"{parent_owned}"
);
}
@@ -687,14 +683,14 @@ fn slash_popup_preserves_footer_and_selection_style() {
composer: surface,
textarea,
popup,
footer,
status,
..
} = composer.layout_with_options(area, ComposerRenderOptions::default());
assert_eq!(popup.bottom(), surface.y);
assert_eq!(surface.bottom(), footer.y);
assert_eq!(surface.bottom(), status.y);
assert_eq!(composer.cursor_pos(area), Some((4, textarea.y)));
let footer_text: String = (0..width).map(|x| buf[(x, height - 1)].symbol()).collect();
assert!(footer_text.contains("model · high · fast"));
let status_text: String = (0..width).map(|x| buf[(x, status.y)].symbol()).collect();
assert!(status_text.contains("model · high · fast"));
if !clipped && width == 80 {
insta::assert_snapshot!("slash_popup_footer_wide", format!("{buf:?}"));
}
@@ -1,5 +1,5 @@
//! Fit warning counts and the configured warnings shortcut into the existing passive hint row.
//! Interactive hints retain priority. Shared status content wraps beside the warning.
//! Interactive hints retain priority. Warnings occupy the trailing edge of the hints row.
//! Only the count and warning label receive an amber accent; surrounding hints stay muted.
use super::*;
@@ -67,7 +67,7 @@ fn extension_content_skips_the_effort_transition_in_both_directions() {
fn effort_transition_does_not_queue_a_missing_outgoing_status_line() {
let (mut composer, _rx) = new_test_composer();
composer.set_status_line_enabled(/*enabled*/ true);
composer.set_status_line(Some(Line::from("gpt-5.4 high · main").into()));
composer.set_status_line(/*status_line*/ None);
composer.set_task_running(/*running*/ true);
composer.set_text_content("queued draft".to_string(), Vec::new(), Vec::new());
composer.set_active_reasoning_effort(
@@ -171,13 +171,17 @@ fn effort_transition_keeps_the_full_footer_row() {
);
let mut buf = Buffer::empty(area);
composer.render(area, &mut buf);
let footer = (0..area.width)
.map(|column| buf[(column, area.bottom() - 1)].symbol())
.collect::<String>();
assert!(footer.contains("gpt-5.4 high · feature-branch"));
assert!(!footer.contains("Plan mode"));
insta::assert_snapshot!("effort_transition_keeps_the_full_footer_row", footer);
let rows = [area.bottom() - 2, area.bottom() - 1].map(|y| {
(0..area.width)
.map(|column| buf[(column, y)].symbol())
.collect::<String>()
.trim_end()
.to_owned()
});
insta::assert_snapshot!(
"effort_transition_keeps_the_full_footer_row",
rows.join("\n")
);
}
#[test]
+2 -10
View File
@@ -877,15 +877,6 @@ impl FooterMode {
}
}
/// Whether callers should reserve the dedicated status-line layout for a contextual footer row.
///
/// The dedicated layout exists for the configurable `/statusline` row. An agent label by itself
/// can be rendered by the standard footer flow, so this only becomes `true` when the status line
/// feature is enabled and the current mode allows contextual footer content.
pub(crate) fn uses_passive_footer_status_layout(props: &FooterProps) -> bool {
props.status_line_enabled && props.mode.shows_passive_line(props.is_task_running)
}
pub(crate) fn footer_line_width(
props: &FooterProps,
collaboration_mode_indicator: Option<CollaborationModeIndicator>,
@@ -1070,7 +1061,8 @@ mod tests {
| FooterMode::ShortcutOverlay
| FooterMode::EscHint => false,
};
let status_line_active = uses_passive_footer_status_layout(props);
let status_line_active = props.status_line_enabled
&& props.mode.shows_passive_line(props.is_task_running);
let passive_status_line = if status_line_active {
passive_footer_status_line(props)
.map(|content| content.into_hyperlink_line().line)
@@ -1,10 +1,9 @@
//! The composer measures the natural width before allocating the left column, then renders
//! the selected content in the allotted rectangle.
//! Native and extension status content measured and painted inside the status rectangle.
use crate::line_truncation::line_width;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::text::Line;
use ratatui::widgets::Widget;
use std::sync::Arc;
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
@@ -13,7 +12,7 @@ use crate::terminal_hyperlinks::HyperlinkLine;
use crate::ui_tree::PreparedUiTree;
use crate::ui_tree::UiTree;
/// Owned content for the footer's negotiated left-hand area.
/// Owned status content displayed below the composer.
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum FooterContent {
Line(Line<'static>),
@@ -30,10 +29,10 @@ impl From<UiTree> for FooterContent {
}
impl FooterContent {
/// Natural display width, before the footer allocates space for the right indicator.
/// Natural display width in terminal cells.
pub(crate) fn width(&self) -> u16 {
let width = match self {
Self::Line(line) => line.width(),
Self::Line(line) => line_width(line),
Self::HyperlinkLine(line) => line.width(),
Self::Extension { content } => return content.measure_natural().width,
};
@@ -43,11 +42,11 @@ impl FooterContent {
/// Paint only inside the final area supplied by the parent.
pub(crate) fn render(self, area: Rect, buf: &mut Buffer) {
match self {
Self::Line(line) => Widget::render(line, area, buf),
Self::Extension { content } => {
with_syntax_theme(|theme| content.render(area, theme, buf));
}
Self::HyperlinkLine(mut line) => {
native => {
let mut line = native.into_hyperlink_line();
let area = Rect {
height: area.height.min(1),
..area
+3 -1
View File
@@ -2445,9 +2445,10 @@ impl BottomPane {
pub(crate) fn backdrop_with_options<'a>(
&'a self,
options: ComposerRenderOptions<'a>,
mut options: ComposerRenderOptions<'a>,
) -> RenderableItem<'a> {
let views = if self.centered_dialog().is_some() {
options.is_dialog_backdrop = true;
&self.view_stack[..self.view_stack.len() - 1]
} else {
&self.view_stack
@@ -2598,6 +2599,7 @@ impl BottomPane {
&& options.warning_count == 0
&& options.footer.is_none()
&& !options.separate_status_line
&& !options.is_dialog_backdrop
&& options.command_popup_placement == CommandPopupPlacement::AboveComposer
{
RenderableItem::Borrowed(&self.composer)
@@ -1,5 +1,6 @@
---
source: tui/src/bottom_pane/chat_composer_effort_tests.rs
expression: footer
expression: "rows.join(\"\\n\")"
---
gpt-5.4 high · feature-branch
? for shortcuts Plan mode
@@ -9,5 +9,5 @@ expression: terminal.backend()
" "
" "
" "
" "
" model · project · ← for agents "
" model · project "
" ← for agents · ? for shortcuts "
@@ -9,5 +9,5 @@ expression: terminal.backend()
" "
" "
" "
" "
" gpt-5.4 high fast · ~/code/codex-1 · Context 0% used Shell mode "
" gpt-5.4 high fast · ~/code/codex-1 · Context 0% used "
" Shell mode "
@@ -1,6 +1,5 @@
---
source: tui/src/bottom_pane/chat_composer.rs
assertion_line: 4485
expression: terminal.backend()
---
" "
@@ -10,5 +9,5 @@ expression: terminal.backend()
" "
" "
" "
" "
" gpt-5.4 high fast · ~/code/codex-1 · Context 0% used "
" ? for shortcuts "
@@ -605,7 +605,6 @@ async fn clipped_voice_composer_keeps_the_draft_and_cursor_visible() {
insta::assert_snapshot!(layouts.join("\n\n"), @"
5 rows:
voice ● listening f9 mute /voice stop
› typed
6 rows:
@@ -8,6 +8,7 @@ connecting:
› Ask Codex to do anything
status line stays visible
? for shortcuts
listening:
voice ● listening f9 mute /voice stop
@@ -16,6 +17,7 @@ listening:
› Ask Codex to do anything
status line stays visible
? for shortcuts
speaking:
voice ● speaking f9 mute /voice stop
@@ -24,6 +26,7 @@ speaking:
› Ask Codex to do anything
status line stays visible
? for shortcuts
muted:
voice ◌ muted f9 unmute /voice stop
@@ -32,6 +35,7 @@ muted:
› Ask Codex to do anything
status line stays visible
? for shortcuts
transcript:
voice ● speaking f9 mute /voice stop
@@ -40,6 +44,7 @@ transcript:
› Ask Codex to do anything
status line stays visible
? for shortcuts
interrupted:
voice ● heard f9 mute /voice stop
@@ -48,3 +53,4 @@ interrupted:
› Ask Codex to do anything
status line stays visible
? for shortcuts
@@ -8,7 +8,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
✗ Request denied for codex to run curl -sS -i -X POST --data-binary
@core/src/codex.rs https://example.com
@@ -18,3 +17,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -8,7 +8,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
✗ Review timed out before codex could run curl -sS -i -X POST --data-binary
@core/src/codex.rs https://example.com
@@ -18,3 +17,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -4,7 +4,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
---
⚠ MCP client for `alpha` failed to start: handshake failed
⚠ MCP startup incomplete (failed: alpha)
@@ -12,3 +11,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -26,7 +26,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
• I’m going to search the repo for where “Change Approved” is rendered to update
@@ -41,4 +40,5 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
› Summarize recent commits
tab to queue message 100% context left
GPT-5.6-Sol default · /tmp/project
tab to queue message
@@ -21,8 +21,8 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
↳ Hello, world! 12
↳ Hello, world! 13
↳ Hello, world! 14
↳ Hello, world! 15
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -11,7 +11,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
• Working (0s • esc to interrupt)
• Messages to be submitted at end of turn
@@ -20,3 +19,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -9,3 +9,4 @@ expression: "normalize_compaction_snapshot(render_bottom_popup(&chat, 80))"
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -9,3 +9,4 @@ expression: "normalize_compaction_snapshot(render_bottom_popup(&chat, 40))"
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -11,6 +11,7 @@ activity:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
history:
<empty>
@@ -21,6 +22,7 @@ activity:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
history:
• Blocked by hook
└ command blocked by policy
@@ -11,6 +11,7 @@ activity:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
history:
• Hook stopped
└ continue with more context
@@ -10,9 +10,11 @@ before completion:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
after completion:
" "
" "
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
@@ -17,3 +17,4 @@ After account change:
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
"› Ask Codex to do anything "
" "
" GPT-5.6 Luna · GPT-5.6 Luna high "
" ? for shortcuts "
@@ -5,4 +5,5 @@ expression: popup
---
› /daybreak
profile: work · provider: server-updated Plan mode (⇧tab to cycle)
profile: work · provider: server-updated
Plan mode (⇧tab to cycle)
@@ -11,3 +11,4 @@ expression: "format!(\"{}\\n{}\", lines_to_single_string(cells.last().unwrap()),
› Viewing sub-agent — direct input is disabled
GPT-6.1-Sol default · /tmp/project
? for shortcuts
@@ -6,3 +6,4 @@ expression: popup
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -8,4 +8,5 @@ expression: snapshot
" "
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project Goal paused (/goal resume) "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts Goal paused (/goal resume) "
@@ -8,4 +8,5 @@ expression: snapshot
" "
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project Goal paused (/goal resume) "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts Goal paused (/goal resume) "
@@ -9,7 +9,7 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -6,10 +6,10 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
• Working (0s • esc to interrupt)
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -8,7 +8,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
⚠ Automatic approval review denied (risk: high): The planned action would
transmit the full contents of a workspace source file (`core/src/codex.rs`) to
`https://example.com`, which is an external and untrusted endpoint.
@@ -22,3 +21,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -9,3 +9,4 @@ expression: normalize_snapshot_paths(rendered)
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -10,3 +10,4 @@ expression: normalize_snapshot_paths(rendered)
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -10,7 +10,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
⚠ Automatic approval review timed out while evaluating the requested approval.
✗ Review timed out before codex could run curl -sS -i -X POST --data-binary
@@ -22,3 +21,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -9,3 +9,4 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 80))"
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -11,3 +11,4 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 80))"
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -15,6 +15,7 @@ exec and hook running:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
history after exec:
• Ran echo done
└ done
@@ -27,6 +28,7 @@ hook running after exec:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
quiet hook completed:
" "
"• Working (0s • esc to interrupt) "
@@ -35,3 +37,4 @@ quiet hook completed:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
@@ -10,4 +10,5 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 80)).replace('
› describe
GPT-5.6-Sol default · /tmp/project
⌃c quit
@@ -10,9 +10,11 @@ before interrupt:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
after interrupt:
" "
" "
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
@@ -11,6 +11,7 @@ expression: "format!(\"80 columns:\\n{}\\n120 columns:\\n{}\", hook_status_frame
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
120 columns:
" "
"• Working (0s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close "
@@ -20,3 +21,4 @@ expression: "format!(\"80 columns:\\n{}\\n120 columns:\\n{}\", hook_status_frame
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
@@ -1,6 +1,6 @@
---
source: tui/src/chatwidget/tests/app_server.rs
expression: "format!(\"transcript:\\n{transcript}\\nfooter:\\n{footer}\\ntitle: {title}\")"
expression: "normalize_snapshot_paths(format!(\"transcript:\\n{transcript}\\nfooter:\\n{footer}\\ntitle: {title}\"))"
---
transcript:
@@ -8,4 +8,5 @@ footer:
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project · review-fix
? for shortcuts
title: review-fix | project
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
@@ -5,4 +5,5 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 80))"
---
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
esc esc to edit previous message
@@ -11,6 +11,7 @@ expression: "format!(\"80 columns:\\n{}\\n120 columns:\\n{}\", hook_status_frame
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
120 columns:
" "
"• Working (0s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close · Running hooks "
@@ -19,3 +20,4 @@ expression: "format!(\"80 columns:\\n{}\\n120 columns:\\n{}\", hook_status_frame
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
@@ -11,6 +11,7 @@ activity:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
history:
<empty>
@@ -23,6 +24,7 @@ activity:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
history:
<empty>
@@ -35,6 +37,7 @@ activity:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
history:
<empty>
@@ -47,5 +50,6 @@ activity:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
history:
<empty>
@@ -16,3 +16,5 @@ expression: "render_bottom_popup(&chat, 100)"
› Review the current changes
for safety
GPT-5.5 default · /tmp/project
@@ -10,3 +10,5 @@ expression: "render_bottom_popup(&chat, 100)"
› Ask Codex to do anything
GPT-5.5 default · /tmp/project
@@ -20,3 +20,5 @@ expression: "render_bottom_popup(&chat, 48)"
› Review the current changes
for safety
GPT-5.5 default · /tmp/project
@@ -10,3 +10,4 @@ expression: normalized_backend_snapshot(terminal.backend())
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
@@ -7,3 +7,4 @@ expression: snapshot
"› Ask Codex to do anything "
" "
" GPT-5.5 max "
" ? for shortcuts "
@@ -6,4 +6,5 @@ expression: snapshot
" "
"› Ask Codex to do anything "
" "
" GPT-5.5 max Plan mode (⇧tab to cycle) "
" GPT-5.5 max "
" ? for shortcuts Plan mode (⇧tab to cycle) "
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
"› Ask Codex to do anything "
" "
" gpt-5.2 high · Roadmap cleanup "
" ? for shortcuts "
@@ -8,3 +8,4 @@ expression: "timer.replace_all(&normalize_snapshot_paths(render_bottom_popup(&ch
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -11,7 +11,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
• Working (0s • esc to interrupt)
• Messages to be submitted at end of turn
@@ -20,3 +19,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
? for shortcuts
@@ -11,6 +11,7 @@ expression: "format!(\"80 columns:\\n{}\\n120 columns:\\n{}\", hook_status_frame
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
120 columns:
" "
"• Working (0s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close "
@@ -20,3 +21,4 @@ expression: "format!(\"80 columns:\\n{}\\n120 columns:\\n{}\", hook_status_frame
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
@@ -8,6 +8,7 @@ before reveal:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
running:
" "
"• Saving session summary "
@@ -16,9 +17,11 @@ running:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
completed:
" "
" "
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
@@ -8,6 +8,7 @@ before reveal:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
running:
" "
"• Running hook "
@@ -16,9 +17,11 @@ running:
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
completed:
" "
" "
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
" ? for shortcuts "
@@ -6,4 +6,5 @@ expression: terminal.backend()
" "
"› Ask a follow-up question "
" "
" GPT-5.6-Sol Side from main thread · ctrl+/ to switch · ctrl+c to close "
" GPT-5.6-Sol "
" ? for shortcuts Side from main thread · ctrl+/ to switch · ctrl+c to close "
@@ -6,4 +6,5 @@ expression: terminal.backend()
" "
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol ctrl+/ for side "
" GPT-5.6-Sol "
" ? for shortcuts ctrl+/ for side "
@@ -6,4 +6,5 @@ expression: terminal.backend()
" "
"› Ask a follow-up question "
" "
" GPT-5.6-Sol default · /tmp/project "
" Side from main thread · main needs input · ctrl+/ to switch · ctrl+c to close "
@@ -2,7 +2,6 @@
source: tui/src/chatwidget/tests/status_and_layout.rs
expression: normalize_snapshot_paths(terminal.backend().vt100().screen().contents())
---
› count to 1
@@ -12,3 +11,4 @@ expression: normalize_snapshot_paths(terminal.backend().vt100().screen().content
› Ask Codex to do anything
gpt-5 default · /tmp/project
? for shortcuts
@@ -3,7 +3,6 @@ source: tui/src/chatwidget/tests/status_and_layout.rs
expression: normalize_snapshot_paths(terminal.backend().vt100().screen().contents())
---
• 1
• Working (0s • esc to interrupt)
@@ -12,3 +11,4 @@ expression: normalize_snapshot_paths(terminal.backend().vt100().screen().content
› Ask Codex to do anything
gpt-5 default · /tmp/project
? for shortcuts
@@ -8,4 +8,5 @@ expression: normalized_backend_snapshot(terminal.backend())
" "
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default Side starting... "
" GPT-5.6-Sol default "
" ? for shortcuts Side starting... "
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · 5.2 credits · ~$0.21 "
" ? for shortcuts "
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
"› Ask Codex to do anything "
" "
" gpt-5.2 high · Standard "
" ? for shortcuts "
@@ -6,4 +6,5 @@ expression: normalized_backend_snapshot(terminal.backend())
" "
"› Ask Codex to do anything "
" "
" GPT-5.5 Pursuing goal (40K / 50K) "
" GPT-5.5 "
" ? for shortcuts Pursuing goal (40K / 50K) "
@@ -6,4 +6,5 @@ expression: normalized_backend_snapshot(terminal.backend())
" "
"› Ask Codex to do anything "
" "
" GPT-5.5 Goal achieved (2d 23h 42m) "
" GPT-5.5 "
" ? for shortcuts Goal achieved (2d 23h 42m) "
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
"› Ask Codex to do anything "
" "
" gpt-5.4 xhigh fast · Context 100% left · /tmp/project "
" ? for shortcuts "
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
"› Ask Codex to do anything "
" "
" gpt-5.4 xhigh fast · Context 0% used · /tmp/project "
" ? for shortcuts "

Some files were not shown because too many files have changed in this diff Show More