diff --git a/codex-rs/tui/src/chatwidget/safety_buffering.rs b/codex-rs/tui/src/chatwidget/safety_buffering.rs index aec589bdf078..649432f21c9d 100644 --- a/codex-rs/tui/src/chatwidget/safety_buffering.rs +++ b/codex-rs/tui/src/chatwidget/safety_buffering.rs @@ -4,6 +4,7 @@ use super::*; 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_MESSAGE_WITH_RETRY: &str = "This request requires additional safety checks, which can take extra time. 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_MESSAGE_WITHOUT_RETRY: &str = @@ -12,7 +13,7 @@ const SAFETY_BUFFERING_MESSAGE_WITHOUT_RETRY: &str = #[derive(Debug)] struct ActiveSafetyBuffering { turn_id: String, - retry_prompt_shown: bool, + last_prompt_had_retry: bool, agent_message_started: bool, } @@ -119,22 +120,18 @@ impl ChatWidget { .map(|(_, turn)| turn.clone()); let thread_id = self.thread_id; let can_offer_retry = faster_model.is_some() && retry_turn.is_some() && thread_id.is_some(); - if !can_offer_retry { - self.bottom_pane - .dismiss_view_by_id(SAFETY_BUFFERING_PROMPT_VIEW_ID); - } let previous_active = self .safety_buffering .active .as_ref() .filter(|active| active.turn_id == turn_id); - let retry_prompt_shown = previous_active.is_some_and(|active| active.retry_prompt_shown); - let should_show_retry_prompt = can_offer_retry && !retry_prompt_shown; + 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(), - retry_prompt_shown: retry_prompt_shown || should_show_retry_prompt, + last_prompt_had_retry: can_offer_retry, agent_message_started, }); @@ -151,45 +148,54 @@ impl ChatWidget { /*details_max_lines*/ 6, ); - let (Some(faster_model), Some(turn), Some(thread_id)) = - (faster_model, retry_turn, thread_id) - else { - return; - }; - if !should_show_retry_prompt { + if !should_show_prompt { return; } + self.bottom_pane + .dismiss_view_by_id(SAFETY_BUFFERING_PROMPT_VIEW_ID); let header = ColumnRenderable::with(vec![ Box::new(Line::from("Additional safety checks").bold()) as Box, - Box::new( - Paragraph::new(Line::from(SAFETY_BUFFERING_MESSAGE_WITH_RETRY).dim()) - .wrap(Wrap { trim: false }), - ), + Box::new(Paragraph::new(Line::from(message).dim()).wrap(Wrap { trim: false })), + ]); + let mut items = Vec::new(); + if let (Some(faster_model), Some(turn), Some(thread_id)) = + (faster_model, retry_turn, thread_id) + { + items.push(SelectionItem { + name: "Retry with a faster model".to_string(), + actions: vec![Box::new(move |tx| { + tx.send(AppEvent::RetrySafetyBufferedTurn { + thread_id, + turn_id: turn_id.clone(), + model: faster_model.clone(), + turn: turn.clone(), + }); + })], + dismiss_on_select: true, + ..Default::default() + }); + } + items.extend([ + SelectionItem { + name: "Keep waiting".to_string(), + dismiss_on_select: true, + ..Default::default() + }, + SelectionItem { + name: "Learn more".to_string(), + actions: vec![Box::new(|tx| { + tx.send(AppEvent::OpenUrlInBrowser { + url: SAFETY_BUFFERING_LEARN_MORE_URL.to_string(), + }); + })], + ..Default::default() + }, ]); self.bottom_pane.show_selection_view(SelectionViewParams { view_id: Some(SAFETY_BUFFERING_PROMPT_VIEW_ID), header: Box::new(header), - items: vec![ - SelectionItem { - name: "Retry with a faster model".to_string(), - actions: vec![Box::new(move |tx| { - tx.send(AppEvent::RetrySafetyBufferedTurn { - thread_id, - turn_id: turn_id.clone(), - model: faster_model.clone(), - turn: turn.clone(), - }); - })], - dismiss_on_select: true, - ..Default::default() - }, - SelectionItem { - name: "Keep waiting".to_string(), - dismiss_on_select: true, - ..Default::default() - }, - ], + items, ..Default::default() }); } 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 4498a472f5e8..6517d733363c 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 @@ -9,5 +9,6 @@ expression: popup › 1. Retry with a faster model 2. Keep waiting + 3. Learn more Press enter to confirm or esc to go back 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 a719f937cd65..f3645db4fcf4 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 @@ -1,11 +1,11 @@ --- source: tui/src/chatwidget/tests/app_server.rs -expression: "render_bottom_popup(&chat, 80)" +expression: popup --- -• Working (0s • esc to interrupt) - └ This request requires additional safety checks, which can take extra time. + Additional safety checks + This request requires additional safety checks, which can take extra time. +› 1. Keep waiting + 2. Learn more -› Ask Codex to do anything - - gpt-5.5 default · /tmp/project + Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index 2753d5c2da31..952aff9f9518 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -125,6 +125,21 @@ async fn safety_buffering_offers_one_retry_with_app_wording() { let popup = render_bottom_popup(&chat, /*width*/ 80); assert_chatwidget_snapshot!("safety_buffering_retry_prompt", popup); + chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + let opened_url = loop { + match rx.try_recv() { + Ok(AppEvent::OpenUrlInBrowser { url }) => break url, + Ok(_) => continue, + Err(err) => panic!("expected learn-more URL event: {err}"), + } + }; + assert_eq!(opened_url, "https://help.openai.com/en/articles/20001326"); + assert!(render_bottom_popup(&chat, /*width*/ 80).contains("Additional safety checks")); + + chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); + chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); let (event_thread_id, event_turn_id, model, turn) = loop { match rx.try_recv() { @@ -201,6 +216,9 @@ async fn safety_buffering_without_retry_shows_short_app_message() { Some(ReplayKind::ThreadSnapshot), ); assert_eq!(render_popup(&chat), popup); + + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + assert!(!render_bottom_popup(&chat, /*width*/ 80).contains("Additional safety checks")); } #[tokio::test] diff --git a/codex-rs/tui/src/history_cell/notices.rs b/codex-rs/tui/src/history_cell/notices.rs index c2fa6c921ba3..7462160d8fa4 100644 --- a/codex-rs/tui/src/history_cell/notices.rs +++ b/codex-rs/tui/src/history_cell/notices.rs @@ -96,8 +96,8 @@ const SAFETY_ACCESS_BLOCK_LEARN_MORE_URL: &str = "https://help.openai.com/en/art pub(crate) fn new_safety_access_block_event() -> SafetyAccessBlockCell { SafetyAccessBlockCell { - body: "We take extra caution with requests involving biological research and applications that could pose safety risks. If you’re a researcher at an approved organization, you may be able to apply for Trusted Access.", - trusted_access_url: "https://openai.com/form/trusted-access-for-life-sciences", + body: "We take extra caution with requests involving biological research and applications that could pose safety risks. Eligible researchers can apply for Trusted Access. If you’re a researcher at an approved organization, you may be able to apply for Trusted Access.", + trusted_access_url: "https://www.openai.com/form/trusted-access-for-biology-research/", } } diff --git a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__safety_access_block_event_snapshot.snap b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__safety_access_block_event_snapshot.snap index fe027f0d9d27..b3ff6dc3e70f 100644 --- a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__safety_access_block_event_snapshot.snap +++ b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__safety_access_block_event_snapshot.snap @@ -4,7 +4,9 @@ expression: rendered --- ⓘ This content can't be shown We take extra caution with requests involving biological research and - applications that could pose safety risks. If you’re a researcher at an - approved organization, you may be able to apply for Trusted Access. - Trusted Access: https://openai.com/form/trusted-access-for-life-sciences + applications that could pose safety risks. Eligible researchers can apply + for Trusted Access. If you’re a researcher at an approved organization, you + may be able to apply for Trusted Access. + Trusted Access: https://www.openai.com/form/trusted-access-for-biology- + research/ Learn more: https://help.openai.com/en/articles/20001326