diff --git a/codex-rs/tui/src/chatwidget/model_popups.rs b/codex-rs/tui/src/chatwidget/model_popups.rs index 1194fc15c6ab..b7d218c5bc45 100644 --- a/codex-rs/tui/src/chatwidget/model_popups.rs +++ b/codex-rs/tui/src/chatwidget/model_popups.rs @@ -5,6 +5,8 @@ use super::*; +const ULTRA_REASONING_CONCURRENCY_WARNING_THRESHOLD: usize = 8; + impl ChatWidget { /// Open a popup to choose a quick auto model. Selecting "All models" /// opens the full picker with every available preset. @@ -102,7 +104,7 @@ impl ChatWidget { model.as_str(), Some(preset.default_reasoning_effort.clone()), ); - let actions = Self::model_selection_actions( + let actions = self.model_selection_actions( model.clone(), Some(preset.default_reasoning_effort.clone()), should_prompt_plan_mode_scope, @@ -214,10 +216,14 @@ impl ChatWidget { } fn model_selection_actions( + &self, model_for_action: String, effort_for_action: Option, should_prompt_plan_mode_scope: bool, ) -> Vec { + let warning = effort_for_action + .as_ref() + .and_then(|effort| self.ultra_reasoning_concurrency_warning(effort)); vec![Box::new(move |tx| { if should_prompt_plan_mode_scope { tx.send(AppEvent::OpenPlanReasoningScopePrompt { @@ -233,6 +239,11 @@ impl ChatWidget { model: model_for_action.clone(), effort: effort_for_action.clone(), }); + if let Some(warning) = warning.clone() { + tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::new_warning_event(warning), + ))); + } })] } @@ -299,14 +310,23 @@ impl ChatWidget { "Set the global default reasoning level and the Plan mode override. This replaces the current {plan_reasoning_source}." ); let subtitle = format!("Choose where to apply {reasoning_phrase}."); + let warning = effort + .as_ref() + .and_then(|effort| self.ultra_reasoning_concurrency_warning(effort)); let plan_only_actions: Vec = vec![Box::new({ let model = model.clone(); let effort = effort.clone(); + let warning = warning.clone(); move |tx| { tx.send(AppEvent::UpdateModel(model.clone())); tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort.clone())); tx.send(AppEvent::PersistPlanModeReasoningEffort(effort.clone())); + if let Some(warning) = warning.clone() { + tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::new_warning_event(warning), + ))); + } } })]; let all_modes_actions: Vec = vec![Box::new(move |tx| { @@ -318,6 +338,11 @@ impl ChatWidget { model: model.clone(), effort: effort.clone(), }); + if let Some(warning) = warning.clone() { + tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::new_warning_event(warning), + ))); + } })]; self.bottom_pane.show_selection_view(SelectionViewParams { @@ -427,6 +452,7 @@ impl ChatWidget { let mut items: Vec = Vec::new(); for choice in choices.iter() { let effort = choice.clone(); + let warning = self.ultra_reasoning_concurrency_warning(&effort); let mut effort_label = Self::reasoning_effort_label(&effort); if Some(choice) == default_choice.as_ref() { effort_label.push_str(" (default)"); @@ -469,6 +495,11 @@ impl ChatWidget { model: model_for_action.clone(), effort: choice_effort.clone(), }); + if let Some(warning) = warning.clone() { + tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::new_warning_event(warning), + ))); + } } })]; @@ -518,14 +549,47 @@ impl ChatWidget { } } + pub(super) fn ultra_reasoning_concurrency_warning( + &self, + effort: &ReasoningEffortConfig, + ) -> Option { + if effort != &ReasoningEffortConfig::Ultra { + return None; + } + + let max_threads = self + .config + .multi_agent_v2 + .max_concurrent_threads_per_session; + if max_threads < ULTRA_REASONING_CONCURRENCY_WARNING_THRESHOLD { + return None; + } + + let max_subagents = max_threads.saturating_sub(1); + Some(format!( + "Ultra reasoning may proactively use multiple agents. This session is configured for \ + {max_threads} concurrent threads with up to {max_subagents} subagents which can \ + increase usage quickly. Consider setting \ + features.multi_agent_v2.max_concurrent_threads_per_session below 8." + )) + } + pub(super) fn apply_model_and_effort_without_persist( &self, model: String, effort: Option, ) { + let warning = effort + .as_ref() + .and_then(|effort| self.ultra_reasoning_concurrency_warning(effort)); self.app_event_tx.send(AppEvent::UpdateModel(model)); self.app_event_tx .send(AppEvent::UpdateReasoningEffort(effort)); + if let Some(warning) = warning { + self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::new_warning_event(warning), + ))); + } } fn apply_model_and_effort(&self, model: String, effort: Option) { diff --git a/codex-rs/tui/src/chatwidget/reasoning_shortcuts.rs b/codex-rs/tui/src/chatwidget/reasoning_shortcuts.rs index 0873bda6e003..b611281d0bcb 100644 --- a/codex-rs/tui/src/chatwidget/reasoning_shortcuts.rs +++ b/codex-rs/tui/src/chatwidget/reasoning_shortcuts.rs @@ -110,8 +110,14 @@ impl ChatWidget { }; if self.collaboration_modes_enabled() && self.active_mode_kind() == ModeKind::Plan { + let warning = self.ultra_reasoning_concurrency_warning(&next_effort); self.app_event_tx .send(AppEvent::UpdatePlanModeReasoningEffort(Some(next_effort))); + if let Some(warning) = warning { + self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new( + crate::history_cell::new_warning_event(warning), + ))); + } } else { self.apply_model_and_effort_without_persist(current_model, Some(next_effort)); } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__ultra_reasoning_selection_high_multi_agent_concurrency_warning.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__ultra_reasoning_selection_high_multi_agent_concurrency_warning.snap new file mode 100644 index 000000000000..56ebfb498b28 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__ultra_reasoning_selection_high_multi_agent_concurrency_warning.snap @@ -0,0 +1,8 @@ +--- +source: tui/src/chatwidget/tests/popups_and_settings.rs +expression: "&warnings[0]" +--- +⚠ Ultra reasoning may proactively use multiple agents. This session is + configured for 8 concurrent threads with up to 7 subagents which can increase + usage quickly. Consider setting + features.multi_agent_v2.max_concurrent_threads_per_session below 8. diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 07cfc7dbbbb2..1b460f81db11 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -3236,6 +3236,68 @@ async fn model_reasoning_selection_popup_applies_custom_effort() { ); } +async fn select_ultra_with_multi_agent_thread_limit(max_threads: usize) -> (bool, Vec) { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await; + chat.config + .multi_agent_v2 + .max_concurrent_threads_per_session = max_threads; + chat.set_reasoning_effort(Some(ReasoningEffortConfig::High)); + + let mut preset = get_available_model(&chat, "gpt-5.4"); + preset.default_reasoning_effort = ReasoningEffortConfig::High; + preset.supported_reasoning_efforts = vec![ + ReasoningEffortPreset { + effort: ReasoningEffortConfig::High, + description: "High reasoning".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffortConfig::Ultra, + description: "Ultra reasoning".to_string(), + }, + ]; + chat.open_reasoning_popup(preset); + while rx.try_recv().is_ok() {} + + chat.handle_key_event(KeyEvent::from(KeyCode::Down)); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + let mut selected_ultra = false; + let mut warnings = Vec::new(); + while let Ok(event) = rx.try_recv() { + match event { + AppEvent::UpdateReasoningEffort(Some(ReasoningEffortConfig::Ultra)) => { + selected_ultra = true; + } + AppEvent::InsertHistoryCell(cell) => { + warnings.push(lines_to_single_string(&cell.display_lines(/*width*/ 80))); + } + _ => {} + } + } + + (selected_ultra, warnings) +} + +#[tokio::test] +async fn ultra_reasoning_selection_warns_for_high_multi_agent_concurrency() { + let (selected_ultra, warnings) = + select_ultra_with_multi_agent_thread_limit(/*max_threads*/ 8).await; + + assert!(selected_ultra); + assert_eq!(warnings.len(), 1); + assert_chatwidget_snapshot!( + "ultra_reasoning_selection_high_multi_agent_concurrency_warning", + &warnings[0] + ); +} + +#[tokio::test] +async fn ultra_reasoning_selection_skips_warning_below_threshold() { + let below_threshold = select_ultra_with_multi_agent_thread_limit(/*max_threads*/ 7).await; + + assert_eq!(below_threshold, (true, Vec::new())); +} + #[tokio::test] async fn model_reasoning_selection_popup_extra_high_warning_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;