tui: warn on Ultra with high multi-agent concurrency (#31621)

## Why

Ultra reasoning may proactively use multiple agents. When
`features.multi_agent_v2.max_concurrent_threads_per_session` is
configured at 8 or higher, explicitly selecting Ultra can allow up to `N
- 1` subagents to work concurrently and increase usage quickly. Showing
the configured limits at selection time makes that tradeoff visible and
points users to the setting that controls it.

## What changed

- Show a warning history cell after the user explicitly selects Ultra
reasoning with a concurrent-thread limit of at least 8.
- Include the configured concurrent-thread count and maximum subagent
count in the warning.
- Apply the warning consistently across model and reasoning pickers,
Plan-mode scope selection, and reasoning shortcuts.
- Keep the trigger limited to the selected reasoning effort and
configured thread limit, independent of how multi-agent v2 is activated.
- Add focused threshold coverage and an `insta` snapshot for the
rendered warning.

## User impact

Users with high multi-agent concurrency receive a concrete warning
immediately after selecting Ultra. Other reasoning efforts, limits below
8, and startup behavior are unchanged.


https://github.com/user-attachments/assets/5795bcb9-432e-42bc-bde3-ee363e9aeb69

## Test plan

- `just test -p codex-tui ultra_reasoning_selection`
- Built the full debug CLI with `cargo build -p codex-cli`.
- Manually verified the debug TUI with a 10-thread limit: switching from
Max to Ultra displayed the warning with 10 concurrent threads and up to
9 subagents.
This commit is contained in:
Shijie Rao authored and GitHub committed 2026-07-08 12:29:21 -07:00
1 parent 1d14221af2
commit 927004c06d
4 files changed
+141 -1

No files matched your search

+65 -1
View File
@@ -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<ReasoningEffortConfig>,
should_prompt_plan_mode_scope: bool,
) -> Vec<SelectionAction> {
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<SelectionAction> = 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<SelectionAction> = 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<SelectionItem> = 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<String> {
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<ReasoningEffortConfig>,
) {
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<ReasoningEffortConfig>) {
@@ -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));
}
@@ -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.
@@ -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<String>) {
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;