Prune TUI snapshots and consolidate behavior tests (#50808)

## What changed

- Remove redundant snapshot fixtures and selected rendering test cases across the TUI.
- Replace several full-output snapshots with direct assertions on messages, popup state, status indicators, and rendered content.
- Consolidate slash-command aliases, command-prefix selection, category titles, and hook events into shared test loops.
- Use shared snapshots for the setup import prompt and paginated transcript overlay across platforms.

GitOrigin-RevId: 461e30ebaf70ba930d91a135d2fc598ee4f04f27
This commit is contained in:
Eric Traut
2026-10-04 06:46:50 +00:00
committed by copyberry
parent ab45264919
commit e57fc9ea5a
97 changed files with 286 additions and 2244 deletions
@@ -251,11 +251,12 @@ async fn archiving_selects_the_next_displayed_task() -> Result<()> {
app_server.shutdown().await?;
}
}
let snapshot = match action {
AgentsOverviewAction::Archive => "archiving_selects_the_next_displayed_task",
AgentsOverviewAction::Delete => "deleting_selects_the_next_displayed_task",
};
insta::assert_snapshot!(snapshot, selections.join("\n"));
if matches!(action, AgentsOverviewAction::Archive) {
insta::assert_snapshot!(
"archiving_selects_the_next_displayed_task",
selections.join("\n")
);
}
}
Ok(())
}
@@ -1,9 +0,0 @@
---
source: tui/src/app/agents_overview_actions_tests.rs
assertion_line: 308
expression: "selections.join(\"\\n\")"
---
› ○ Task 3
› ○ Other 2
› ○ Task 1
› ○ Task 4
@@ -1,11 +0,0 @@
---
source: tui/src/app/tests.rs
expression: "rendered.lines.iter().map(rendered_line_text).collect::<Vec<_>>().join(\"\\n\")"
---
latest user question
• Final answer:
Pattern Outcome
━━━━━━━━━━━━ ━━━━━━━━━━━
Table tail Preserved
+9 -9
View File
@@ -7144,15 +7144,15 @@ async fn required_stream_reflow_during_capped_initial_replay_survives_transcript
let rendered = app.render_transcript_lines_for_reflow(/*width*/ 80);
assert_eq!(rendered.lines.len(), 7);
assert_snapshot!(
"required_stream_reflow_during_capped_initial_replay_survives_transcript_overlay",
rendered
.lines
.iter()
.map(rendered_line_text)
.collect::<Vec<_>>()
.join("\n")
);
let rendered = rendered
.lines
.iter()
.map(rendered_line_text)
.collect::<Vec<_>>()
.join("\n");
assert!(rendered.contains("Table tail"));
assert!(rendered.contains("Preserved"));
assert!(!rendered.contains("stale streamed table tail"));
Ok(())
}
@@ -118,9 +118,9 @@ async fn mcp_login_retry_suppresses_old_completion_and_late_browser_open() {
for event in snapshot.events {
app.handle_thread_event_replay(event);
}
assert_snapshot!(
"mcp_login_retry_completion",
next_history_message(&mut events)
assert_eq!(
next_history_message(&mut events),
"• Signed in to enterprise."
);
assert!(events.try_recv().is_err());
// A cancellation delivered after the replacement response is stale as well.
@@ -1,5 +0,0 @@
---
source: tui/src/app/tests/mcp_login_tests.rs
expression: next_history_message(&mut events)
---
• Signed in to enterprise.
@@ -1,17 +0,0 @@
---
source: tui/src/app/tests/user_verification_routes_tests.rs
expression: "render_bottom_popup(&app.chat_widget, 80)"
---
Approve deployment?
Thread: Verifier [worker]
Server: deployments
Verify the production deployment.
› 1. Verify and approve (y)
2. Cancel this request (c)
Press enter to confirm or esc to cancel
@@ -137,9 +137,9 @@ async fn side_toggle_surfaces_pending_verification_from_an_inactive_thread() ->
app.toggle_side_conversation(&mut tui, &mut app_server)
.await?;
assert_eq!(app.active_thread_id, Some(primary_thread_id));
insta::assert_snapshot!(
"side_toggle_surfaces_pending_user_verification",
render_bottom_popup(&app.chat_widget, /*width*/ 80)
assert!(app.chat_widget.has_active_view());
assert!(
render_bottom_popup(&app.chat_widget, /*width*/ 80).contains("Approve deployment?")
);
app_server.shutdown().await?;
Ok(())
+65 -336
View File
@@ -5266,18 +5266,6 @@ mod tests {
},
);
snapshot_composer_state(
"footer_mode_ctrl_c_interrupt",
/*enhanced_keys_supported*/ true,
|composer| {
composer.set_task_running(/*running*/ true);
composer.show_quit_shortcut_hint(
key_hint::ctrl(KeyCode::Char('c')),
/*has_focus*/ true,
);
},
);
snapshot_composer_state(
"footer_mode_ctrl_c_then_esc_hint",
/*enhanced_keys_supported*/ true,
@@ -9275,20 +9263,6 @@ mod tests {
},
);
snapshot_composer_state(
"remote_image_rows_selected",
/*enhanced_keys_supported*/ false,
|composer| {
composer.set_remote_image_urls(vec![
"https://example.com/one.png".to_string(),
"https://example.com/two.png".to_string(),
]);
composer.set_text_content("describe these".to_string(), Vec::new(), Vec::new());
composer.draft.textarea.set_cursor(/*pos*/ 0);
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
},
);
snapshot_composer_state(
"remote_image_rows_after_delete_first",
/*enhanced_keys_supported*/ false,
@@ -9308,7 +9282,7 @@ mod tests {
}
#[test]
fn slash_popup_model_first_for_mo_ui() {
fn slash_popup_model_clipping_snapshot() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
@@ -9326,293 +9300,51 @@ mod tests {
// Type "/mo" humanlike so paste-burst doesn’t interfere.
type_chars_humanlike(&mut composer, &['/', 'm', 'o']);
for (height, snapshot, cursor) in [
(3, "slash_popup_mo_clipped", (5, 1)),
(5, "slash_popup_mo", (5, 2)),
let mut terminal =
Terminal::new(TestBackend::new(/*width*/ 60, /*height*/ 3)).expect("create terminal");
terminal
.draw(|f| {
composer.render(f.area(), f.buffer_mut());
assert_eq!(composer.cursor_pos(f.area()), Some((5, 1)));
})
.expect("draw composer");
// The picker sits above the draft; clipping must keep the draft and cursor visible.
insta::assert_snapshot!("slash_popup_mo_clipped", terminal.backend());
}
#[test]
fn slash_popup_selects_first_matching_command() {
use super::super::command_popup::CommandItem;
for (input, expected) in [
("/mo", "model"),
("/res", "resume"),
("/ar", "archive"),
("/pet", "pets"),
("/bt", "btw"),
("/si", "side"),
] {
let mut terminal =
Terminal::new(TestBackend::new(/*width*/ 60, height)).expect("create terminal");
terminal
.draw(|f| {
composer.render(f.area(), f.buffer_mut());
assert_eq!(composer.cursor_pos(f.area()), Some(cursor));
})
.expect("draw composer");
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
/*has_input_focus*/ true,
sender,
/*enhanced_keys_supported*/ false,
"Ask Codex to do anything".to_string(),
/*disable_paste_burst*/ false,
);
type_chars_humanlike(&mut composer, &input.chars().collect::<Vec<_>>());
// The picker sits above the draft; clipping must keep the draft and cursor visible.
insta::assert_snapshot!(snapshot, terminal.backend());
}
}
#[test]
fn slash_popup_model_first_for_mo_logic() {
use super::super::command_popup::CommandItem;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
/*has_input_focus*/ true,
sender,
/*enhanced_keys_supported*/ false,
"Ask Codex to do anything".to_string(),
/*disable_paste_burst*/ false,
);
type_chars_humanlike(&mut composer, &['/', 'm', 'o']);
match &composer.popups.active {
ActivePopup::Command(popup) => match popup.selected_item() {
Some(CommandItem::Builtin(cmd)) => {
assert_eq!(cmd.command(), "model")
}
Some(CommandItem::ServiceTier(command)) => {
panic!("expected model command, got service tier {command:?}")
}
None => panic!("no selected command for '/mo'"),
},
_ => panic!("slash popup not active after typing '/mo'"),
}
}
#[test]
fn slash_popup_resume_for_res_ui() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
/*has_input_focus*/ true,
sender,
/*enhanced_keys_supported*/ false,
"Ask Codex to do anything".to_string(),
/*disable_paste_burst*/ false,
);
// Type "/res" humanlike so paste-burst doesn’t interfere.
type_chars_humanlike(&mut composer, &['/', 'r', 'e', 's']);
let mut terminal = Terminal::new(TestBackend::new(60, 6)).expect("terminal");
terminal
.draw(|f| composer.render(f.area(), f.buffer_mut()))
.expect("draw composer");
// Snapshot should show /resume as the first entry for /res.
insta::assert_snapshot!("slash_popup_res", terminal.backend());
}
#[test]
fn slash_popup_archive_for_ar_ui() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
/*has_input_focus*/ true,
sender,
/*enhanced_keys_supported*/ false,
"Ask Codex to do anything".to_string(),
/*disable_paste_burst*/ false,
);
type_chars_humanlike(&mut composer, &['/', 'a', 'r']);
let mut terminal = Terminal::new(TestBackend::new(60, 5)).expect("terminal");
terminal
.draw(|f| composer.render(f.area(), f.buffer_mut()))
.expect("draw composer");
insta::assert_snapshot!("slash_popup_ar", terminal.backend());
}
#[test]
fn slash_popup_resume_for_res_logic() {
use super::super::command_popup::CommandItem;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
/*has_input_focus*/ true,
sender,
/*enhanced_keys_supported*/ false,
"Ask Codex to do anything".to_string(),
/*disable_paste_burst*/ false,
);
type_chars_humanlike(&mut composer, &['/', 'r', 'e', 's']);
match &composer.popups.active {
ActivePopup::Command(popup) => match popup.selected_item() {
Some(CommandItem::Builtin(cmd)) => {
assert_eq!(cmd.command(), "resume")
}
Some(CommandItem::ServiceTier(command)) => {
panic!("expected resume command, got service tier {command:?}")
}
None => panic!("no selected command for '/res'"),
},
_ => panic!("slash popup not active after typing '/res'"),
}
}
#[test]
fn slash_popup_pets_for_pet_ui() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
/*has_input_focus*/ true,
sender,
/*enhanced_keys_supported*/ false,
"Ask Codex to do anything".to_string(),
/*disable_paste_burst*/ false,
);
type_chars_humanlike(&mut composer, &['/', 'p', 'e', 't']);
let mut terminal = Terminal::new(TestBackend::new(60, 5)).expect("terminal");
terminal
.draw(|f| composer.render(f.area(), f.buffer_mut()))
.expect("draw composer");
insta::assert_snapshot!("slash_popup_pet", terminal.backend());
}
#[test]
fn slash_popup_pets_for_pet_logic() {
use super::super::command_popup::CommandItem;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
/*has_input_focus*/ true,
sender,
/*enhanced_keys_supported*/ false,
"Ask Codex to do anything".to_string(),
/*disable_paste_burst*/ false,
);
type_chars_humanlike(&mut composer, &['/', 'p', 'e', 't']);
match &composer.popups.active {
ActivePopup::Command(popup) => match popup.selected_item() {
Some(CommandItem::Builtin(cmd)) => {
assert_eq!(cmd.command(), "pets")
}
Some(CommandItem::ServiceTier(command)) => {
panic!("expected pets command, got service tier {command:?}")
}
None => panic!("no selected command for '/pet'"),
},
_ => panic!("slash popup not active after typing '/pet'"),
}
}
#[test]
fn slash_popup_btw_for_bt_ui() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
/*has_input_focus*/ true,
sender,
/*enhanced_keys_supported*/ false,
"Ask Codex to do anything".to_string(),
/*disable_paste_burst*/ false,
);
type_chars_humanlike(&mut composer, &['/', 'b', 't']);
let mut terminal = Terminal::new(TestBackend::new(60, 5)).expect("terminal");
terminal
.draw(|f| composer.render(f.area(), f.buffer_mut()))
.expect("draw composer");
insta::assert_snapshot!("slash_popup_bt", terminal.backend());
}
#[test]
fn slash_popup_btw_for_bt_logic() {
use super::super::command_popup::CommandItem;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
/*has_input_focus*/ true,
sender,
/*enhanced_keys_supported*/ false,
"Ask Codex to do anything".to_string(),
/*disable_paste_burst*/ false,
);
type_chars_humanlike(&mut composer, &['/', 'b', 't']);
match &composer.popups.active {
ActivePopup::Command(popup) => match popup.selected_item() {
Some(CommandItem::Builtin(cmd)) => {
assert_eq!(cmd.command(), "btw")
}
Some(CommandItem::ServiceTier(command)) => {
panic!("expected btw command, got service tier {command:?}")
}
None => panic!("no selected command for '/bt'"),
},
_ => panic!("slash popup not active after typing '/bt'"),
}
}
#[test]
fn slash_popup_side_for_si_ui() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
/*has_input_focus*/ true,
sender,
/*enhanced_keys_supported*/ false,
"Ask Codex to do anything".to_string(),
/*disable_paste_burst*/ false,
);
type_chars_humanlike(&mut composer, &['/', 's', 'i']);
let mut terminal = Terminal::new(TestBackend::new(60, 5)).expect("terminal");
terminal
.draw(|f| composer.render(f.area(), f.buffer_mut()))
.expect("draw composer");
insta::assert_snapshot!("slash_popup_si", terminal.backend());
}
#[test]
fn slash_popup_side_for_si_logic() {
use super::super::command_popup::CommandItem;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
/*has_input_focus*/ true,
sender,
/*enhanced_keys_supported*/ false,
"Ask Codex to do anything".to_string(),
/*disable_paste_burst*/ false,
);
type_chars_humanlike(&mut composer, &['/', 's', 'i']);
match &composer.popups.active {
ActivePopup::Command(popup) => match popup.selected_item() {
Some(CommandItem::Builtin(cmd)) => {
assert_eq!(cmd.command(), "side")
}
Some(CommandItem::ServiceTier(command)) => {
panic!("expected side command, got service tier {command:?}")
}
None => panic!("no selected command for '/si'"),
},
_ => panic!("slash popup not active after typing '/si'"),
match &composer.popups.active {
ActivePopup::Command(popup) => match popup.selected_item() {
Some(CommandItem::Builtin(cmd)) => assert_eq!(cmd.command(), expected),
Some(CommandItem::ServiceTier(command)) => {
panic!("expected {expected} command, got service tier {command:?}")
}
None => panic!("no selected command for '{input}'"),
},
_ => panic!("slash popup not active after typing '{input}'"),
}
}
}
@@ -12063,31 +11795,28 @@ mod tests {
assert!(composer.handle_paste_burst_flush(now + PasteBurst::recommended_flush_delay()));
assert_eq!(composer.draft.textarea.text(), " ");
assert!(!composer.is_in_paste_burst());
let snapshot_name = if modifiers == KeyModifiers::NONE {
"plain_space_is_rendered"
} else {
"shift_space_is_rendered"
};
snapshot_composer_state(
snapshot_name,
/*enhanced_keys_supported*/ false,
|composer| {
let now = Instant::now();
for input in [
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE),
KeyEvent::new(KeyCode::Char(' '), modifiers),
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE),
] {
composer.handle_input_basic_with_time(input, now);
}
assert!(composer.handle_paste_burst_flush(
now + PasteBurst::recommended_active_flush_delay()
));
},
);
}
snapshot_composer_state(
"plain_space_is_rendered",
/*enhanced_keys_supported*/ false,
|composer| {
let now = Instant::now();
for input in [
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE),
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE),
] {
composer.handle_input_basic_with_time(input, now);
}
assert!(
composer.handle_paste_burst_flush(
now + PasteBurst::recommended_active_flush_delay()
)
);
},
);
}
#[test]
@@ -570,26 +570,6 @@ mod tests {
insta::assert_snapshot!("command_popup_voice", format!("{buf:?}"));
}
#[cfg(target_os = "macos")]
#[test]
fn default_command_popup_items_snapshot() {
let mut popup = CommandPopup::new(CommandPopupFlags::default(), Vec::new());
popup.on_composer_text_change("/".to_string());
let commands = popup
.filtered_items()
.into_iter()
.map(|item| {
let command = item.command();
let description = item.description();
format!("/{command} - {description}")
})
.collect::<Vec<_>>()
.join("\n");
insta::assert_snapshot!("command_popup_default_items", commands);
}
#[test]
fn prefix_filter_limits_matches_for_ac() {
let mut popup = CommandPopup::new(CommandPopupFlags::default(), Vec::new());
@@ -298,13 +298,8 @@ fn voice_discovery_requires_the_client_runtime() {
.unwrap();
assert!(view.pre_draw_tick(Instant::now()));
assert_eq!(view.features.len(), usize::from(supported));
snapshot_view(
if supported {
"voice_runtime_available"
} else {
"voice_runtime_unavailable"
},
&view,
);
if supported {
snapshot_view("voice_runtime_available", &view);
}
}
}
@@ -48,17 +48,16 @@ fn make_view(category: FeedbackCategory) -> FeedbackNoteView {
}
#[test]
fn feedback_view_bad_result() {
let view = make_view(FeedbackCategory::BadResult);
let rendered = render(&view, /*width*/ 60);
insta::assert_snapshot!("feedback_view_bad_result", rendered);
}
#[test]
fn feedback_view_good_result() {
let view = make_view(FeedbackCategory::GoodResult);
let rendered = render(&view, /*width*/ 60);
insta::assert_snapshot!("feedback_view_good_result", rendered);
fn feedback_category_titles() {
for (category, title) in [
(FeedbackCategory::BadResult, "▌ Tell us more (bad result)"),
(FeedbackCategory::GoodResult, "▌ Tell us more (good result)"),
(FeedbackCategory::Bug, "▌ Tell us more (bug)"),
(FeedbackCategory::Other, "▌ Tell us more (other)"),
] {
let rendered = render(&make_view(category), /*width*/ 60);
assert_eq!(rendered.lines().next(), Some(title));
}
}
#[test]
@@ -68,13 +67,6 @@ fn feedback_view_bug() {
insta::assert_snapshot!("feedback_view_bug", rendered);
}
#[test]
fn feedback_view_other() {
let view = make_view(FeedbackCategory::Other);
let rendered = render(&view, /*width*/ 60);
insta::assert_snapshot!("feedback_view_other", rendered);
}
#[test]
fn feedback_view_safety_check() {
let view = make_view(FeedbackCategory::SafetyCheck);
@@ -82,22 +74,6 @@ fn feedback_view_safety_check() {
insta::assert_snapshot!("feedback_view_safety_check", rendered);
}
#[test]
fn feedback_view_with_connectivity_diagnostics() {
let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let view = FeedbackNoteView::new(
FeedbackCategory::Bug,
/*turn_id*/ None,
tx,
/*include_logs*/ false,
FeedbackAudience::External,
);
let rendered = render(&view, /*width*/ 60);
insta::assert_snapshot!("feedback_view_with_connectivity_diagnostics", rendered);
}
#[test]
fn feedback_disclosure_matches_audience_at_normal_and_narrow_widths() {
let mut rendered = Vec::new();
-17
View File
@@ -1375,23 +1375,6 @@ mod tests {
},
);
snapshot_footer(
"footer_ctrl_c_quit_running",
FooterProps {
mode: FooterMode::QuitShortcutReminder,
esc_backtrack_hint: false,
is_task_running: true,
queue_submissions: false,
collaboration_modes_enabled: false,
is_wsl: false,
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
status_line_value: None,
status_line_enabled: false,
key_hints: FooterKeyHints::default_bindings(),
active_agent_label: None,
},
);
snapshot_footer(
"footer_esc_hint_idle",
FooterProps {
@@ -222,13 +222,6 @@ mod tests {
assert_eq!(queue.desired_height(/*width*/ 40), 0);
}
#[test]
fn desired_height_one_message() {
let mut queue = PendingInputPreview::new();
queue.queued_messages.push("Hello, world!".to_string());
assert_eq!(queue.desired_height(/*width*/ 40), 3);
}
#[test]
fn render_one_message() {
let mut queue = PendingInputPreview::new();
@@ -240,21 +233,6 @@ mod tests {
assert_snapshot!("render_one_message", format!("{buf:?}"));
}
#[test]
fn render_one_message_with_shift_left_binding() {
let mut queue = PendingInputPreview::new();
queue.queued_messages.push("Hello, world!".to_string());
queue.set_edit_binding(Some(key_hint::shift(KeyCode::Left).into()));
let width = 40;
let height = queue.desired_height(width);
let mut buf = Buffer::empty(Rect::new(0, 0, width, height));
queue.render(Rect::new(0, 0, width, height), &mut buf);
assert_snapshot!(
"render_one_message_with_shift_left_binding",
format!("{buf:?}")
);
}
#[test]
fn render_two_messages() {
let mut queue = PendingInputPreview::new();
@@ -1,13 +0,0 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
" "
"› Ask Codex to do anything "
" "
" "
" "
" "
" "
" "
" ⌃c again to quit "
@@ -1,13 +0,0 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
" "
" [Image #1] "
" [Image #2] "
" "
"› describe these "
" "
" "
" "
" 100% context left "
@@ -1,13 +0,0 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
" "
"› a b "
" "
" "
" "
" "
" "
" "
" 100% context left "
@@ -1,9 +0,0 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
"› /archive archive this session "
" "
"› /ar "
" "
" 100% context left "
@@ -1,9 +0,0 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
"› /btw start a side conversation in an ephemeral fork "
" "
"› /bt "
" "
" 100% context left "
@@ -1,9 +0,0 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
"› /model choose what model and reasoning effort to use "
" "
"› /mo "
" "
" 100% context left "
@@ -1,9 +0,0 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
"› /pets choose or hide the terminal pet "
" "
"› /pet "
" "
" 100% context left "
@@ -1,11 +0,0 @@
---
source: tui/src/bottom_pane/chat_composer.rs
assertion_line: 9511
expression: terminal.backend()
---
"› /resume resume a saved chat "
" "
"› /res "
" "
" "
" 100% context left "
@@ -1,9 +0,0 @@
---
source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend()
---
"› /side start a side conversation in an ephemeral fork "
" "
"› /si "
" "
" 100% context left "
@@ -1,53 +0,0 @@
---
source: tui/src/bottom_pane/command_popup.rs
expression: commands
---
/model - choose what model and reasoning effort to use
/ide - include current selection, open files, and other context from your IDE
/permissions - choose what Codex is allowed to do
/keymap - remap TUI shortcuts
/vim - toggle Vim mode for the composer
/experimental - toggle experimental features
/approve - approve one retry of a recent auto-review denial
/memories - configure memory use and generation
/skills - use skills to improve how Codex performs specific tasks
/import - import setup, this project, and recent chats from Claude Code
/hooks - view and manage lifecycle hooks
/review - review my current changes and find issues
/rename - rename the current thread
/new - start a new chat during a conversation
/archive - archive this session
/delete - permanently delete this session
/resume - resume a saved chat
/fork - fork the current chat
/app - continue this session in the Desktop app
/init - create an AGENTS.md file with instructions for Codex
/compact - summarize conversation to prevent hitting the context limit
/recap - summarize the current conversation now
/agents - open the agent command center
/side - start a side conversation in an ephemeral fork
/copy - copy the last response or part of it
/export - export the conversation as markdown
/raw - toggle raw scrollback mode for copy-friendly terminal selection
/tui - choose the TUI mode for the next launch
/diff - show git diff (including untracked files)
/mention - mention a file
/status - show current session configuration and token usage
/daemon - Manage the local background server
/warnings - view retained warnings and diagnostic details
/cd - change the current working directory
/pwd - show the current working directory
/title - configure which items appear in the terminal title
/statusline - configure which items appear in the status line
/theme - choose a syntax highlighting theme
/pets - choose or hide the terminal pet
/mcp - list MCP tools; use /mcp verbose or /mcp login <name>
/logout - log out of Codex
/exit - exit Codex
/feedback - send logs to maintainers
/rollout - print the rollout file path
/ps - list background terminals
/stop - stop all background terminals
/clear - clear the terminal and start a new chat
/test-approval - test approval request
/subagents - switch between this session's subagents
@@ -1,15 +0,0 @@
---
source: tui/src/bottom_pane/experimental_features_view_tests.rs
expression: buffer_text(&buffer)
---
Experimental features
Checked features are configured on. Some experimental features take effect
only in new tasks or after restarting the Codex server.
No server experiments available.
No experimental features available for now
space toggle · enter save · esc save/close
@@ -1,11 +0,0 @@
---
source: tui/src/bottom_pane/feedback_note_view_tests.rs
expression: rendered
---
▌ Tell us more (bad result)
▌
▌ Your feedback can be used to improve ChatGPT. Learn more.
▌
▌ (optional) Write a short description to help us further
Press enter to submit or esc to cancel
@@ -1,11 +0,0 @@
---
source: tui/src/bottom_pane/feedback_note_view_tests.rs
expression: rendered
---
▌ Tell us more (good result)
▌
▌ Your feedback can be used to improve ChatGPT. Learn more.
▌
▌ (optional) Write a short description to help us further
Press enter to submit or esc to cancel
@@ -1,11 +0,0 @@
---
source: tui/src/bottom_pane/feedback_note_view_tests.rs
expression: rendered
---
▌ Tell us more (other)
▌
▌ Your feedback can be used to improve ChatGPT. Learn more.
▌
▌ (optional) Write a short description to help us further
Press enter to submit or esc to cancel
@@ -1,11 +0,0 @@
---
source: tui/src/bottom_pane/feedback_note_view_tests.rs
expression: rendered
---
▌ Tell us more (bug)
▌
▌ Your feedback can be used to improve ChatGPT. Learn more.
▌
▌ (optional) Write a short description to help us further
Press enter to submit or esc to cancel
@@ -1,5 +0,0 @@
---
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
" ⌃c again to quit "
@@ -1,27 +0,0 @@
---
source: tui/src/bottom_pane/message_queue.rs
expression: "format!(\"{buf:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 40, height: 5 },
content: [
" ↳ This is ",
" a message ",
" with many ",
" … ",
" alt + ↑ edit ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 11, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 13, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 13, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 5, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 16, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}
@@ -1,18 +0,0 @@
---
source: tui/src/bottom_pane/message_queue.rs
expression: "format!(\"{buf:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 40, height: 2 },
content: [
" ↳ Hello, world! ",
" alt + ↑ edit ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 17, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 16, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}
@@ -1,22 +0,0 @@
---
source: tui/src/bottom_pane/message_queue.rs
expression: "format!(\"{buf:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 40, height: 3 },
content: [
" ↳ Hello, world! ",
" ↳ This is another message ",
" alt + ↑ edit ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 17, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 27, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 16, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}
@@ -1,25 +0,0 @@
---
source: tui/src/bottom_pane/message_queue.rs
expression: "format!(\"{buf:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 40, height: 4 },
content: [
" ↳ This is a longer message that should",
" be wrapped ",
" ↳ This is another message ",
" alt + ↑ edit ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 14, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 27, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 16, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}
@@ -1,22 +0,0 @@
---
source: tui/src/bottom_pane/pending_input_preview.rs
expression: "format!(\"{buf:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 40, height: 3 },
content: [
"• Queued follow-up inputs ",
" ↳ Hello, world! ",
" ⇧← edit last queued message ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 17, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
x: 6, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 31, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}
@@ -153,7 +153,6 @@ async fn delegation_started_before_peer_connection_keeps_its_voice_origin() {
Ok(AppCommand::RealtimeConversationSpeech { text, .. }) if text.as_str() == "Spoken answer"
));
assert!(ops.try_recv().is_err());
let mut rendered_history = Vec::new();
commit_realtime_history_events(&mut chat, &mut events);
while let Ok(event) = events.try_recv() {
if let AppEvent::InsertHistoryCell(cell) = event
@@ -166,13 +165,8 @@ async fn delegation_started_before_peer_connection_keeps_its_voice_origin() {
.collect::<String>();
assert!(!rendered.contains("Spoken answer"));
assert!(!rendered.contains("<realtime_delegation>"));
rendered_history.push(rendered);
}
}
insta::assert_snapshot!(
"voice_delegation_during_connection",
rendered_history.join("\n")
);
}
#[tokio::test]
@@ -1,6 +0,0 @@
---
source: tui/src/chatwidget/realtime_tests/handoffs.rs
assertion_line: 514
expression: "rendered_history.join(\"\\n\")"
---
• Voice conversation started. Use /voice mute to mute or /voice to stop.
@@ -1,9 +0,0 @@
---
source: tui/src/chatwidget/realtime_tests/transcripts.rs
assertion_line: 446
expression: rendered
---
› Correction
• First answer
@@ -324,7 +324,6 @@ async fn intentional_stop_preserves_both_interleaved_partials_once() {
.join("\n");
assert_eq!(rendered.matches("Correction").count(), 1);
assert_eq!(rendered.matches("First answer").count(), 1);
insta::assert_snapshot!("interleaved_partials_on_stop", rendered);
}
#[tokio::test]
@@ -1,12 +0,0 @@
---
source: tui/src/chatwidget/tests/bedrock_catalog_tests.rs
expression: "render_bottom_popup(&chat, 100)"
---
Advanced Reasoning
⚠ Consumes usage limits faster
› 1. Max For difficult problems when quality matters more than speed · higher usage
2. Ultra For demanding work using multiple agents · highest usage
enter default · s session · esc back
@@ -1,153 +0,0 @@
---
source: tui/src/chatwidget/tests.rs
expression: "lines[start_idx..].join(\"\\n\")"
---
• I need to check the codex-rs repository to explain why the project's binaries
are large. The user is likely seeking specifics about the setup: are Rust
builds static, what features are enabled, and is debug information included?
It could be due to static linking, included OpenSSL, or how panic handling
is set up. I should look into the Cargo.toml file to confirm features and
profiles without needing to edit any code. Let's get started on this!
─ Worked for 0s ────────────────────────────────────────────────────────────────
• I’m going to scan the workspace and Cargo manifests to see build profiles and
dependencies that impact binary size. Then I’ll summarize the main causes.
• Explored
└ List ls -la
Read Cargo.toml
• I'm reviewing the workspace's release profile, which has settings like
lto=fat, strip=symbols, and codegen-units=1 to reduce binary size. However, it
seems that "strip = symbols" only removes symbols, not debug information. In
Rust 1.73+, both "symbols" and "debuginfo" stripping options are available. I
should check whether using these results in a smaller binary. I’ll also skim
through crates for heavy dependencies in Cargo.toml, including cli, core,
exec, linux-sandbox, tui, login, ollama, and mcp.
• Ran for d in ansi-escape apply-patch arg0 cli common core exec execpolicy
│ file-search linux-sandbox login mcp-client mcp-server ollama
│ tui; do echo "--- $d/Cargo.toml"; sed -n '1,200p' $d/Cargo.toml; echo;
│ … +1 line
└ --- ansi-escape/Cargo.toml
[package]
… +243 lines
] }
tracing = { version
• Explored
└ Read Cargo.toml
• I’m evaluating how binary size is influenced by various factors. There are
many binaries, such as codex and codex-exec, which include dependencies by
default, pulling in features like Tokio and hyper. For instance, reqwest
includes default features like TLS through rustls. If default features remain
enabled, it inflates the size with many additional components. I notice
that for musl targets, using vendored builds of OpenSSL could significantly
increase binary size. This is quite the intricate web of dependencies!
Analyzing musl and binary size
I’m considering the implications of building for musl with static linking.
In this case, OpenSSL might come into play through some crates, but reqwest
defaults to using rustls, although openssl-sys vendored could increase
binary size. It’s interesting to think about whether they’re targeting musl,
especially since it seems they might often be building for Mac and Linux with
dynamic linking instead.
I also realize there’s a difference between debug and release builds — release
should be smaller due to optimizations, but if they’re defaulting to debug
builds, that could lead to much larger binaries.
Assessing crate impacts on size
I need to be precise when analyzing binary sizes from various crates. Some
crates, like ratatui-image, include heavy features such as libjpeg decoders,
which can really bulk things up. Then there are the tree-sitter crates that
contain large static grammar tables, adding significant size. The starlark
crate and tracing-subscriber also contribute to size increases.
With static linking per binary being standard in Rust, it leads to multiple
copies of the same dependencies unless cdylibs are used. That makes the
compile times longer, but using LTO helps reduce bloat somewhat through
deduplication.
Evaluating binary size factors
Having multiple binaries indeed increases the overall size significantly.
I'm considering options like upgrading to "opt-level = 'z'" or using
"strip=debuginfo" for reducing size. It's interesting that while the profile
only sets symbols for stripping, not debuginfo, this could inflate sizes if
retained in release builds.
The distinction between "symbols" and "debuginfo" is critical; "symbols"
keeps debug information which may not align with the goal of minimizing
size. I’d like to verify with the Rust documentation, but my network access
is restricted.
Explaining large binary sizes
I’m analyzing why binaries in this project are so large. The primary reasons
include monolithic static linking, where each binary includes the full
dependency graph without shared libraries. Heavy dependencies such as reqwest,
hyper, and TLS components also contribute significantly to size, along with
image decoders and other libraries.
Using MUSL targets means vendored OpenSSL can drastically increase the
size due to static linking. Furthermore, if the release profile keeps debug
information due to using "strip = symbols," that can lead to larger binaries
as well. It’s a mix of several factors, including panic strategies and
platform-specific linking behavior.
Identifying binary size contributors
I’m considering why binaries are still large even without static linking. The
tracing-subscriber crate potentially adds weight because it includes regex,
though it can sometimes use a lighter version. References from files like
"core/Cargo.toml" show dependencies like reqwest and tokio, while the tui uses
the image crate.
To present this clearly, I’ll format the final answer with two main sections:
"Main Causes" and "Build-Mode Notes." I can also include brief suggestions for
reducing size, but I want to stay focused on answering the user's question.
─ Worked for 0s ────────────────────────────────────────────────────────────────
• Here’s what’s driving size in this workspace’s binaries.
Main Causes
- Static linking style: Each bin (codex, codex-tui, codex-exec, codex-mcp-
server, etc.) statically links its full dependency graph, so common code isn’t
shared at runtime across executables.
- Heavy deps (HTTP/TLS): reqwest brings in Hyper, HTTP/2, compressors,
and a TLS stack (rustls by default; OpenSSL on musl). In core, login, tui,
and ollama you enable reqwest with json/stream, which still pulls a large
transitive set.
- Image/terminal stack: codex-tui includes image (with jpeg), ratatui,
crossterm, and ratatui-image, which together contribute significant code size
(decoders, pixel ops, terminal backends).
- Parsers/VMs: tree-sitter + tree-sitter-bash (in core and apply-patch) and
starlark (in execpolicy) include sizeable parser/VM tables and runtimes.
- Tokio runtime: Broad tokio features (rt-multi-thread, macros, process,
signal) across many crates inflate code size even if only subsets are used
per bin.
- Panic + backtraces: Default panic = unwind and backtrace support keep
unwinding tables and symbols that add weight.
- Per-target OpenSSL (musl): For *-unknown-linux-musl, core enables openssl-
sys with vendored, compiling OpenSSL into the binary—this adds multiple
megabytes per executable.
Build-Mode Notes
- Release settings: You use lto = "fat" and codegen-units = 1 (good for size),
but strip = "symbols" keeps debuginfo. Debuginfo is often the largest single
contributor; if you build in release with that setting, binaries can still
be large.
- Debug builds: cargo build (dev profile) includes full debuginfo, no LTO, and
assertions—outputs are much larger than cargo build --release.
If you want, I can outline targeted trims (e.g., strip = "debuginfo", opt-
level = "z", panic abort, tighter tokio/reqwest features) and estimate impact
per binary.
@@ -1,5 +0,0 @@
---
source: tui/src/chatwidget/tests.rs
expression: terminal.backend()
---
" "
@@ -1,6 +0,0 @@
---
source: tui/src/chatwidget/tests.rs
expression: terminal.backend()
---
" "
" "
@@ -1,7 +0,0 @@
---
source: tui/src/chatwidget/tests/status_and_layout.rs
expression: normalized_backend_snapshot(terminal.backend())
---
" "
" "
" "
@@ -1,7 +0,0 @@
---
source: tui/src/chatwidget/tests/exec_flow.rs
assertion_line: 217
expression: lines_to_single_string(&aborted_long)
---
✗ You canceled the request to run echo
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...
@@ -1,6 +0,0 @@
---
source: tui/src/chatwidget/tests/exec_flow.rs
assertion_line: 183
expression: lines_to_single_string(&aborted_multi)
---
✗ You canceled the request to run echo line1 ...
@@ -1,6 +0,0 @@
---
source: tui/src/chatwidget/tests/exec_flow.rs
assertion_line: 47
expression: lines_to_single_string(&decision)
---
✔ You approved codex to run echo hello world this time
@@ -1,44 +0,0 @@
---
source: tui/src/chatwidget/tests/exec_flow.rs
expression: "format!(\"{buf:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 80, height: 14 },
content: [
" ",
" ",
" Would you like to run the following command? ",
" ",
" Reason: this is a test reason such as one that would be produced by the ",
" model ",
" ",
" $ echo hello world ",
" ",
" ",
"› 1. Yes, proceed (y) ",
" 2. No, and tell Codex what to do differently (esc) ",
" ",
" Press enter to confirm or esc to cancel ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 2, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
x: 46, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 10, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC,
x: 73, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 2, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC,
x: 7, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 4, y: 7, fg: Rgb(137, 180, 250), bg: Reset, underline: Reset, modifier: NONE,
x: 8, y: 7, fg: Rgb(205, 214, 244), bg: Reset, underline: Reset, modifier: NONE,
x: 20, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED,
x: 0, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 48, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
x: 51, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 2, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 8, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
x: 13, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 28, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
x: 31, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
]
}
@@ -1,7 +0,0 @@
---
source: tui/src/chatwidget/tests.rs
expression: active_blob(&chat)
---
• Explored
└ List ls -la
Read foo.txt
@@ -1,5 +0,0 @@
---
source: tui/src/chatwidget/tests/exec_flow.rs
expression: combined
---
• Viewed image example.png
@@ -1,11 +0,0 @@
---
source: tui/src/chatwidget/tests/goal_menu.rs
expression: rendered_goal_summary(&mut rx)
---
Goal
Status: stalled
Objective: Keep improving the bare goal command until it feels calm and useful.
Time used: 1m
Tokens used: 12.5K
Commands: /goal edit, /goal resume, /goal clear
@@ -1,11 +0,0 @@
---
source: tui/src/chatwidget/tests/goal_menu.rs
expression: rendered_goal_summary(&mut rx)
---
Goal
Status: usage limited
Objective: Keep improving the bare goal command until it feels calm and useful.
Time used: 1m
Tokens used: 12.5K
Commands: /goal edit, /goal resume, /goal clear
@@ -1,11 +0,0 @@
---
source: tui/src/chatwidget/tests/exec_flow.rs
expression: normalized_backend_snapshot(terminal.backend())
---
" "
"• Working (0s • esc to interrupt) "
" "
" "
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
@@ -1,6 +0,0 @@
---
source: tui/src/chatwidget/tests.rs
expression: snapshot
---
cells=1
■ Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue.
@@ -1,5 +0,0 @@
---
source: tui/src/chatwidget/tests/helpers.rs
expression: combined
---
↳ Hook · Heads up from the hook
@@ -1,5 +0,0 @@
---
source: tui/src/chatwidget/tests/exec_flow.rs
expression: combined
---
• Viewed image example.png
@@ -1,11 +0,0 @@
---
source: tui/src/chatwidget/tests/compaction_tests.rs
expression: "normalize_compaction_snapshot(render_bottom_popup(&chat, 80))"
---
• Compacting context (<elapsed> • esc to interrupt)
└ Making room to continue.
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project
@@ -1,11 +0,0 @@
---
source: tui/src/chatwidget/tests/permission_shortcuts_tests.rs
assertion_line: 156
expression: "render_bottom_popup(&chat, 80)"
---
Update Model Permissions
› Loading permission profiles…
enter select · esc back
@@ -1,7 +0,0 @@
---
source: tui/src/chatwidget/tests/plan_mode.rs
expression: "render_bottom_popup(&chat, 36)"
---
› make a plan
gpt-5 default · /tmp/project
@@ -1,12 +0,0 @@
---
source: tui/src/chatwidget/tests/plan_mode.rs
expression: popup
---
Implement this plan?
1. Yes, implement this plan Switch to Default and start coding
› 2. Yes, clear context and implement Fresh thread with this plan
3. No, stay in Plan mode Continue planning with the model
enter select · esc back
@@ -1,5 +0,0 @@
---
source: tui/src/chatwidget/tests/helpers.rs
expression: combined
---
↳ Hook · Heads up from the hook
@@ -1,5 +0,0 @@
---
source: tui/src/chatwidget/tests/helpers.rs
expression: combined
---
↳ Hook · Heads up from the hook
@@ -1,11 +0,0 @@
---
source: tui/src/chatwidget/tests/exec_flow.rs
expression: normalized_backend_snapshot(terminal.backend())
---
" "
"• Working (0s • esc to interrupt) "
" "
" "
"› Ask Codex to do anything "
" "
" GPT-5.6-Sol default · /tmp/project "
@@ -1,5 +0,0 @@
---
source: tui/src/chatwidget/tests.rs
expression: header
---
› Ask Codex to do anything
@@ -1,5 +0,0 @@
---
source: tui/src/chatwidget/tests/helpers.rs
expression: combined
---
↳ Hook · Heads up from the hook
@@ -1,14 +0,0 @@
---
source: tui/src/chatwidget/tests.rs
expression: terminal.backend().vt100().screen().contents()
---
✨ New version available! Would you like to update?
Full release notes: https://github.com/openai/codex/releases/latest
› 1. Yes, update now
2. No, not now
3. Don't remind me
Press enter to confirm or esc to go back
@@ -63,10 +63,12 @@ async fn bedrock_astra_model_and_reasoning_pickers() {
_ => None,
});
chat.open_advanced_reasoning_popup(advanced.expect("advanced reasoning popup"));
assert_chatwidget_snapshot!(
format!("bedrock_{name}_astra_advanced_reasoning"),
render_bottom_popup(&chat, /*width*/ 100)
);
if name == "mantle" {
assert_chatwidget_snapshot!(
"bedrock_mantle_astra_advanced_reasoning",
render_bottom_popup(&chat, /*width*/ 100)
);
}
chat.handle_key_event(KeyEvent::from(KeyCode::Char('2')));
let selected =
std::iter::from_fn(|| events.try_recv().ok()).find_map(|event| match event {
@@ -92,9 +92,10 @@ async fn compaction_status_survives_follow_up_and_preserves_turn_time() {
async fn manual_compaction_shows_status_before_backend_events() {
let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
chat.dispatch_command(SlashCommand::Compact);
assert_chatwidget_snapshot!(
"manual_compaction_pending",
normalize_compaction_snapshot(render_bottom_popup(&chat, /*width*/ 80))
assert!(chat.bottom_pane.status_indicator_visible());
assert_eq!(
chat.bottom_pane.status_widget().unwrap().header(),
"Compacting context"
);
assert!(chat.handle_turn_start_rejection("Could not start compaction".to_string()));
assert!(!chat.bottom_pane.status_indicator_visible());
+5 -269
View File
@@ -420,259 +420,6 @@ declined
");
}
#[tokio::test]
async fn exec_approval_emits_proposed_command_and_decision_history() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
// Trigger an exec approval request with a short, single-line command
let ev = ExecApprovalRequestEvent {
kind: Default::default(),
call_id: "call-short".into(),
approval_id: Some("call-short".into()),
turn_id: "turn-short".into(),
environment_id: None,
command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
reason: Some(
"this is a test reason such as one that would be produced by the model".into(),
),
network_approval_context: None,
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
};
handle_exec_approval_request(&mut chat, "sub-short", ev);
let proposed_cells = drain_insert_history(&mut rx);
assert!(
proposed_cells.is_empty(),
"expected approval request to render via modal without emitting history cells"
);
// The approval modal should display the command snippet for user confirmation.
let area = Rect::new(0, 0, 80, chat.desired_height(/*width*/ 80));
let mut buf = ratatui::buffer::Buffer::empty(area);
chat.render(area, &mut buf);
assert_chatwidget_snapshot!("exec_approval_modal_exec", format!("{buf:?}"));
// Approve via keyboard and verify a concise decision history line is added
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
let decision = drain_insert_history(&mut rx)
.pop()
.expect("expected decision cell in history");
assert_chatwidget_snapshot!(
"exec_approval_history_decision_approved_short",
lines_to_single_string(&decision)
);
}
#[test]
fn app_server_exec_approval_request_splits_shell_wrapped_command() {
let script = r#"python3 -c 'print("Hello, world!")'"#;
let request = exec_approval_request_from_params(
AppServerCommandExecutionRequestApprovalParams {
kind: Default::default(),
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "item-1".to_string(),
started_at_ms: 0,
approval_id: Some("approval-1".to_string()),
environment_id: None,
reason: None,
network_approval_context: None,
command: Some(
shlex::try_join(["/bin/zsh", "-lc", script])
.expect("round-trippable shell wrapper"),
),
cwd: Some(test_path_buf("/tmp").abs().into()),
command_actions: None,
additional_permissions: None,
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
available_decisions: None,
},
&test_path_buf("/tmp").abs(),
);
assert_eq!(
request.command,
vec![
"/bin/zsh".to_string(),
"-lc".to_string(),
script.to_string(),
]
);
}
#[tokio::test]
async fn exec_approval_uses_approval_id_when_present() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
handle_exec_approval_request(
&mut chat,
"sub-short",
ExecApprovalRequestEvent {
kind: Default::default(),
call_id: "call-parent".into(),
approval_id: Some("approval-subcommand".into()),
turn_id: "turn-short".into(),
environment_id: None,
command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
reason: Some(
"this is a test reason such as one that would be produced by the model".into(),
),
network_approval_context: None,
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
},
);
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
let mut found = false;
while let Ok(app_ev) = rx.try_recv() {
if let AppEvent::SubmitThreadOp {
op: Op::ExecApproval { id, decision, .. },
..
} = app_ev
{
assert_eq!(id, "approval-subcommand");
assert_matches!(
decision,
codex_app_server_protocol::CommandExecutionApprovalDecision::Accept
);
found = true;
break;
}
}
assert!(found, "expected ExecApproval op to be sent");
}
#[tokio::test]
async fn exec_approval_decision_truncates_multiline_and_long_commands() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
// Multiline command: modal should show full command, history records decision only
let ev_multi = ExecApprovalRequestEvent {
kind: Default::default(),
call_id: "call-multi".into(),
approval_id: Some("call-multi".into()),
turn_id: "turn-multi".into(),
environment_id: None,
command: vec!["bash".into(), "-lc".into(), "echo line1\necho line2".into()],
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
reason: Some(
"this is a test reason such as one that would be produced by the model".into(),
),
network_approval_context: None,
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
};
handle_exec_approval_request(&mut chat, "sub-multi", ev_multi);
let proposed_multi = drain_insert_history(&mut rx);
assert!(
proposed_multi.is_empty(),
"expected multiline approval request to render via modal without emitting history cells"
);
let area = Rect::new(0, 0, 80, chat.desired_height(/*width*/ 80));
let mut buf = ratatui::buffer::Buffer::empty(area);
chat.render(area, &mut buf);
let mut saw_first_line = false;
for y in 0..area.height {
let mut row = String::new();
for x in 0..area.width {
row.push(buf[(x, y)].symbol().chars().next().unwrap_or(' '));
}
if row.contains("echo line1") {
saw_first_line = true;
break;
}
}
assert!(
saw_first_line,
"expected modal to show first line of multiline snippet"
);
// Deny via keyboard; decision snippet should be single-line and elided with " ..."
chat.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));
let aborted_multi = drain_insert_history(&mut rx)
.pop()
.expect("expected aborted decision cell (multiline)");
assert_chatwidget_snapshot!(
"exec_approval_history_decision_aborted_multiline",
lines_to_single_string(&aborted_multi)
);
// Very long single-line command: decision snippet should be truncated <= 80 chars with trailing ...
let long = format!("echo {}", "a".repeat(200));
let ev_long = ExecApprovalRequestEvent {
kind: Default::default(),
call_id: "call-long".into(),
approval_id: Some("call-long".into()),
turn_id: "turn-long".into(),
environment_id: None,
command: vec!["bash".into(), "-lc".into(), long],
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
reason: None,
network_approval_context: None,
proposed_execpolicy_amendment: None,
proposed_network_policy_amendments: None,
additional_permissions: None,
available_decisions: None,
};
handle_exec_approval_request(&mut chat, "sub-long", ev_long);
let proposed_long = drain_insert_history(&mut rx);
assert!(
proposed_long.is_empty(),
"expected long approval request to avoid emitting history cells before decision"
);
chat.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));
let aborted_long = drain_insert_history(&mut rx)
.pop()
.expect("expected aborted decision cell (long)");
assert_chatwidget_snapshot!(
"exec_approval_history_decision_aborted_long",
lines_to_single_string(&aborted_long)
);
}
#[tokio::test]
async fn preamble_keeps_working_status_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.thread_id = Some(ThreadId::new());
// Regression sequence: a preamble line is committed to history before any exec/tool event.
// After commentary completes, the status row should be restored before subsequent work.
chat.on_task_started();
chat.on_agent_message_delta("Preamble line\n".to_string());
chat.on_commit_tick();
drain_insert_history(&mut rx);
complete_assistant_message(
&mut chat,
"msg-commentary-snapshot",
"Preamble line\n",
Some(MessagePhase::Commentary),
);
let height = chat.desired_height(/*width*/ 80);
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, height))
.expect("create terminal");
terminal
.draw(|f| chat.render(f.area(), f.buffer_mut()))
.expect("draw preamble + status widget");
assert_chatwidget_snapshot!(
"preamble_keeps_working_status",
normalized_backend_snapshot(terminal.backend())
);
}
#[tokio::test]
async fn unified_exec_begin_restores_status_indicator_after_preamble() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -1319,7 +1066,7 @@ async fn view_image_tool_call_adds_history_cell() {
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1, "expected a single history cell");
let combined = lines_to_single_string(&cells[0]);
assert_chatwidget_snapshot!("local_image_attachment_history_snapshot", combined);
assert_eq!(combined, "• Viewed image example.png\n");
}
#[tokio::test]
@@ -1334,7 +1081,7 @@ async fn view_image_tool_call_preserves_foreign_path() {
let cells = drain_insert_history(&mut rx);
assert_eq!(cells.len(), 1, "expected a single history cell");
let combined = lines_to_single_string(&cells[0]);
assert_chatwidget_snapshot!("foreign_image_attachment_history_snapshot", combined);
assert_eq!(combined, "• Viewed image example.png\n");
}
#[tokio::test]
@@ -1347,18 +1094,6 @@ async fn image_generation_begin_restores_working_status_after_single_line_preamb
assert!(chat.bottom_pane.is_task_running());
assert!(chat.bottom_pane.status_indicator_visible());
let width: u16 = 80;
let height = chat.desired_height(width);
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(width, height))
.expect("create terminal");
terminal
.draw(|frame| chat.render(frame.area(), frame.buffer_mut()))
.expect("draw image generation status");
assert_chatwidget_snapshot!(
"image_generation_begin_restores_working_status",
normalized_backend_snapshot(terminal.backend())
);
}
#[tokio::test]
@@ -1422,7 +1157,8 @@ async fn exec_history_extends_previous_when_consecutive() {
"",
/*exit_code*/ 0,
);
assert_chatwidget_snapshot!("exploring_step4_finish_cat_foo", active_blob(&chat));
let explored_foo = active_blob(&chat);
assert_chatwidget_snapshot!("exploring_step4_finish_cat_foo", &explored_foo);
// 5) Start & complete "sed -n 100,200p foo.txt" (treated as Read of foo.txt)
let begin_sed_range = begin_exec(&mut chat, "call-sed-range", "sed -n 100,200p foo.txt");
@@ -1433,7 +1169,7 @@ async fn exec_history_extends_previous_when_consecutive() {
"",
/*exit_code*/ 0,
);
assert_chatwidget_snapshot!("exploring_step5_finish_sed_range", active_blob(&chat));
assert_eq!(active_blob(&chat), explored_foo);
// 6) Start & complete "cat bar.txt"
let begin_cat_bar = begin_exec(&mut chat, "call-cat-bar", "cat bar.txt");
@@ -28,34 +28,6 @@ async fn goal_menu_paused_snapshot() {
assert_chatwidget_snapshot!("goal_menu_paused", rendered_goal_summary(&mut rx));
}
#[tokio::test]
async fn goal_menu_blocked_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let thread_id = ThreadId::new();
chat.show_goal_summary(test_goal(
thread_id,
AppThreadGoalStatus::Blocked,
/*token_budget*/ None,
));
assert_chatwidget_snapshot!("goal_menu_blocked", rendered_goal_summary(&mut rx));
}
#[tokio::test]
async fn goal_menu_usage_limited_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let thread_id = ThreadId::new();
chat.show_goal_summary(test_goal(
thread_id,
AppThreadGoalStatus::UsageLimited,
/*token_budget*/ None,
));
assert_chatwidget_snapshot!("goal_menu_usage_limited", rendered_goal_summary(&mut rx));
}
#[tokio::test]
async fn goal_menu_budget_limited_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
+3 -3
View File
@@ -2,6 +2,7 @@ use super::*;
use codex_app_server_protocol::ImageGenerationItem;
use codex_app_server_protocol::PluginAvailability;
use codex_utils_absolute_path::test_support::PathExt;
use pretty_assertions::assert_eq;
pub(super) async fn test_config() -> (tempfile::TempDir, Config) {
// Start from the built-in defaults so tests do not inherit host/system config.
@@ -1751,11 +1752,10 @@ pub(super) fn hook_run(
}
}
pub(super) async fn assert_hook_events_snapshot(
pub(super) async fn assert_hook_events(
event_name: codex_app_server_protocol::HookEventName,
run_id: &str,
status_message: &str,
snapshot_name: &str,
) {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.on_task_started();
@@ -1816,7 +1816,7 @@ pub(super) async fn assert_hook_events_snapshot(
.iter()
.map(|lines| lines_to_single_string(lines))
.collect::<String>();
assert_chatwidget_snapshot!(snapshot_name, combined);
assert_eq!(combined, "↳ Hook · Heads up from the hook\n");
}
/// Normalize timestamps only in structurally identified completion footer cells.
@@ -156,10 +156,7 @@ async fn permission_shortcuts_load_once_and_reuse_the_picker_catalog() {
chat.chat_keymap.next_permission_mode = vec![crate::key_hint::plain(KeyCode::F(8))];
chat.handle_key_event(KeyEvent::from(KeyCode::F(8)));
let request_id = assert_matches!(events.try_recv(), Ok(AppEvent::FetchPermissionProfiles { request_id, .. }) => request_id);
assert_chatwidget_snapshot!(
"permission_shortcut_loading",
render_bottom_popup(&chat, /*width*/ 80)
);
assert!(chat.bottom_pane.has_active_view());
chat.handle_key_event(KeyEvent::from(KeyCode::Esc));
chat.handle_key_event(KeyEvent::from(KeyCode::F(8)));
assert!(events.try_recv().is_err(), "reuse pending fetch");
@@ -79,19 +79,6 @@ async fn plan_draft_footer_snapshot() {
);
}
#[tokio::test]
async fn plan_draft_footer_narrow_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
chat.bottom_pane
.set_composer_text("make a plan".to_string(), Vec::new(), Vec::new());
chat.pre_draw_tick();
assert_chatwidget_snapshot!(
"plan_draft_footer_narrow",
normalize_snapshot_paths(render_bottom_popup(&chat, /*width*/ 36))
);
}
#[tokio::test]
async fn plan_implementation_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
@@ -115,17 +102,6 @@ async fn plan_implementation_popup_context_usage_snapshot() {
assert_chatwidget_snapshot!("plan_implementation_popup_context_usage", popup);
}
#[tokio::test]
async fn plan_implementation_popup_no_selected_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
chat.on_plan_item_completed("- Step 1\n- Step 2\n".to_string());
chat.open_plan_implementation_prompt();
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
let popup = render_bottom_popup(&chat, /*width*/ 80);
assert_chatwidget_snapshot!("plan_implementation_popup_no_selected", popup);
}
#[tokio::test]
async fn plan_implementation_popup_yes_emits_submit_message_event() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
+93 -202
View File
@@ -3,30 +3,6 @@ use pretty_assertions::assert_eq;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
#[tokio::test]
async fn forked_thread_history_line_without_name_shows_id_once_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let forked_from_id =
ThreadId::from_string("019c2d47-4935-7423-a190-05691f566092").expect("forked id");
chat.emit_forked_thread_event(forked_from_id, /*fork_parent_title*/ None);
let history_cell = tokio::time::timeout(std::time::Duration::from_secs(2), async {
loop {
match rx.recv().await {
Some(AppEvent::InsertHistoryCell(cell)) => break cell,
Some(_) => continue,
None => panic!("app event channel closed before forked thread history was emitted"),
}
}
})
.await
.expect("timed out waiting for forked thread history");
let combined = lines_to_single_string(&history_cell.display_lines(/*width*/ 80));
assert_chatwidget_snapshot!("forked_thread_history_line_without_name", combined);
}
#[tokio::test]
async fn suppressed_interrupted_turn_notice_skips_history_warning() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -147,57 +123,34 @@ async fn slash_side_is_rejected_for_side_threads() {
}
#[tokio::test]
async fn slash_side_is_rejected_during_review_mode() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.review.is_review_mode = true;
async fn side_aliases_are_rejected_during_review_mode() {
for (command, name) in [(SlashCommand::Side, "side"), (SlashCommand::Btw, "btw")] {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.review.is_review_mode = true;
chat.dispatch_command(SlashCommand::Side);
chat.dispatch_command(command);
let event = rx
.try_recv()
.expect("expected review-mode side conversation error");
match event {
AppEvent::InsertHistoryCell(cell) => {
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
assert!(
rendered.contains("'/side' is unavailable while code review is running."),
"expected review-mode side conversation error, got {rendered:?}"
);
let event = rx
.try_recv()
.expect("expected review-mode side conversation error");
match event {
AppEvent::InsertHistoryCell(cell) => {
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
assert!(
rendered.contains(&format!(
"'/{name}' is unavailable while code review is running."
)),
"expected review-mode side conversation error, got {rendered:?}"
);
}
other => panic!("expected InsertHistoryCell error, got {other:?}"),
}
other => panic!("expected InsertHistoryCell error, got {other:?}"),
assert!(rx.try_recv().is_err(), "expected no follow-up events");
assert!(
op_rx.try_recv().is_err(),
"expected no side conversation op"
);
}
assert!(rx.try_recv().is_err(), "expected no follow-up events");
assert!(
op_rx.try_recv().is_err(),
"expected no side conversation op"
);
}
#[tokio::test]
async fn slash_btw_is_rejected_during_review_mode() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.review.is_review_mode = true;
chat.dispatch_command(SlashCommand::Btw);
let event = rx
.try_recv()
.expect("expected review-mode btw conversation error");
match event {
AppEvent::InsertHistoryCell(cell) => {
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
assert!(
rendered.contains("'/btw' is unavailable while code review is running."),
"expected review-mode btw conversation error, got {rendered:?}"
);
}
other => panic!("expected InsertHistoryCell error, got {other:?}"),
}
assert!(rx.try_recv().is_err(), "expected no follow-up events");
assert!(
op_rx.try_recv().is_err(),
"expected no side conversation op"
);
}
#[tokio::test]
@@ -248,145 +201,83 @@ async fn submit_user_message_as_plain_user_turn_does_not_run_shell_commands() {
}
#[tokio::test]
async fn slash_side_without_args_starts_empty_side_conversation() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let parent_thread_id = ThreadId::new();
chat.thread_id = Some(parent_thread_id);
chat.on_task_started();
chat.bottom_pane
.set_composer_text("/side".to_string(), Vec::new(), Vec::new());
async fn side_aliases_without_args_start_empty_side_conversations() {
for input in ["/side", "/btw"] {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let parent_thread_id = ThreadId::new();
chat.thread_id = Some(parent_thread_id);
chat.on_task_started();
chat.bottom_pane
.set_composer_text(input.to_string(), Vec::new(), Vec::new());
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
assert_matches!(
rx.try_recv(),
Ok(AppEvent::StartSide {
parent_thread_id: emitted_parent_thread_id,
user_message: None,
}) if emitted_parent_thread_id == parent_thread_id
);
assert!(
op_rx.try_recv().is_err(),
"bare /side should not submit an op on the parent thread"
);
assert!(chat.input_queue.queued_user_messages.is_empty());
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
assert_matches!(
rx.try_recv(),
Ok(AppEvent::StartSide {
parent_thread_id: emitted_parent_thread_id,
user_message: None,
}) if emitted_parent_thread_id == parent_thread_id
);
assert!(
op_rx.try_recv().is_err(),
"bare {input} should not submit an op on the parent thread"
);
assert!(chat.input_queue.queued_user_messages.is_empty());
}
}
#[tokio::test]
async fn slash_btw_without_args_starts_empty_side_conversation() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let parent_thread_id = ThreadId::new();
chat.thread_id = Some(parent_thread_id);
chat.on_task_started();
chat.bottom_pane
.set_composer_text("/btw".to_string(), Vec::new(), Vec::new());
async fn side_aliases_request_forked_questions_while_task_running() {
for input in ["/side explore the codebase", "/btw explore the codebase"] {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let parent_thread_id = ThreadId::new();
chat.thread_id = Some(parent_thread_id);
chat.local_settings.tui.status_line = Some(vec!["model-with-reasoning".to_string()]);
chat.refresh_status_line();
chat.on_task_started();
chat.show_welcome_banner = false;
chat.bottom_pane
.set_composer_text(input.to_string(), Vec::new(), Vec::new());
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
assert_matches!(
rx.try_recv(),
Ok(AppEvent::StartSide {
parent_thread_id: emitted_parent_thread_id,
user_message: Some(user_message),
}) if emitted_parent_thread_id == parent_thread_id
&& user_message
== UserMessage {
text: "explore the codebase".to_string(),
local_images: Vec::new(),
remote_image_urls: Vec::new(),
text_elements: Vec::new(),
mention_bindings: Vec::new(),
}
);
assert!(
op_rx.try_recv().is_err(),
"expected no op on the parent thread"
);
assert_matches!(
rx.try_recv(),
Ok(AppEvent::StartSide {
parent_thread_id: emitted_parent_thread_id,
user_message: None,
}) if emitted_parent_thread_id == parent_thread_id
);
assert!(
op_rx.try_recv().is_err(),
"bare /btw should not submit an op on the parent thread"
);
assert!(chat.input_queue.queued_user_messages.is_empty());
}
#[tokio::test]
async fn slash_side_requests_forked_side_question_while_task_running() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let parent_thread_id = ThreadId::new();
chat.thread_id = Some(parent_thread_id);
chat.local_settings.tui.status_line = Some(vec!["model-with-reasoning".to_string()]);
chat.refresh_status_line();
chat.on_task_started();
chat.show_welcome_banner = false;
chat.bottom_pane.set_composer_text(
"/side explore the codebase".to_string(),
Vec::new(),
Vec::new(),
);
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
assert_matches!(
rx.try_recv(),
Ok(AppEvent::StartSide {
parent_thread_id: emitted_parent_thread_id,
user_message: Some(user_message),
}) if emitted_parent_thread_id == parent_thread_id
&& user_message
== UserMessage {
text: "explore the codebase".to_string(),
local_images: Vec::new(),
remote_image_urls: Vec::new(),
text_elements: Vec::new(),
mention_bindings: Vec::new(),
}
);
assert!(
op_rx.try_recv().is_err(),
"expected no op on the parent thread"
);
let width = 80;
let height = chat.desired_height(width);
let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("create terminal");
terminal
.draw(|f| chat.render(f.area(), f.buffer_mut()))
.expect("draw side conversation footer");
assert_chatwidget_snapshot!(
"slash_side_requests_forked_side_question_while_task_running",
normalized_backend_snapshot(terminal.backend())
);
}
#[tokio::test]
async fn slash_btw_requests_forked_side_question_while_task_running() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let parent_thread_id = ThreadId::new();
chat.thread_id = Some(parent_thread_id);
chat.on_task_started();
chat.bottom_pane.set_composer_text(
"/btw explore the codebase".to_string(),
Vec::new(),
Vec::new(),
);
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
assert_matches!(
rx.try_recv(),
Ok(AppEvent::StartSide {
parent_thread_id: emitted_parent_thread_id,
user_message: Some(user_message),
}) if emitted_parent_thread_id == parent_thread_id
&& user_message
== UserMessage {
text: "explore the codebase".to_string(),
local_images: Vec::new(),
remote_image_urls: Vec::new(),
text_elements: Vec::new(),
mention_bindings: Vec::new(),
}
);
assert!(
op_rx.try_recv().is_err(),
"expected no op on the parent thread"
);
if input.starts_with("/side") {
let width = 80;
let height = chat.desired_height(width);
let mut terminal =
Terminal::new(TestBackend::new(width, height)).expect("create terminal");
terminal
.draw(|f| chat.render(f.area(), f.buffer_mut()))
.expect("draw side conversation footer");
assert_chatwidget_snapshot!(
"slash_side_requests_forked_side_question_while_task_running",
normalized_backend_snapshot(terminal.backend())
);
}
}
}
#[tokio::test]
@@ -1530,12 +1530,12 @@ async fn no_op_stub_slash_command_is_available_from_local_recall() {
}
#[tokio::test]
async fn slash_quit_requests_exit() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.dispatch_command(SlashCommand::Quit);
assert_matches!(rx.try_recv(), Ok(AppEvent::Exit(ExitMode::ShutdownFirst)));
async fn slash_exit_aliases_request_exit() {
for command in [SlashCommand::Quit, SlashCommand::Exit] {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.dispatch_command(command);
assert_matches!(rx.try_recv(), Ok(AppEvent::Exit(ExitMode::ShutdownFirst)));
}
}
#[tokio::test]
@@ -2631,15 +2631,6 @@ async fn queued_menu_slash_keeps_agent_turn_complete_notification() {
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
}
#[tokio::test]
async fn slash_exit_requests_exit() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.dispatch_command(SlashCommand::Exit);
assert_matches!(rx.try_recv(), Ok(AppEvent::Exit(ExitMode::ShutdownFirst)));
}
#[tokio::test]
async fn slash_stop_submits_background_terminal_cleanup() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -2771,23 +2762,25 @@ async fn slash_archive_cancellation_keeps_task_running() {
}
#[tokio::test]
async fn slash_memory_drop_reports_stubbed_feature() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
async fn slash_memory_maintenance_reports_stubbed_feature() {
for command in [SlashCommand::MemoryDrop, SlashCommand::MemoryUpdate] {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.dispatch_command(SlashCommand::MemoryDrop);
chat.dispatch_command(command);
let event = rx.try_recv().expect("expected unsupported-feature error");
match event {
AppEvent::InsertHistoryCell(cell) => {
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
assert!(rendered.contains("Memory maintenance: Not available in TUI yet."));
let event = rx.try_recv().expect("expected unsupported-feature error");
match event {
AppEvent::InsertHistoryCell(cell) => {
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
assert!(rendered.contains("Memory maintenance: Not available in TUI yet."));
}
other => panic!("expected InsertHistoryCell error, got {other:?}"),
}
other => panic!("expected InsertHistoryCell error, got {other:?}"),
assert!(
op_rx.try_recv().is_err(),
"expected no memory op to be sent"
);
}
assert!(
op_rx.try_recv().is_err(),
"expected no memory op to be sent"
);
}
#[tokio::test]
@@ -2878,26 +2871,6 @@ async fn slash_memories_opens_memory_menu() {
assert!(op_rx.try_recv().is_err(), "expected no core op to be sent");
}
#[tokio::test]
async fn slash_memory_update_reports_stubbed_feature() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.dispatch_command(SlashCommand::MemoryUpdate);
let event = rx.try_recv().expect("expected unsupported-feature error");
match event {
AppEvent::InsertHistoryCell(cell) => {
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
assert!(rendered.contains("Memory maintenance: Not available in TUI yet."));
}
other => panic!("expected InsertHistoryCell error, got {other:?}"),
}
assert!(
op_rx.try_recv().is_err(),
"expected no memory op to be sent"
);
}
#[tokio::test]
async fn slash_resume_opens_picker_while_mcp_startup_is_running() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -3031,34 +3004,19 @@ async fn slash_pets_with_arg_selects_named_pet() {
#[tokio::test]
#[serial]
async fn slash_pets_disable_disables_pets_even_on_unsupported_terminal() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
force_tmux_pet_image_unsupported(&mut chat);
async fn slash_pet_disable_aliases_work_on_unsupported_terminal() {
for input in ["/pets disable", "/pet hide"] {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
force_tmux_pet_image_unsupported(&mut chat);
chat.bottom_pane
.set_composer_text("/pets disable".to_string(), Vec::new(), Vec::new());
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
assert_matches!(rx.try_recv(), Ok(AppEvent::PetDisabled));
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
}
#[tokio::test]
#[serial]
async fn slash_pet_hide_disables_pets_even_on_unsupported_terminal() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
force_tmux_pet_image_unsupported(&mut chat);
chat.bottom_pane
.set_composer_text("/pet hide".to_string(), Vec::new(), Vec::new());
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
assert_matches!(rx.try_recv(), Ok(AppEvent::PetDisabled));
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
chat.bottom_pane
.set_composer_text(input.to_string(), Vec::new(), Vec::new());
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
assert_matches!(rx.try_recv(), Ok(AppEvent::PetDisabled));
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
}
}
#[tokio::test]
@@ -3736,10 +3694,6 @@ async fn transcript_copy_feedback_stays_in_the_footer_without_history_or_interru
"transcript_copy_unconfirmed",
render_bottom_popup(&chat, /*width*/ 80)
);
insta::assert_snapshot!(
"transcript_copy_unconfirmed_narrow",
render_bottom_popup(&chat, /*width*/ 40)
);
chat.open_warnings(&[Arc::new(history_cell::new_warning_event(
"selected café".into(),
))]);
@@ -1,7 +0,0 @@
---
source: tui/src/chatwidget/tests/slash_commands.rs
expression: "render_bottom_popup(&chat, 40)"
---
› Ask Codex to do anything
Copy unconfirmed; /export saves chat
@@ -2468,23 +2468,29 @@ async fn ui_snapshots_small_heights_idle() {
}
}
// Snapshot test: ChatWidget at very small heights (task running)
// Validates how status + composer are presented within tight space.
// Running state remains hidden when the terminal is too short to present it.
#[tokio::test]
async fn ui_snapshots_small_heights_task_running() {
async fn ui_small_heights_hide_running_state() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (idle_chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
// Activate status line
handle_turn_started(&mut chat, "turn-1");
handle_agent_reasoning_delta(&mut chat, "**Thinking**");
for h in [1u16, 2, 3] {
let name = format!("chat_small_running_h{h}");
let mut idle_terminal =
Terminal::new(TestBackend::new(40, h)).expect("create idle terminal");
idle_terminal
.draw(|f| idle_chat.render(f.area(), f.buffer_mut()))
.expect("draw idle chat");
let mut terminal = Terminal::new(TestBackend::new(40, h)).expect("create terminal");
terminal
.draw(|f| chat.render(f.area(), f.buffer_mut()))
.expect("draw chat running");
assert_chatwidget_snapshot!(name, normalized_backend_snapshot(terminal.backend()));
assert_eq!(
normalized_backend_snapshot(terminal.backend()),
normalized_backend_snapshot(idle_terminal.backend()),
);
}
}
@@ -5008,36 +5014,31 @@ async fn user_prompt_submit_app_server_hook_notifications_render_snapshot() {
}
#[tokio::test]
async fn interrupt_hook_events_render_snapshot() {
assert_hook_events_snapshot(
codex_app_server_protocol::HookEventName::Interrupt,
"interrupt:0:/tmp/hooks.json",
"cleaning up the interrupted turn",
"interrupt_hook_events_render_snapshot",
)
.await;
}
#[tokio::test]
async fn pre_tool_use_hook_events_render_snapshot() {
assert_hook_events_snapshot(
codex_app_server_protocol::HookEventName::PreToolUse,
"pre-tool-use:0:/tmp/hooks.json",
"warming the shell",
"pre_tool_use_hook_events_render_snapshot",
)
.await;
}
#[tokio::test]
async fn post_tool_use_hook_events_render_snapshot() {
assert_hook_events_snapshot(
codex_app_server_protocol::HookEventName::PostToolUse,
"post-tool-use:0:/tmp/hooks.json",
"warming the shell",
"post_tool_use_hook_events_render_snapshot",
)
.await;
async fn hook_events_render_consistently() {
for (event_name, run_id, status_message) in [
(
codex_app_server_protocol::HookEventName::Interrupt,
"interrupt:0:/tmp/hooks.json",
"cleaning up the interrupted turn",
),
(
codex_app_server_protocol::HookEventName::PreToolUse,
"pre-tool-use:0:/tmp/hooks.json",
"warming the shell",
),
(
codex_app_server_protocol::HookEventName::PostToolUse,
"post-tool-use:0:/tmp/hooks.json",
"warming the shell",
),
(
codex_app_server_protocol::HookEventName::SessionStart,
"session-start:0:/tmp/hooks.json",
"warming the shell",
),
] {
assert_hook_events(event_name, run_id, status_message).await;
}
}
#[tokio::test]
@@ -5588,17 +5589,6 @@ async fn stopped_hook_hides_model_context_and_preserves_stop_reason_snapshot() {
);
}
#[tokio::test]
async fn session_start_hook_events_render_snapshot() {
assert_hook_events_snapshot(
codex_app_server_protocol::HookEventName::SessionStart,
"session-start:0:/tmp/hooks.json",
"warming the shell",
"session_start_hook_events_render_snapshot",
)
.await;
}
fn hook_started_run(
id: &str,
event_name: codex_app_server_protocol::HookEventName,
@@ -868,9 +868,6 @@ mod tests {
);
let rendered = render_screen(&screen, /*width*/ 80, /*height*/ 24);
#[cfg(windows)]
assert_snapshot!("external_agent_config_migration_prompt_windows", rendered);
#[cfg(not(windows))]
assert_snapshot!("external_agent_config_migration_prompt", rendered);
}
@@ -1,24 +0,0 @@
---
source: tui/src/external_agent_config_migration/mod.rs
expression: rendered
---
> Import setup
Bring over supported setup from another coding agent.
Codex may add files to your current project folder.
Your existing setup will not be changed.
[x] Tools & setup
Settings, instructions, integrations, agents, commands, and skills
Importing: Settings 1, Memory 1
[x] Current project
Add Codex files alongside your existing project files
Importing: Plugins 6, Instructions 1
[x] Chat sessions (1)
Last 30 days of chats
Importing: Chat sessions 1
Selected 5 of 5 items.
› 1. Import selected
2. Customize selection
3. Cancel
Use ↑/↓ to move, enter to select, c to customize
@@ -1,11 +0,0 @@
---
source: tui/src/history_cell/tests.rs
expression: rendered
---
╭─────────────────────────────────────────────────╮
│ ✨ Update available! 0.0.0 -> 9.9.9 │
│ Run vp install -g @openai/codex to update. │
│ │
│ See full release notes: │
│ https://github.com/openai/codex/releases/latest │
╰─────────────────────────────────────────────────╯
-11
View File
@@ -1397,17 +1397,6 @@ fn pnpm_update_available_history_cell_snapshot() {
insta::assert_snapshot!(rendered);
}
#[test]
fn vite_plus_update_available_history_cell_snapshot() {
let cell = UpdateAvailableHistoryCell::new(
"9.9.9".to_string(),
Some(UpdateAction::VitePlusGlobalLatest),
);
let rendered = render_lines(&cell.display_lines(/*width*/ 110)).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn web_search_history_cell_without_detail_snapshot() {
let cell = new_web_search_call("call-1".to_string(), String::new(), WebSearchAction::Other);
-10
View File
@@ -1184,16 +1184,6 @@ mod tests {
#[test]
fn picker_question_actions_snapshot() {
let runtime = RuntimeKeymap::defaults();
let params = build_keymap_picker_params_for_selected_action(
&runtime,
&TuiKeymap::default(),
"chat",
"skip_question",
);
assert_snapshot!(
"keymap_question_actions",
render_picker(params, /*width*/ 120)
);
let descriptions = ["edit_queued_message", "prompt_stack_back", "skip_question"]
.map(|action| {
render_picker(
-17
View File
@@ -1556,23 +1556,6 @@ fn code_block_multiple_lines_inside_unordered_list() {
assert_eq!(lines, vec!["• Item", "", " first", " second"]);
}
#[test]
fn code_block_inside_unordered_list_item_multiple_lines() {
let md = "- Item\n\n ```\n first\n second\n ```\n";
let text = render_markdown_text(md);
let lines: Vec<String> = text
.lines
.iter()
.map(|l| {
l.spans
.iter()
.map(|s| s.content.clone())
.collect::<String>()
})
.collect();
assert_eq!(lines, vec!["• Item", "", " first", " second"]);
}
#[test]
fn list_item_after_code_block_keeps_blank_separator() {
let md = "1. First:\n\n ```rust\n fn first() {}\n ```\n\n2. Second:\n";
-54
View File
@@ -483,60 +483,6 @@ mod tests {
assert_snapshot!("model_migration_prompt_gpt5_family", terminal.backend());
}
#[test]
fn prompt_snapshot_gpt5_codex() {
let backend = VT100Backend::new(/*width*/ 60, /*height*/ 22);
let mut terminal = Terminal::with_options(backend).expect("terminal");
terminal.set_viewport_area(Rect::new(0, 0, 60, 22));
let screen = ModelMigrationScreen::new(
FrameRequester::test_dummy(),
migration_copy_for_models(
"gpt-5-codex",
"gpt-5.1-codex-max",
Some("https://www.codex.com/models/gpt-5.1-codex-max".to_string()),
/*migration_copy*/ None,
/*migration_markdown*/ None,
"gpt-5.1-codex-max".to_string(),
Some("Codex-optimized flagship for deep and fast reasoning.".to_string()),
/*can_opt_out*/ false,
),
);
{
let mut frame = terminal.get_frame();
frame.render_widget_ref(&screen, frame.area());
}
terminal.flush().expect("flush");
assert_snapshot!("model_migration_prompt_gpt5_codex", terminal.backend());
}
#[test]
fn prompt_snapshot_gpt5_codex_mini() {
let backend = VT100Backend::new(/*width*/ 60, /*height*/ 22);
let mut terminal = Terminal::with_options(backend).expect("terminal");
terminal.set_viewport_area(Rect::new(0, 0, 60, 22));
let screen = ModelMigrationScreen::new(
FrameRequester::test_dummy(),
migration_copy_for_models(
"gpt-5-codex-mini",
"gpt-5.1-codex-mini",
Some("https://www.codex.com/models/gpt-5.1-codex-mini".to_string()),
/*migration_copy*/ None,
/*migration_markdown*/ None,
"gpt-5.1-codex-mini".to_string(),
Some("Optimized for codex. Cheaper, faster, but less capable.".to_string()),
/*can_opt_out*/ false,
),
);
{
let mut frame = terminal.get_frame();
frame.render_widget_ref(&screen, frame.area());
}
terminal.flush().expect("flush");
assert_snapshot!("model_migration_prompt_gpt5_codex_mini", terminal.backend());
}
#[test]
fn escape_key_accepts_prompt() {
let mut screen = ModelMigrationScreen::new(
@@ -1,48 +0,0 @@
---
source: tui/src/pager_overlay/transcript_tests.rs
assertion_line: 417
expression: snapshots
---
--- loading ---
/ T R A N S C R I P T / / / / / / / / / / / / / / / / / / / / / / / / /
recent transcript
↑ Loading earlier messages…
--- partial ---
/ T R A N S C R I P T / / / / / / / / / / / / / / / / / / / / / / / / /
recent transcript
Earlier messages available. ⌃space select
↑/↓ to scroll · pgup/pgdn to page · home/end to jump
q close · f3 find · esc browse prompts
--- failed ---
/ T R A N S C R I P T / / / / / / / / / / / / / / / / / / / / / / / / /
recent transcript
Retry history: ⌥</⌃home.
--- complete ---
/ T R A N S C R I P T / / / / / / / / / / / / / / / / / / / / / / / / /
recent transcript
⌃space select
↑/↓ to scroll · pgup/pgdn to page · home/end to jump
q close · f3 find · esc browse prompts
@@ -454,12 +454,7 @@ fn transcript_overlay_snapshots_paginated_history_states() {
snapshots.push_str(&format!("--- {name} ---\n{}", buffer_to_text(&buf, area)));
}
let snapshot_name = if cfg!(target_os = "macos") {
"transcript_overlay_paginated_history_states"
} else {
"transcript_overlay_paginated_history_states_non_macos"
};
assert_snapshot!(snapshot_name, snapshots);
assert_snapshot!("transcript_overlay_paginated_history_states", snapshots);
}
#[test]
-30
View File
@@ -5668,36 +5668,6 @@ session_picker_view = "dense"
);
}
#[test]
fn dense_session_snapshot_includes_cwd_in_all_filter() {
assert_snapshot!(
"resume_picker_dense_all",
render_dense_row_snapshot(
/*show_all*/ true, /*filter_cwd*/ None, /*width*/ 120,
)
);
}
#[test]
fn dense_session_snapshot_auto_hides_cwd_when_narrow() {
assert_snapshot!(
"resume_picker_dense_all_auto_hidden_cwd",
render_dense_row_snapshot(
/*show_all*/ true, /*filter_cwd*/ None, /*width*/ 100,
)
);
}
#[test]
fn dense_session_snapshot_forces_cwd_when_narrow() {
assert_snapshot!(
"resume_picker_dense_all_forced_cwd",
render_dense_row_snapshot(
/*show_all*/ true, /*filter_cwd*/ None, /*width*/ 48,
)
);
}
#[test]
fn dense_session_snapshot_drops_metadata_when_narrow() {
assert_snapshot!(
@@ -1,39 +0,0 @@
---
source: tui/src/keymap_setup.rs
expression: "render_picker(params, 120)"
---
Keymap
All configurable shortcuts.
151 actions, 0 customized, 5 unbound.
All Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval ›
Type to search shortcuts
Global - Open Agents unbound
Global Open Transcript ctrl-t
Global Find Transcript f3
Global Focus Activity f4
Global Open External Editor ctrl-g
Global Copy ctrl-o
Global Clear Terminal ctrl-l
Global - Toggle Vim Mode unbound
Global Toggle Raw Output alt-r
Global Toggle Side Conversation ctrl-/
Chat Interrupt Turn esc
Chat Decrease Reasoning Effort alt-,, shift-down
Chat Increase Reasoning Effort alt-., shift-up
Chat - Previous Permission Mode unbound
Chat - Next Permission Mode unbound
Chat Edit Queued Message shift-left, alt-up
Chat Prompt Stack Back shift-right, alt-down
› Chat Skip Question ctrl-]
Chat Toggle Voice f8
Chat Toggle Voice Mute ctrl-x
Composer Submit enter
Composer Queue tab
Composer Toggle Shortcuts ?, shift-?
Composer History Search Previous ctrl-r
↓
left/right group · enter edit shortcut · * custom · - unbound · esc close
@@ -1,16 +0,0 @@
---
source: tui/src/model_migration.rs
expression: terminal.backend()
---
Codex just got an upgrade. Introducing
gpt-5.1-codex-max.
We recommend switching from gpt-5-codex to
gpt-5.1-codex-max.
Codex-optimized flagship for deep and fast reasoning.
Learn more about gpt-5.1-codex-max at
https://www.codex.com/models/gpt-5.1-codex-max
enter/esc continue · ⌃c quit
@@ -1,16 +0,0 @@
---
source: tui/src/model_migration.rs
expression: terminal.backend()
---
Codex just got an upgrade. Introducing
gpt-5.1-codex-mini.
We recommend switching from gpt-5-codex-mini to
gpt-5.1-codex-mini.
Optimized for codex. Cheaper, faster, but less capable.
Learn more about gpt-5.1-codex-mini at
https://www.codex.com/models/gpt-5.1-codex-mini
enter/esc continue · ⌃c quit
@@ -1,5 +0,0 @@
---
source: tui/src/resume_picker.rs
expression: "render_dense_row_snapshot(true, None, 120,)"
---
› 15m ago Propose session picker redesign with enough title text to exercise truncation
@@ -1,5 +0,0 @@
---
source: tui/src/resume_picker.rs
expression: "render_dense_row_snapshot(true, None, 100,)"
---
› 15m ago Propose session picker redesign with enough title text to exercise truncation
@@ -1,5 +0,0 @@
---
source: tui/src/resume_picker.rs
expression: "render_dense_row_snapshot(true, None, 48,)"
---
› 15m ago Propose session picker redesig...
@@ -1,10 +0,0 @@
---
source: tui/src/transcript_view_tests.rs
expression: text(&selected)
---
row 22
row 23
row 24
row 25
row 26
row 27
@@ -1,19 +0,0 @@
---
source: tui/src/status/tests.rs
expression: sanitized
---
/status
>_ OpenAI Codex (v0.0.0)
Visit https://chatgpt.com/settings/usage for up-to-date
information on rate limits and credits
Model: gpt-5.1-codex-max (reasoning none, summaries auto)
Directory: [[workspace]]
Permissions: Custom (workspace with network access, Ask for approval)
Agents.md: <none>
Token usage: 750 total (500 input + 250 output)
Context window: 100% left (750 used / 272K)
Limits: not available for this account
+1 -1
View File
@@ -2101,7 +2101,7 @@ async fn status_snapshot_treats_refreshing_empty_limits_as_unavailable() {
}
}
let sanitized = sanitize_directory(rendered_lines).join("\n");
assert_snapshot!(sanitized);
assert!(sanitized.contains("Limits: not available for this account"));
}
#[tokio::test]
+3 -4
View File
@@ -648,10 +648,9 @@ fn selecting_within_an_edge_row_does_not_start_selection_autoscroll() {
);
assert_eq!(view.selected_text(&cells).as_deref(), Some("row"));
}
insta::assert_snapshot!(
format!("horizontal_selection_at_edge_{edge}"),
text(&selected)
);
if edge == 0 {
insta::assert_snapshot!("horizontal_selection_at_edge_0", text(&selected));
}
let drag = MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 3,