Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion codex-rs/tui/src/chatwidget/model_popups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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),
)));
}
})]
}

Expand Down Expand Up @@ -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| {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)");
Expand Down Expand Up @@ -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),
)));
}
}
})];

Expand Down Expand Up @@ -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>) {
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/tui/src/chatwidget/reasoning_shortcuts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 62 additions & 0 deletions codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading