diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 4e94ec6819..336fe3fbe7 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -770,6 +770,16 @@ impl App { tracing::error!(error = ?err, "failed to start turn through app server"); } } + AppEvent::ConfirmSafetyBufferedRetry { + thread_id, + turn_id, + model, + turn, + prompt, + } => { + self.chat_widget + .confirm_safety_buffered_retry(thread_id, turn_id, model, turn, prompt); + } AppEvent::RetrySafetyBufferedTurn { thread_id, turn_id, diff --git a/codex-rs/tui/src/app/safety_buffering.rs b/codex-rs/tui/src/app/safety_buffering.rs index 0325f185db..dee5108352 100644 --- a/codex-rs/tui/src/app/safety_buffering.rs +++ b/codex-rs/tui/src/app/safety_buffering.rs @@ -41,10 +41,6 @@ impl App { return; } if !self.chat_widget.can_retry_safety_buffered_turn(&turn_id) { - self.app_event_tx.send(AppEvent::UpdateModel(model)); - self.app_event_tx.send(AppEvent::UpdateReasoningEffort(Some( - ReasoningEffortConfig::Low, - ))); return; } diff --git a/codex-rs/tui/src/app/tests/safety_buffering.rs b/codex-rs/tui/src/app/tests/safety_buffering.rs index 3d5950b10e..9735915ee7 100644 --- a/codex-rs/tui/src/app/tests/safety_buffering.rs +++ b/codex-rs/tui/src/app/tests/safety_buffering.rs @@ -714,6 +714,13 @@ goals = true .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); let second_retry = loop { match app_event_rx.try_recv() { + Ok(event @ AppEvent::ConfirmSafetyBufferedRetry { .. }) => { + Box::pin(app.handle_event(&mut tui, &mut app_server, event)).await?; + app.chat_widget + .handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + app.chat_widget + .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + } Ok(AppEvent::RetrySafetyBufferedTurn { thread_id, turn_id, diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 89270a947a..cea9c29326 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -300,6 +300,15 @@ pub(crate) enum AppEvent { op: AppCommand, }, + /// Confirm retrying a safety-buffered turn with the server-selected model. + ConfirmSafetyBufferedRetry { + thread_id: ThreadId, + turn_id: String, + model: String, + turn: AppCommand, + prompt: UserMessage, + }, + /// Interrupt, fork, and retry a safety-buffered turn with the server-selected model. RetrySafetyBufferedTurn { thread_id: ThreadId, diff --git a/codex-rs/tui/src/chatwidget/safety_buffering.rs b/codex-rs/tui/src/chatwidget/safety_buffering.rs index be65e5ee58..b158568520 100644 --- a/codex-rs/tui/src/chatwidget/safety_buffering.rs +++ b/codex-rs/tui/src/chatwidget/safety_buffering.rs @@ -1,16 +1,33 @@ -//! Safety-buffering status and retry UI for active turns. +//! Safety-buffering status and retry confirmation UI for active turns. +//! Both views share an ID so response streaming, completion, or buffering updates dismiss either one. use super::*; +use crate::wrapping::word_wrap_lines; use codex_app_server_protocol::ModelSafetyBufferingUpdatedNotification; const SAFETY_BUFFERING_PROMPT_VIEW_ID: &str = "safety-buffering-prompt"; const SAFETY_BUFFERING_LEARN_MORE_URL: &str = "https://help.openai.com/en/articles/20001326"; -const SAFETY_BUFFERING_HEADER: &str = - "Our systems are thinking a bit more about this request before responding."; -const SAFETY_BUFFERING_MESSAGE_WITH_RETRY: &str = "Hang tight or retry with a faster model for a quicker response, though it may be less capable of handling complex requests."; +const SAFETY_BUFFERING_HEADER: &str = "Giving this request a little extra thought"; +const SAFETY_BUFFERING_MESSAGE_WITH_RETRY: &str = "If you'd rather not wait, retry with a faster model. It may be less capable of handling complex requests."; const SAFETY_BUFFERING_FOOTER: &str = "No action is required. Codex will keep waiting, and this menu will close when the response is ready."; +struct SafetyBufferingHeader(Vec>); + +impl Renderable for SafetyBufferingHeader { + fn render(&self, area: Rect, buf: &mut Buffer) { + Renderable::render( + &Paragraph::new(word_wrap_lines(&self.0, usize::from(area.width))), + area, + buf, + ); + } + + fn desired_height(&self, width: u16) -> u16 { + word_wrap_lines(&self.0, usize::from(width)).len() as u16 + } +} + #[derive(Debug)] struct ActiveSafetyBuffering { turn_id: String, @@ -42,8 +59,13 @@ impl ChatWidget { } pub(super) fn mark_safety_buffering_agent_message_started(&mut self) { - if let Some(active) = self.safety_buffering.active.as_mut() { + if let Some(active) = self.safety_buffering.active.as_mut() + && !active.agent_message_started + { active.agent_message_started = true; + self.bottom_pane + .dismiss_view_by_id(SAFETY_BUFFERING_PROMPT_VIEW_ID); + self.restore_reasoning_status_header(); } } @@ -93,6 +115,11 @@ impl ChatWidget { if matches!(replay_kind, Some(ReplayKind::ResumeInitialMessages)) || !self.turn_lifecycle.agent_turn_running || self.turn_lifecycle.last_turn_id.as_deref() != Some(turn_id.as_str()) + || self + .safety_buffering + .active + .as_ref() + .is_some_and(|active| active.turn_id == turn_id && active.agent_message_started) { return; } @@ -135,12 +162,10 @@ impl ChatWidget { .filter(|active| active.turn_id == turn_id); let should_show_prompt = previous_active.is_none_or(|active| active.last_prompt_had_retry != can_offer_retry); - let agent_message_started = - previous_active.is_some_and(|active| active.agent_message_started); self.safety_buffering.active = Some(ActiveSafetyBuffering { turn_id: turn_id.clone(), last_prompt_had_retry: can_offer_retry, - agent_message_started, + agent_message_started: false, }); let status_details = if can_offer_retry { @@ -162,16 +187,10 @@ impl ChatWidget { self.bottom_pane .dismiss_view_by_id(SAFETY_BUFFERING_PROMPT_VIEW_ID); - let mut header = vec![Box::new( - Paragraph::new(Line::from(SAFETY_BUFFERING_HEADER).bold()).wrap(Wrap { trim: false }), - ) as Box]; + let mut header = vec![Line::from(SAFETY_BUFFERING_HEADER).bold()]; if can_offer_retry { - header.push(Box::new( - Paragraph::new(Line::from(SAFETY_BUFFERING_MESSAGE_WITH_RETRY).dim()) - .wrap(Wrap { trim: false }), - )); + header.push(Line::from(SAFETY_BUFFERING_MESSAGE_WITH_RETRY).dim()); } - let header = ColumnRenderable::with(header); let mut items = Vec::new(); if let (Some(faster_model), Some(turn), Some(prompt), Some(thread_id)) = (faster_model, retry_turn, retry_prompt, thread_id) @@ -179,7 +198,7 @@ impl ChatWidget { items.push(SelectionItem { name: "Retry with a faster model".to_string(), actions: vec![Box::new(move |tx| { - tx.send(AppEvent::RetrySafetyBufferedTurn { + tx.send(AppEvent::ConfirmSafetyBufferedRetry { thread_id, turn_id: turn_id.clone(), model: faster_model.clone(), @@ -209,11 +228,68 @@ impl ChatWidget { ]); self.bottom_pane.show_selection_view(SelectionViewParams { view_id: Some(SAFETY_BUFFERING_PROMPT_VIEW_ID), - header: Box::new(header), + header: Box::new(SafetyBufferingHeader(header)), footer_note: Some(Line::from(SAFETY_BUFFERING_FOOTER).dim()), footer_hint: Some(Line::default()), items, ..Default::default() }); } + + pub(crate) fn confirm_safety_buffered_retry( + &mut self, + thread_id: ThreadId, + turn_id: String, + model: String, + turn: AppCommand, + prompt: UserMessage, + ) { + if self.thread_id != Some(thread_id) || !self.can_retry_safety_buffered_turn(&turn_id) { + return; + } + + let model_name = self + .model_catalog + .try_list_models() + .ok() + .and_then(|models| models.into_iter().find(|preset| preset.model == model)) + .map(|preset| preset.display_name) + .unwrap_or_else(|| model.clone()); + self.bottom_pane + .dismiss_view_by_id(SAFETY_BUFFERING_PROMPT_VIEW_ID); + self.bottom_pane.show_selection_view(SelectionViewParams { + view_id: Some(SAFETY_BUFFERING_PROMPT_VIEW_ID), + header: Box::new(SafetyBufferingHeader(vec![ + "Stop this attempt and retry?".bold().into(), + Line::default(), + "This will stop the current attempt and retry in a new thread. Any file changes or other actions already taken will remain.".dim().into(), + Line::default(), + format!("Your message will be sent again using {model_name}, which may be less capable on complex tasks.").dim().into(), + ])), + footer_hint: Some(Line::default()), + items: vec![ + SelectionItem { + name: "Keep waiting".to_string(), + dismiss_on_select: true, + ..Default::default() + }, + SelectionItem { + name: "Stop and retry".to_string(), + actions: vec![Box::new(move |tx| { + tx.send(AppEvent::RetrySafetyBufferedTurn { + thread_id, + turn_id: turn_id.clone(), + model: model.clone(), + turn: turn.clone(), + prompt: prompt.clone(), + }); + })], + dismiss_on_select: true, + require_explicit_confirmation: true, + ..Default::default() + }, + ], + ..Default::default() + }); + } } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation.snap new file mode 100644 index 0000000000..70110fcbd8 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation.snap @@ -0,0 +1,14 @@ +--- +source: tui/src/chatwidget/tests/app_server.rs +expression: "render_bottom_popup(&chat, 80)" +--- + Stop this attempt and retry? + + This will stop the current attempt and retry in a new thread. Any file + changes or other actions already taken will remain. + + Your message will be sent again using Faster Model, which may be less + capable on complex tasks. + +› 1. Keep waiting + 2. Stop and retry diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation_narrow.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation_narrow.snap new file mode 100644 index 0000000000..55b6541c77 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_confirmation_narrow.snap @@ -0,0 +1,17 @@ +--- +source: tui/src/chatwidget/tests/app_server.rs +expression: "render_bottom_popup(&chat, 40)" +--- + Stop this attempt and retry? + + This will stop the current attempt + and retry in a new thread. Any file + changes or other actions already + taken will remain. + + Your message will be sent again + using Faster Model, which may be + less capable on complex tasks. + +› 1. Keep waiting + 2. Stop and retry diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_prompt.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_prompt.snap index 767c9c393b..54c6ee4a9f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_prompt.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_retry_prompt.snap @@ -2,9 +2,9 @@ source: tui/src/chatwidget/tests/app_server.rs expression: popup --- - Our systems are thinking a bit more about this request before responding. - Hang tight or retry with a faster model for a quicker response, though it - may be less capable of handling complex requests. + Giving this request a little extra thought + If you'd rather not wait, retry with a faster model. It may be less capable + of handling complex requests. › 1. Retry with a faster model 2. Dismiss and keep waiting diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_side_conversation_without_retry.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_side_conversation_without_retry.snap index 56efb00764..54263e2752 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_side_conversation_without_retry.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_side_conversation_without_retry.snap @@ -2,7 +2,7 @@ source: tui/src/chatwidget/tests/app_server.rs expression: popup --- - Our systems are thinking a bit more about this request before responding. + Giving this request a little extra thought › 1. Dismiss and keep waiting 2. Learn more diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_status_without_retry.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_status_without_retry.snap index 56efb00764..54263e2752 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_status_without_retry.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__safety_buffering_status_without_retry.snap @@ -2,7 +2,7 @@ source: tui/src/chatwidget/tests/app_server.rs expression: popup --- - Our systems are thinking a bit more about this request before responding. + Giving this request a little extra thought › 1. Dismiss and keep waiting 2. Learn more diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index f983ceb674..a690b7750a 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -4,8 +4,7 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::CodexErrorDetails; use pretty_assertions::assert_eq; -const SAFETY_BUFFERING_HEADER_TEXT: &str = - "Our systems are thinking a bit more about this request before responding."; +const SAFETY_BUFFERING_HEADER_TEXT: &str = "Giving this request a little extra thought"; fn thread_settings_for_test( model: &str, @@ -113,9 +112,41 @@ fn safety_buffering_notification( } } +fn open_safety_buffering_retry_confirmation( + chat: &mut ChatWidget, + rx: &mut tokio::sync::mpsc::UnboundedReceiver, +) { + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + loop { + match rx.try_recv() { + Ok(AppEvent::ConfirmSafetyBufferedRetry { + thread_id, + turn_id, + model, + turn, + prompt, + }) => { + chat.confirm_safety_buffered_retry(thread_id, turn_id, model, turn, prompt); + break; + } + Ok(AppEvent::RetrySafetyBufferedTurn { .. }) => { + panic!("retry must wait for confirmation"); + } + Ok(_) => continue, + Err(err) => panic!("expected safety-buffering confirmation event: {err}"), + } + } + assert!(chat.turn_lifecycle.agent_turn_running); + assert!(render_bottom_popup(chat, /*width*/ 80).contains("Stop this attempt and retry?")); +} + #[tokio::test] async fn safety_buffering_offers_one_retry_with_app_wording() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + let mut preset = get_available_model(&chat, "gpt-5.4"); + preset.model = "faster-model".to_string(); + preset.display_name = "Faster Model".to_string(); + chat.model_catalog = Arc::new(ModelCatalog::new(vec![preset])); let (thread_id, turn_id, _) = start_safety_buffering_test_turn(&mut chat, &mut op_rx); let notification = safety_buffering_notification(thread_id, turn_id, Some("faster-model")); @@ -146,6 +177,21 @@ async fn safety_buffering_offers_one_retry_with_app_wording() { chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); + open_safety_buffering_retry_confirmation(&mut chat, &mut rx); + assert_chatwidget_snapshot!( + "safety_buffering_retry_confirmation", + render_bottom_popup(&chat, /*width*/ 80) + ); + assert_chatwidget_snapshot!( + "safety_buffering_retry_confirmation_narrow", + render_bottom_popup(&chat, /*width*/ 40) + ); + assert!(op_rx.try_recv().is_err()); + while let Ok(event) = rx.try_recv() { + assert!(!matches!(event, AppEvent::RetrySafetyBufferedTurn { .. })); + } + + chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); let (event_thread_id, event_turn_id, model, turn, prompt) = loop { match rx.try_recv() { @@ -171,6 +217,60 @@ async fn safety_buffering_offers_one_retry_with_app_wording() { ); } +#[tokio::test] +async fn safety_buffering_retry_confirmation_can_keep_waiting() { + for key in [KeyCode::Enter, KeyCode::Esc] { + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + let (thread_id, turn_id, _) = start_safety_buffering_test_turn(&mut chat, &mut op_rx); + chat.handle_server_notification( + ServerNotification::ModelSafetyBufferingUpdated(safety_buffering_notification( + thread_id, + turn_id, + Some("faster-model"), + )), + /*replay_kind*/ None, + ); + open_safety_buffering_retry_confirmation(&mut chat, &mut rx); + + chat.handle_key_event(KeyEvent::new(key, KeyModifiers::NONE)); + + assert!(!render_bottom_popup(&chat, /*width*/ 80).contains("Stop this attempt and retry?")); + assert!(chat.can_retry_safety_buffered_turn(turn_id)); + assert!(op_rx.try_recv().is_err()); + while let Ok(event) = rx.try_recv() { + assert!(!matches!(event, AppEvent::RetrySafetyBufferedTurn { .. })); + } + } +} + +#[tokio::test] +async fn safety_buffering_retry_confirmation_closes_when_turn_completes() { + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + let (thread_id, turn_id, turn) = start_safety_buffering_test_turn(&mut chat, &mut op_rx); + chat.handle_server_notification( + ServerNotification::ModelSafetyBufferingUpdated(safety_buffering_notification( + thread_id, + turn_id, + Some("faster-model"), + )), + /*replay_kind*/ None, + ); + open_safety_buffering_retry_confirmation(&mut chat, &mut rx); + + handle_turn_completed(&mut chat, turn_id, /*duration_ms*/ None); + // A queued request to open the confirmation must not reopen it after completion. + chat.confirm_safety_buffered_retry( + thread_id, + turn_id.to_string(), + "faster-model".to_string(), + turn, + UserMessage::from("Explain the request"), + ); + + assert!(!render_bottom_popup(&chat, /*width*/ 80).contains("Stop this attempt and retry?")); + assert!(!chat.can_retry_safety_buffered_turn(turn_id)); +} + #[tokio::test] async fn safety_buffering_does_not_offer_retry_in_side_conversation() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -191,8 +291,8 @@ async fn safety_buffering_does_not_offer_retry_in_side_conversation() { } #[tokio::test] -async fn safety_buffering_remains_visible_until_turn_completes() { - let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; +async fn safety_buffering_retry_confirmation_closes_when_response_starts() { + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let (thread_id, turn_id, _) = start_safety_buffering_test_turn(&mut chat, &mut op_rx); chat.handle_server_notification( ServerNotification::ModelSafetyBufferingUpdated(safety_buffering_notification( @@ -203,11 +303,30 @@ async fn safety_buffering_remains_visible_until_turn_completes() { /*replay_kind*/ None, ); assert!(chat.can_retry_safety_buffered_turn(turn_id)); + open_safety_buffering_retry_confirmation(&mut chat, &mut rx); chat.on_agent_message_delta("Visible response".to_string()); assert!(!chat.can_retry_safety_buffered_turn(turn_id)); - assert!(render_bottom_popup(&chat, /*width*/ 80).contains(SAFETY_BUFFERING_HEADER_TEXT)); + let popup = render_bottom_popup(&chat, /*width*/ 80); + assert!(!popup.contains("Stop this attempt and retry?")); + assert!(!popup.contains(SAFETY_BUFFERING_HEADER_TEXT)); + + for (show_buffering_ui, faster_model) in [ + (true, Some("faster-model")), + (true, None), + (false, None), + (true, Some("faster-model")), + ] { + let mut notification = safety_buffering_notification(thread_id, turn_id, faster_model); + notification.show_buffering_ui = show_buffering_ui; + chat.handle_server_notification( + ServerNotification::ModelSafetyBufferingUpdated(notification), + /*replay_kind*/ None, + ); + assert!(!chat.can_retry_safety_buffered_turn(turn_id)); + assert_eq!(render_bottom_popup(&chat, /*width*/ 80), popup); + } handle_turn_completed(&mut chat, turn_id, /*duration_ms*/ None); @@ -256,7 +375,7 @@ async fn safety_buffering_without_retry_shows_short_app_message() { #[tokio::test] async fn safety_buffering_ignores_hidden_stale_and_historical_updates() { - let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let (thread_id, turn_id, _) = start_safety_buffering_test_turn(&mut chat, &mut op_rx); let mut hidden = safety_buffering_notification(thread_id, turn_id, Some("faster-model")); @@ -289,6 +408,7 @@ async fn safety_buffering_ignores_hidden_stale_and_historical_updates() { /*replay_kind*/ None, ); assert!(render_bottom_popup(&chat, /*width*/ 80).contains(SAFETY_BUFFERING_HEADER_TEXT)); + open_safety_buffering_retry_confirmation(&mut chat, &mut rx); hidden.show_buffering_ui = false; chat.handle_server_notification( ServerNotification::ModelSafetyBufferingUpdated(hidden), @@ -303,6 +423,7 @@ async fn safety_buffering_ignores_hidden_stale_and_historical_updates() { None ); assert!(!render_bottom_popup(&chat, /*width*/ 80).contains(SAFETY_BUFFERING_HEADER_TEXT)); + assert!(!render_bottom_popup(&chat, /*width*/ 80).contains("Stop this attempt and retry?")); } #[tokio::test] diff --git a/codex-rs/tui/src/chatwidget/tests/misalignment_policy_tests.rs b/codex-rs/tui/src/chatwidget/tests/misalignment_policy_tests.rs index 396bdc76bc..ad942cc668 100644 --- a/codex-rs/tui/src/chatwidget/tests/misalignment_policy_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/misalignment_policy_tests.rs @@ -21,7 +21,10 @@ async fn misalignment_policy_failure_stops_the_thread_and_renders_once() { }), /*replay_kind*/ None, ); - assert!(render_bottom_popup(&chat, /*width*/ 80).contains("thinking a bit more")); + assert!( + render_bottom_popup(&chat, /*width*/ 80) + .contains("Giving this request a little extra thought") + ); chat.queue_user_message(UserMessage::from("queued follow-up")); chat.bottom_pane .set_composer_text("stale draft".to_string(), Vec::new(), Vec::new());