diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_pet.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_pet.snap index 65af8ebf28..3ac116bb46 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_pet.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_pet.snap @@ -6,4 +6,4 @@ expression: terminal.backend() "› /pet " " " " " -" /pets choose or disable the terminal pet " +" /pets choose or hide the terminal pet " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_running_h3.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_running_h3.snap index 4487d0652e..d258e6a450 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_running_h3.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_running_h3.snap @@ -1,7 +1,7 @@ --- -source: tui/src/chatwidget/tests.rs -expression: terminal.backend() +source: tui/src/chatwidget/tests/status_and_layout.rs +expression: normalized_backend_snapshot(terminal.backend()) --- -" " -" " +" Running " +" Thinking" " " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap index dfcfae199f..77e52fb63c 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap @@ -39,6 +39,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) • Investigating rendering code (0s • esc to interrupt) -› Summarize recent commits - +› Summarize recent commits Running + Thinking tab to queue message 100% context left diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap index 9642be5f12..44a143a0d0 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap @@ -23,6 +23,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) ↳ Hello, world! 14 ↳ Hello, world! 15 -› Ask Codex to do anything - +› Ask Codex to do anything Running + Thinking gpt-5.5 default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__compact_queues_user_messages_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__compact_queues_user_messages_snapshot.snap index 777439a1cd..1cf397d08c 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__compact_queues_user_messages_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__compact_queues_user_messages_snapshot.snap @@ -17,6 +17,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) • Messages to be submitted at end of turn ↳ Steer submitted while /compact was running. -› Ask Codex to do anything - +› Ask Codex to do anything Running + Thinking gpt-5.5 default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_parallel_reviews_render_aggregate_status.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_parallel_reviews_render_aggregate_status.snap index 653a48e949..04bc3e962e 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_parallel_reviews_render_aggregate_status.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_parallel_reviews_render_aggregate_status.snap @@ -7,6 +7,6 @@ expression: normalize_snapshot_paths(rendered) • rm -rf '/tmp/guardian target 2' -› Ask Codex to do anything - +› Ask Codex to do anything Running + Thinking gpt-5.5 default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__preamble_keeps_working_status.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__preamble_keeps_working_status.snap index 4748d8f678..1fd45a0a07 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__preamble_keeps_working_status.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__preamble_keeps_working_status.snap @@ -6,6 +6,6 @@ expression: normalized_backend_snapshot(terminal.backend()) "• Working (0s • esc to interrupt) " " " " " -"› Ask Codex to do anything " -" " +"› Ask Codex to do anything Running " +" Thinking" " gpt-5.5 default · /tmp/project " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap index d90ccd9c5f..ee519581df 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap @@ -17,6 +17,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) • Messages to be submitted at end of turn ↳ Steer submitted while /review was running. -› Ask Codex to do anything - +› Ask Codex to do anything Running + Thinking gpt-5.5 default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_side_requests_forked_side_question_while_task_running.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_side_requests_forked_side_question_while_task_running.snap index 47acb50d1f..62657532da 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_side_requests_forked_side_question_while_task_running.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_side_requests_forked_side_question_while_task_running.snap @@ -6,6 +6,6 @@ expression: normalized_backend_snapshot(terminal.backend()) "• Working (0s • esc to interrupt) " " " " " -"› Ask Codex to do anything " -" " +"› Ask Codex to do anything Running " +" Thinking" " gpt-5.5 default Side starting... " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap index c7bfc01487..fd4008facf 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap @@ -6,6 +6,6 @@ expression: normalized_backend_snapshot(terminal.backend()) "• Analyzing (0s • esc to interrupt) " " " " " -"› Ask Codex to do anything " -" " +"› Ask Codex to do anything Running " +" Thinking" " gpt-5.5 default · /tmp/project " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_begin_restores_working_status.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_begin_restores_working_status.snap index b589d02e3f..61773d4f77 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_begin_restores_working_status.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_begin_restores_working_status.snap @@ -6,6 +6,6 @@ expression: normalized_backend_snapshot(terminal.backend()) "• Working (0s • esc to interrupt) · 1 background terminal running · /ps to view…" " " " " -"› Ask Codex to do anything " -" " +"› Ask Codex to do anything Running " +" Thinking" " gpt-5.5 default · /tmp/project " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_status_renders_command_in_single_details_row.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_status_renders_command_in_single_details_row.snap index 67574c3729..d7436fa9d7 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_status_renders_command_in_single_details_row.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_status_renders_command_in_single_details_row.snap @@ -6,6 +6,6 @@ expression: normalize_snapshot_paths(rendered) └ cargo test -p codex-core -- --exact… -› Ask Codex to do anything - +› Ask Codex to do anything Running + Thinking gpt-5.5 default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 0af75ff4ab..1a22e5c203 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -260,6 +260,7 @@ pub(super) async fn make_chatwidget_manual( current_status: StatusIndicatorState::working(), active_hook_cell: None, ambient_pet, + pet_image_support_override: None, retry_status_header: None, pending_status_indicator_restore: false, suppress_queue_autosend: false, diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 0eb1dfb0b8..c56f92158a 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -2,47 +2,16 @@ use super::*; use pretty_assertions::assert_eq; use serial_test::serial; -struct EnvVarGuard { - key: &'static str, - previous: Option, +fn force_pet_image_support(chat: &mut ChatWidget) { + chat.set_pet_image_support_for_tests(crate::pets::PetImageSupport::Supported( + crate::pets::ImageProtocol::Kitty, + )); } -impl EnvVarGuard { - fn remove(key: &'static str) -> Self { - let previous = std::env::var_os(key); - unsafe { std::env::remove_var(key) }; - Self { key, previous } - } - - fn set(key: &'static str, value: &'static str) -> Self { - let previous = std::env::var_os(key); - unsafe { std::env::set_var(key, value) }; - Self { key, previous } - } -} - -impl Drop for EnvVarGuard { - fn drop(&mut self) { - match self.previous.take() { - Some(value) => unsafe { std::env::set_var(self.key, value) }, - None => unsafe { std::env::remove_var(self.key) }, - } - } -} - -fn supported_pet_image_env() -> [EnvVarGuard; 6] { - [ - EnvVarGuard::remove("TMUX"), - EnvVarGuard::remove("TMUX_PANE"), - EnvVarGuard::remove("ZELLIJ"), - EnvVarGuard::remove("ZELLIJ_SESSION_NAME"), - EnvVarGuard::remove("ZELLIJ_VERSION"), - EnvVarGuard::set("KITTY_WINDOW_ID", "test-window"), - ] -} - -fn tmux_pet_image_env() -> EnvVarGuard { - EnvVarGuard::set("TMUX", "session") +fn force_tmux_pet_image_unsupported(chat: &mut ChatWidget) { + chat.set_pet_image_support_for_tests(crate::pets::PetImageSupport::Unsupported( + crate::pets::PetImageUnsupportedReason::Tmux, + )); } fn complete_turn_with_message(chat: &mut ChatWidget, turn_id: &str, message: Option<&str>) { @@ -1793,8 +1762,8 @@ async fn slash_resume_with_arg_requests_named_session() { #[tokio::test] #[serial] async fn slash_pets_opens_picker() { - let _env_guard = supported_pet_image_env(); let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + force_pet_image_support(&mut chat); chat.dispatch_command(SlashCommand::Pets); @@ -1808,8 +1777,8 @@ async fn slash_pets_opens_picker() { #[tokio::test] #[serial] async fn slash_pets_with_arg_selects_named_pet() { - let _env_guard = supported_pet_image_env(); let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + force_pet_image_support(&mut chat); chat.bottom_pane .set_composer_text("/pets chefito".to_string(), Vec::new(), Vec::new()); @@ -1825,8 +1794,8 @@ async fn slash_pets_with_arg_selects_named_pet() { #[tokio::test] #[serial] async fn slash_pets_disable_disables_pets_even_on_unsupported_terminal() { - let _env_guard = tmux_pet_image_env(); 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()); @@ -1840,8 +1809,8 @@ async fn slash_pets_disable_disables_pets_even_on_unsupported_terminal() { #[tokio::test] #[serial] async fn slash_pet_hide_disables_pets_even_on_unsupported_terminal() { - let _env_guard = tmux_pet_image_env(); 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()); @@ -1855,8 +1824,8 @@ async fn slash_pet_hide_disables_pets_even_on_unsupported_terminal() { #[tokio::test] #[serial] async fn slash_pets_on_unsupported_terminal_warns_without_picker() { - let _env_guard = tmux_pet_image_env(); let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + force_tmux_pet_image_unsupported(&mut chat); chat.dispatch_command(SlashCommand::Pets); @@ -1874,8 +1843,8 @@ async fn slash_pets_on_unsupported_terminal_warns_without_picker() { #[tokio::test] #[serial] async fn slash_pets_with_arg_on_unsupported_terminal_warns_without_selection() { - let _env_guard = tmux_pet_image_env(); 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 chefito".to_string(), Vec::new(), Vec::new()); diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index f1fab2c0e0..6aaa8b7aae 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -3,43 +3,10 @@ use crate::bottom_pane::goal_status_indicator_line; use pretty_assertions::assert_eq; use serial_test::serial; -struct EnvVarGuard { - key: &'static str, - previous: Option, -} - -impl EnvVarGuard { - fn remove(key: &'static str) -> Self { - let previous = std::env::var_os(key); - unsafe { std::env::remove_var(key) }; - Self { key, previous } - } - - fn set(key: &'static str, value: &'static str) -> Self { - let previous = std::env::var_os(key); - unsafe { std::env::set_var(key, value) }; - Self { key, previous } - } -} - -impl Drop for EnvVarGuard { - fn drop(&mut self) { - match self.previous.take() { - Some(value) => unsafe { std::env::set_var(self.key, value) }, - None => unsafe { std::env::remove_var(self.key) }, - } - } -} - -fn supported_pet_image_env() -> [EnvVarGuard; 6] { - [ - EnvVarGuard::remove("TMUX"), - EnvVarGuard::remove("TMUX_PANE"), - EnvVarGuard::remove("ZELLIJ"), - EnvVarGuard::remove("ZELLIJ_SESSION_NAME"), - EnvVarGuard::remove("ZELLIJ_VERSION"), - EnvVarGuard::set("KITTY_WINDOW_ID", "test-window"), - ] +fn force_pet_image_support(chat: &mut ChatWidget) { + chat.set_pet_image_support_for_tests(crate::pets::PetImageSupport::Supported( + crate::pets::ImageProtocol::Kitty, + )); } /// Receiving a token usage update without usage clears the context indicator. @@ -1239,11 +1206,11 @@ async fn ui_snapshots_small_heights_task_running() { #[tokio::test] #[serial] -async fn ambient_pet_defaults_to_codex_and_stays_above_the_footer() { +async fn ambient_pet_defaults_to_codex_and_anchors_to_composer_bottom() { use ratatui::layout::Rect; - let _env_guard = supported_pet_image_env(); - let (chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + force_pet_image_support(&mut chat); assert_eq!( chat.ambient_pet .as_ref() @@ -1255,18 +1222,57 @@ async fn ambient_pet_defaults_to_codex_and_stays_above_the_footer() { /*x*/ 0, /*y*/ 0, /*width*/ 60, /*height*/ 20, ); let draw = chat - .ambient_pet_draw(area) + .ambient_pet_draw(area, area.bottom()) .expect("ambient pet draw request"); assert_eq!(draw.x, 51); - assert_eq!(draw.y, 10); + assert_eq!(draw.y, 14); assert_eq!(draw.columns, 9); assert_eq!(draw.rows, 5); + assert_eq!( + draw.y.saturating_add(draw.rows), + area.bottom().saturating_sub(/*rhs*/ 1) + ); + + handle_turn_started(&mut chat, "turn-1"); + handle_agent_reasoning_delta(&mut chat, "**Thinking**"); + let draw_with_status = chat + .ambient_pet_draw(area, area.bottom()) + .expect("ambient pet draw request with status"); + assert_eq!(draw_with_status.y, draw.y); + assert_eq!( + draw_with_status.y.saturating_add(draw_with_status.rows), + area.bottom().saturating_sub(/*rhs*/ 1) + ); +} + +#[tokio::test] +#[serial] +async fn ambient_pet_screen_bottom_anchor_uses_terminal_bottom() { + use codex_config::types::TuiPetAnchor; + use ratatui::layout::Rect; + + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + force_pet_image_support(&mut chat); + + let terminal_area = Rect::new( + /*x*/ 0, /*y*/ 0, /*width*/ 80, /*height*/ 24, + ); + let composer_bottom_y = 20; + let default_draw = chat + .ambient_pet_draw(terminal_area, composer_bottom_y) + .expect("composer-anchored pet draw request"); + assert_eq!(default_draw.y, 14); + + chat.config.tui_pet_anchor = TuiPetAnchor::ScreenBottom; + let screen_bottom_draw = chat + .ambient_pet_draw(terminal_area, composer_bottom_y) + .expect("screen-bottom anchored pet draw request"); + assert_eq!(screen_bottom_draw.y, 18); } #[tokio::test] #[serial] async fn ambient_pet_can_be_disabled() { - let _env_guard = supported_pet_image_env(); let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.set_tui_pet(Some(crate::pets::DISABLED_PET_ID.to_string())); @@ -1279,24 +1285,30 @@ async fn ambient_pet_can_be_disabled() { async fn ambient_pet_draw_uses_terminal_screen_area_not_short_inline_viewport() { use ratatui::layout::Rect; - let _env_guard = supported_pet_image_env(); - let (chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + force_pet_image_support(&mut chat); assert!( - chat.ambient_pet_draw(Rect::new( - /*x*/ 0, /*y*/ 21, /*width*/ 80, /*height*/ 3, - )) + chat.ambient_pet_draw( + Rect::new( + /*x*/ 0, /*y*/ 21, /*width*/ 80, /*height*/ 3, + ), + /*composer_bottom_y*/ 24 + ) .is_none(), "a normal short inline viewport cannot fit the ambient pet" ); let draw = chat - .ambient_pet_draw(Rect::new( - /*x*/ 0, /*y*/ 0, /*width*/ 80, /*height*/ 24, - )) + .ambient_pet_draw( + Rect::new( + /*x*/ 0, /*y*/ 0, /*width*/ 80, /*height*/ 24, + ), + /*composer_bottom_y*/ 24, + ) .expect("full terminal screen has room for the ambient pet"); assert_eq!(draw.x, 71); - assert_eq!(draw.y, 14); + assert_eq!(draw.y, 18); } #[tokio::test] @@ -1305,8 +1317,8 @@ async fn ambient_pet_uses_the_app_notification_labels() { use ratatui::Terminal; use ratatui::backend::TestBackend; - let _env_guard = supported_pet_image_env(); let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + force_pet_image_support(&mut chat); for (kind, label) in [ (crate::pets::PetNotificationKind::Running, "Running"), (crate::pets::PetNotificationKind::Waiting, "Needs input"),