mirror of
https://github.com/openai/codex.git
synced 2026-11-05 16:29:31 +00:00
Make advanced reasoning selection explicit in the TUI (#32746)
## Why `Max` and `Ultra` consume usage limits faster than standard reasoning levels, so they should not be selected accidentally while navigating the normal effort scale. ## What changed - Move `Max` and `Ultra` behind a `More reasoning…` entry with a dedicated warning and descriptions. - Keep the reasoning shortcuts from silently increasing into advanced efforts. - Apply `Ultra` to the active conversation without changing defaults for new threads, while preserving it across mode switches and thread resumes. - Record applied thread settings in thread metadata, including explicit clearing of reasoning effort, so resumed threads restore their latest model settings unless the user supplied an override. ## Testing - Add TUI coverage for the advanced picker, shortcuts, Plan mode, configuration defaults, and resumed conversations. - Add app-server and thread-store coverage for persisting, clearing, and restoring model and reasoning settings. GitOrigin-RevId: 6708c7c2e8d38f491bf63000ea34f83476afdfe6
This commit is contained in:
1 parent
2a4942c081
commit
769a5de257
27 files changed
+1447
-92
No files matched your search
@@ -100,22 +100,39 @@ impl ChatWidget {
|
||||
let description =
|
||||
(!preset.description.is_empty()).then_some(preset.description.clone());
|
||||
let model = preset.model.clone();
|
||||
let should_prompt_plan_mode_scope = self.should_prompt_plan_mode_reasoning_scope(
|
||||
model.as_str(),
|
||||
Some(preset.default_reasoning_effort.clone()),
|
||||
);
|
||||
let actions = self.model_selection_actions(
|
||||
model.clone(),
|
||||
Some(preset.default_reasoning_effort.clone()),
|
||||
should_prompt_plan_mode_scope,
|
||||
);
|
||||
let requires_advanced_selection =
|
||||
Self::is_advanced_reasoning_effort(&preset.default_reasoning_effort)
|
||||
|| preset
|
||||
.supported_reasoning_efforts
|
||||
.iter()
|
||||
.any(|option| Self::is_advanced_reasoning_effort(&option.effort));
|
||||
let actions: Vec<SelectionAction> = if requires_advanced_selection {
|
||||
let preset_for_action = preset.clone();
|
||||
vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::OpenReasoningPopup {
|
||||
model: preset_for_action.clone(),
|
||||
});
|
||||
})]
|
||||
} else {
|
||||
let should_prompt_plan_mode_scope = self
|
||||
.should_prompt_plan_mode_reasoning_scope(
|
||||
model.as_str(),
|
||||
Some(preset.default_reasoning_effort.clone()),
|
||||
);
|
||||
self.model_selection_actions(
|
||||
model.clone(),
|
||||
Some(preset.default_reasoning_effort.clone()),
|
||||
should_prompt_plan_mode_scope,
|
||||
)
|
||||
};
|
||||
SelectionItem {
|
||||
name: model.clone(),
|
||||
description,
|
||||
is_current: model.as_str() == current_model,
|
||||
is_default: preset.is_default,
|
||||
actions,
|
||||
dismiss_on_select: true,
|
||||
dismiss_on_select: !requires_advanced_selection,
|
||||
dismiss_parent_on_child_accept: requires_advanced_selection,
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
@@ -225,20 +242,24 @@ impl ChatWidget {
|
||||
.as_ref()
|
||||
.and_then(|effort| self.ultra_reasoning_concurrency_warning(effort));
|
||||
vec![Box::new(move |tx| {
|
||||
if should_prompt_plan_mode_scope {
|
||||
if effort_for_action == Some(ReasoningEffortConfig::Ultra) {
|
||||
tx.send(AppEvent::ApplyAdvancedReasoning {
|
||||
model: model_for_action.clone(),
|
||||
effort: ReasoningEffortConfig::Ultra,
|
||||
});
|
||||
} else if should_prompt_plan_mode_scope {
|
||||
tx.send(AppEvent::OpenPlanReasoningScopePrompt {
|
||||
model: model_for_action.clone(),
|
||||
effort: effort_for_action.clone(),
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
tx.send(AppEvent::UpdateModel(model_for_action.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort_for_action.clone()));
|
||||
tx.send(AppEvent::PersistModelSelection {
|
||||
model: model_for_action.clone(),
|
||||
effort: effort_for_action.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
tx.send(AppEvent::UpdateModel(model_for_action.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort_for_action.clone()));
|
||||
tx.send(AppEvent::PersistModelSelection {
|
||||
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),
|
||||
@@ -372,10 +393,13 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
/// Open a popup to choose the reasoning effort (stage 2) for the given model.
|
||||
/// Open a popup to choose the standard reasoning effort for the given model.
|
||||
///
|
||||
/// Max and Ultra require an explicit second step so expensive efforts cannot
|
||||
/// be selected accidentally while moving through the normal effort scale.
|
||||
pub(crate) fn open_reasoning_popup(&mut self, preset: ModelPreset) {
|
||||
let default_effort = preset.default_reasoning_effort;
|
||||
let supported = preset.supported_reasoning_efforts;
|
||||
let default_effort = preset.default_reasoning_effort.clone();
|
||||
let supported = &preset.supported_reasoning_efforts;
|
||||
let in_plan_mode =
|
||||
self.collaboration_modes_enabled() && self.active_mode_kind() == ModeKind::Plan;
|
||||
|
||||
@@ -400,15 +424,18 @@ impl ChatWidget {
|
||||
|| preset.model.starts_with("gpt-5.1-codex-max")
|
||||
|| preset.model.starts_with("gpt-5.2");
|
||||
|
||||
let mut choices: Vec<ReasoningEffortConfig> = supported
|
||||
let mut all_choices: Vec<ReasoningEffortConfig> = supported
|
||||
.iter()
|
||||
.map(|option| option.effort.clone())
|
||||
.collect();
|
||||
if choices.is_empty() {
|
||||
choices.push(default_effort.clone());
|
||||
if all_choices.is_empty() {
|
||||
all_choices.push(default_effort.clone());
|
||||
}
|
||||
let (choices, advanced_choices): (Vec<_>, Vec<_>) = all_choices
|
||||
.into_iter()
|
||||
.partition(|effort| !Self::is_advanced_reasoning_effort(effort));
|
||||
|
||||
if choices.len() == 1 {
|
||||
if choices.len() == 1 && advanced_choices.is_empty() {
|
||||
let selected_effort = choices.first().cloned();
|
||||
let selected_model = preset.model;
|
||||
if self
|
||||
@@ -427,9 +454,7 @@ impl ChatWidget {
|
||||
|
||||
let default_choice = choices
|
||||
.contains(&default_effort)
|
||||
.then(|| default_effort.clone())
|
||||
.or_else(|| choices.first().cloned())
|
||||
.or(Some(default_effort));
|
||||
.then(|| default_effort.clone());
|
||||
|
||||
let model_slug = preset.model.to_string();
|
||||
let is_current_model = self.current_model() == preset.model.as_str();
|
||||
@@ -443,7 +468,7 @@ impl ChatWidget {
|
||||
self.effective_reasoning_effort()
|
||||
}
|
||||
} else {
|
||||
default_choice.clone()
|
||||
default_choice.clone().or_else(|| choices.first().cloned())
|
||||
};
|
||||
let selection_choice = highlight_choice.clone().or_else(|| default_choice.clone());
|
||||
let initial_selected_idx = choices
|
||||
@@ -452,7 +477,6 @@ 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)");
|
||||
@@ -476,32 +500,16 @@ impl ChatWidget {
|
||||
None
|
||||
};
|
||||
|
||||
let model_for_action = model_slug.clone();
|
||||
let choice_effort = Some(effort);
|
||||
let should_prompt_plan_mode_scope = self.should_prompt_plan_mode_reasoning_scope(
|
||||
model_slug.as_str(),
|
||||
choice_effort.clone(),
|
||||
);
|
||||
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
if should_prompt_plan_mode_scope {
|
||||
tx.send(AppEvent::OpenPlanReasoningScopePrompt {
|
||||
model: model_for_action.clone(),
|
||||
effort: choice_effort.clone(),
|
||||
});
|
||||
} else {
|
||||
tx.send(AppEvent::UpdateModel(model_for_action.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(choice_effort.clone()));
|
||||
tx.send(AppEvent::PersistModelSelection {
|
||||
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),
|
||||
)));
|
||||
}
|
||||
}
|
||||
})];
|
||||
let actions = self.model_selection_actions(
|
||||
model_slug.clone(),
|
||||
choice_effort,
|
||||
should_prompt_plan_mode_scope,
|
||||
);
|
||||
|
||||
items.push(SelectionItem {
|
||||
name: effort_label,
|
||||
@@ -514,6 +522,36 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
if !advanced_choices.is_empty() {
|
||||
let advanced_label = advanced_choices
|
||||
.iter()
|
||||
.map(Self::reasoning_effort_label)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" and ");
|
||||
let verb = if advanced_choices.len() == 1 {
|
||||
"consumes"
|
||||
} else {
|
||||
"consume"
|
||||
};
|
||||
let preset_for_action = preset;
|
||||
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::OpenAdvancedReasoningPopup {
|
||||
model: preset_for_action.clone(),
|
||||
});
|
||||
})];
|
||||
items.push(SelectionItem {
|
||||
name: "More reasoning…".to_string(),
|
||||
description: Some(format!("{advanced_label} {verb} usage limits faster")),
|
||||
is_current: is_current_model
|
||||
&& highlight_choice
|
||||
.as_ref()
|
||||
.is_some_and(Self::is_advanced_reasoning_effort),
|
||||
actions,
|
||||
dismiss_parent_on_child_accept: true,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from(
|
||||
format!("Select Reasoning Level for {model_slug}").bold(),
|
||||
@@ -528,6 +566,76 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
/// Open the explicit Max/Ultra effort picker for the given model.
|
||||
pub(crate) fn open_advanced_reasoning_popup(&mut self, preset: ModelPreset) {
|
||||
let mut choices = preset
|
||||
.supported_reasoning_efforts
|
||||
.iter()
|
||||
.map(|option| option.effort.clone())
|
||||
.filter(Self::is_advanced_reasoning_effort)
|
||||
.collect::<Vec<_>>();
|
||||
if choices.is_empty()
|
||||
&& Self::is_advanced_reasoning_effort(&preset.default_reasoning_effort)
|
||||
{
|
||||
choices.push(preset.default_reasoning_effort.clone());
|
||||
}
|
||||
choices.sort_by_key(|effort| matches!(effort, ReasoningEffortConfig::Ultra));
|
||||
if choices.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let model_slug = preset.model.to_string();
|
||||
let is_current_model = self.current_model() == preset.model.as_str();
|
||||
let highlight_choice = is_current_model
|
||||
.then(|| self.effective_reasoning_effort())
|
||||
.flatten();
|
||||
let mut items = Vec::new();
|
||||
for effort in choices {
|
||||
let description = match &effort {
|
||||
ReasoningEffortConfig::Max => {
|
||||
"For difficult problems when quality matters more than speed · higher usage"
|
||||
}
|
||||
ReasoningEffortConfig::Ultra => {
|
||||
"For demanding work using multiple agents · highest usage"
|
||||
}
|
||||
_ => unreachable!("advanced choices are limited to Max and Ultra"),
|
||||
};
|
||||
let should_prompt_plan_mode_scope = self
|
||||
.should_prompt_plan_mode_reasoning_scope(model_slug.as_str(), Some(effort.clone()));
|
||||
let actions = self.model_selection_actions(
|
||||
model_slug.clone(),
|
||||
Some(effort.clone()),
|
||||
should_prompt_plan_mode_scope,
|
||||
);
|
||||
|
||||
items.push(SelectionItem {
|
||||
name: Self::reasoning_effort_label(&effort),
|
||||
description: Some(description.to_string()),
|
||||
is_current: is_current_model && Some(&effort) == highlight_choice.as_ref(),
|
||||
actions,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Advanced Reasoning".bold()));
|
||||
header.push(Line::from("⚠ Consumes usage limits faster".cyan()));
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
header: Box::new(header),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn is_advanced_reasoning_effort(effort: &ReasoningEffortConfig) -> bool {
|
||||
matches!(
|
||||
effort,
|
||||
ReasoningEffortConfig::Max | ReasoningEffortConfig::Ultra
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn reasoning_effort_label(effort: &ReasoningEffortConfig) -> String {
|
||||
match effort {
|
||||
ReasoningEffortConfig::None => "None".to_string(),
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
//! current model preset's default, and it walks only efforts advertised by the
|
||||
//! active model. Unsupported efforts anchor to the model default, or the first
|
||||
//! advertised effort when the default is absent, before stepping through the
|
||||
//! advertised order.
|
||||
//! advertised order. Raising never silently crosses into Max or Ultra; those
|
||||
//! efforts require the explicit advanced-reasoning picker.
|
||||
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
@@ -109,6 +110,34 @@ impl ChatWidget {
|
||||
return true;
|
||||
};
|
||||
|
||||
if direction == ReasoningShortcutDirection::Raise
|
||||
&& Self::is_advanced_reasoning_effort(&next_effort)
|
||||
{
|
||||
let advanced_label = choices
|
||||
.iter()
|
||||
.filter(|effort| Self::is_advanced_reasoning_effort(effort))
|
||||
.map(Self::reasoning_effort_label)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" and ");
|
||||
let verb = if advanced_label.contains(" and ") {
|
||||
"are"
|
||||
} else {
|
||||
"is"
|
||||
};
|
||||
let model_path = if current_model.starts_with("codex-auto-") {
|
||||
current_model
|
||||
} else {
|
||||
format!("All models → {current_model}")
|
||||
};
|
||||
self.add_info_message(
|
||||
format!(
|
||||
"{advanced_label} {verb} available under /model → {model_path} → More reasoning…"
|
||||
),
|
||||
/*hint*/ None,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.collaboration_modes_enabled() && self.active_mode_kind() == ModeKind::Plan {
|
||||
let warning = self.ultra_reasoning_concurrency_warning(&next_effort);
|
||||
self.app_event_tx
|
||||
@@ -136,11 +165,13 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn reasoning_choices(preset: &ModelPreset) -> Vec<ReasoningEffortConfig> {
|
||||
let mut choices: Vec<ReasoningEffortConfig> = preset
|
||||
let (mut choices, mut advanced_choices): (Vec<_>, Vec<_>) = preset
|
||||
.supported_reasoning_efforts
|
||||
.iter()
|
||||
.map(|option| option.effort.clone())
|
||||
.collect();
|
||||
.partition(|effort| !ChatWidget::is_advanced_reasoning_effort(effort));
|
||||
advanced_choices.sort_by_key(|effort| matches!(effort, ReasoningEffortConfig::Ultra));
|
||||
choices.extend(advanced_choices);
|
||||
if choices.is_empty() {
|
||||
choices.push(preset.default_reasoning_effort.clone());
|
||||
}
|
||||
|
||||
@@ -83,6 +83,9 @@ impl ChatWidget {
|
||||
Some(session.reasoning_effort.clone()),
|
||||
/*developer_instructions*/ None,
|
||||
);
|
||||
if session.reasoning_effort == Some(ReasoningEffortConfig::Ultra) {
|
||||
self.set_plan_mode_reasoning_effort(Some(ReasoningEffortConfig::Ultra));
|
||||
}
|
||||
match session.collaboration_mode.as_deref() {
|
||||
Some(collaboration_mode) => {
|
||||
self.set_effective_collaboration_mode(collaboration_mode.clone());
|
||||
|
||||
@@ -345,7 +345,6 @@ impl ChatWidget {
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Used in tests
|
||||
pub(crate) fn current_collaboration_mode(&self) -> &CollaborationMode {
|
||||
&self.current_collaboration_mode
|
||||
}
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
expression: popup
|
||||
---
|
||||
Select Reasoning Level for gpt-5.4
|
||||
|
||||
› 1. More reasoning… Ultra consumes usage limits faster
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
expression: popup
|
||||
---
|
||||
Advanced Reasoning
|
||||
⚠ Consumes usage limits faster
|
||||
|
||||
1. Max For difficult problems when quality matters more than
|
||||
speed · higher usage
|
||||
› 2. Ultra (current) For demanding work using multiple agents · highest usage
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
+3
-3
@@ -6,8 +6,8 @@ expression: popup
|
||||
|
||||
1. Low Fast responses with lighter reasoning
|
||||
2. Medium (default) Balances speed and reasoning depth for everyday tasks
|
||||
3. Max Maximum available reasoning
|
||||
› 4. High (current) Greater reasoning depth for complex problems
|
||||
5. Extra high Extra high reasoning depth for complex problems
|
||||
› 3. High (current) Greater reasoning depth for complex problems
|
||||
4. Extra high Extra high reasoning depth for complex problems
|
||||
5. More reasoning… Max and Ultra consume usage limits faster
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
@@ -75,6 +75,43 @@ async fn resumed_initial_messages_render_history() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn restored_conversation_ultra_remains_selected_after_switching_to_plan() {
|
||||
let (mut chat, _rx, _ops) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
|
||||
chat.set_plan_mode_reasoning_effort(Some(ReasoningEffortConfig::High));
|
||||
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "gpt-5.4".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::Ultra),
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
});
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::BackTab));
|
||||
|
||||
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Plan);
|
||||
assert_eq!(
|
||||
chat.current_reasoning_effort(),
|
||||
Some(ReasoningEffortConfig::Ultra)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replayed_user_messages_seed_composer_history() {
|
||||
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -427,6 +427,52 @@ async fn reasoning_shortcut_in_plan_mode_updates_plan_override_without_prompt_or
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn advanced_reasoning_selection_in_plan_mode_uses_expected_scope() {
|
||||
for effort in [ReasoningEffortConfig::Ultra, ReasoningEffortConfig::Max] {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
|
||||
let plan_mask = collaboration_modes::plan_mask(chat.model_catalog.as_ref())
|
||||
.expect("expected plan collaboration mode");
|
||||
chat.set_collaboration_mask(plan_mask);
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
|
||||
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||
preset.supported_reasoning_efforts = vec![ReasoningEffortPreset {
|
||||
effort: effort.clone(),
|
||||
description: "Advanced reasoning".to_string(),
|
||||
}];
|
||||
chat.open_advanced_reasoning_popup(preset);
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
if effort == ReasoningEffortConfig::Ultra {
|
||||
assert!(events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::ApplyAdvancedReasoning {
|
||||
model,
|
||||
effort: ReasoningEffortConfig::Ultra,
|
||||
} if model == "gpt-5.4"
|
||||
)));
|
||||
assert!(events.iter().all(|event| !matches!(
|
||||
event,
|
||||
AppEvent::OpenPlanReasoningScopePrompt { .. }
|
||||
| AppEvent::PersistPlanModeReasoningEffort(_)
|
||||
| AppEvent::PersistModelSelection { .. }
|
||||
)));
|
||||
} else {
|
||||
assert!(events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::OpenPlanReasoningScopePrompt {
|
||||
model,
|
||||
effort: Some(ReasoningEffortConfig::Max),
|
||||
} if model == "gpt-5.4"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_mode_reasoning_override_is_marked_current_in_reasoning_popup() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
|
||||
@@ -3196,12 +3196,40 @@ async fn model_reasoning_selection_popup_snapshot() {
|
||||
description: "Maximum available reasoning".to_string(),
|
||||
},
|
||||
);
|
||||
preset
|
||||
.supported_reasoning_efforts
|
||||
.push(ReasoningEffortPreset {
|
||||
effort: ReasoningEffortConfig::Ultra,
|
||||
description: "Ultra reasoning".to_string(),
|
||||
});
|
||||
chat.open_reasoning_popup(preset);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert_chatwidget_snapshot!("model_reasoning_selection_popup", popup);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn model_advanced_reasoning_selection_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
chat.set_reasoning_effort(Some(ReasoningEffortConfig::Ultra));
|
||||
|
||||
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||
preset.supported_reasoning_efforts.extend([
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffortConfig::Ultra,
|
||||
description: "Ultra reasoning".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffortConfig::Max,
|
||||
description: "Maximum available reasoning".to_string(),
|
||||
},
|
||||
]);
|
||||
chat.open_advanced_reasoning_popup(preset);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert_chatwidget_snapshot!("model_advanced_reasoning_selection_popup", popup);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn model_reasoning_selection_popup_applies_custom_effort() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
@@ -3262,11 +3290,21 @@ async fn select_ultra_with_multi_agent_thread_limit(max_threads: usize) -> (bool
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
let advanced_preset = std::iter::from_fn(|| rx.try_recv().ok()).find_map(|event| match event {
|
||||
AppEvent::OpenAdvancedReasoningPopup { model } => Some(model),
|
||||
_ => None,
|
||||
});
|
||||
chat.open_advanced_reasoning_popup(advanced_preset.expect("advanced reasoning popup"));
|
||||
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)) => {
|
||||
AppEvent::ApplyAdvancedReasoning {
|
||||
effort: ReasoningEffortConfig::Ultra,
|
||||
..
|
||||
} => {
|
||||
selected_ultra = true;
|
||||
}
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
@@ -3299,6 +3337,38 @@ async fn ultra_reasoning_selection_skips_warning_below_threshold() {
|
||||
assert_eq!(below_threshold, (true, Vec::new()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn max_reasoning_selection_persists_model_selection() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
|
||||
|
||||
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||
preset.supported_reasoning_efforts = vec![ReasoningEffortPreset {
|
||||
effort: ReasoningEffortConfig::Max,
|
||||
description: "Maximum reasoning".to_string(),
|
||||
}];
|
||||
chat.open_advanced_reasoning_popup(preset);
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::UpdateReasoningEffort(Some(ReasoningEffortConfig::Max))
|
||||
)));
|
||||
assert!(events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::PersistModelSelection {
|
||||
model,
|
||||
effort: Some(ReasoningEffortConfig::Max),
|
||||
} if model == "gpt-5.4"
|
||||
)));
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.all(|event| !matches!(event, AppEvent::ApplyAdvancedReasoning { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
#[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;
|
||||
@@ -3421,6 +3491,96 @@ async fn reasoning_shortcut_is_ignored_with_model_popup_open() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reasoning_up_shortcut_does_not_silently_enter_advanced_effort() {
|
||||
for (model, model_path) in [
|
||||
("gpt-5.4", "All models → gpt-5.4"),
|
||||
("codex-auto-test", "codex-auto-test"),
|
||||
] {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||
preset.id = model.to_string();
|
||||
preset.model = model.to_string();
|
||||
preset.display_name = model.to_string();
|
||||
preset.supported_reasoning_efforts.extend([
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffortConfig::Max,
|
||||
description: "Maximum reasoning".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffortConfig::Ultra,
|
||||
description: "Ultra reasoning".to_string(),
|
||||
},
|
||||
]);
|
||||
chat.model_catalog = std::sync::Arc::new(ModelCatalog::new(vec![preset]));
|
||||
chat.set_model(model);
|
||||
|
||||
for effort in [ReasoningEffortConfig::XHigh, ReasoningEffortConfig::Max] {
|
||||
chat.set_reasoning_effort(Some(effort));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('.'), KeyModifiers::ALT));
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(events.iter().all(|event| !matches!(
|
||||
event,
|
||||
AppEvent::UpdateReasoningEffort(_) | AppEvent::ApplyAdvancedReasoning { .. }
|
||||
)));
|
||||
let messages = events
|
||||
.into_iter()
|
||||
.filter_map(|event| match event {
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
Some(lines_to_single_string(&cell.display_lines(/*width*/ 140)))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
messages,
|
||||
vec![format!(
|
||||
"• Max and Ultra are available under /model → {model_path} → More reasoning…\n"
|
||||
)]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reasoning_down_shortcut_can_leave_advanced_effort() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||
preset.supported_reasoning_efforts.extend([
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffortConfig::Ultra,
|
||||
description: "Ultra reasoning".to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffortConfig::Max,
|
||||
description: "Maximum reasoning".to_string(),
|
||||
},
|
||||
]);
|
||||
chat.model_catalog = std::sync::Arc::new(ModelCatalog::new(vec![preset]));
|
||||
|
||||
for (current, expected) in [
|
||||
(ReasoningEffortConfig::Ultra, ReasoningEffortConfig::Max),
|
||||
(ReasoningEffortConfig::Max, ReasoningEffortConfig::XHigh),
|
||||
] {
|
||||
chat.set_reasoning_effort(Some(current));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char(','), KeyModifiers::ALT));
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::UpdateReasoningEffort(Some(effort)) if effort == &expected
|
||||
)));
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.all(|event| !matches!(event, AppEvent::PersistModelSelection { .. }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reasoning_popup_shows_extra_high_with_space() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
@@ -3488,6 +3648,60 @@ async fn single_reasoning_option_skips_selection() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn advanced_only_reasoning_option_requires_explicit_selection() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||
preset.default_reasoning_effort = ReasoningEffortConfig::Ultra;
|
||||
preset.supported_reasoning_efforts = vec![ReasoningEffortPreset {
|
||||
effort: ReasoningEffortConfig::Ultra,
|
||||
description: "Ultra reasoning".to_string(),
|
||||
}];
|
||||
chat.open_reasoning_popup(preset);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert_chatwidget_snapshot!("advanced_only_reasoning_selection_popup", popup);
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(events.iter().all(|event| !matches!(
|
||||
event,
|
||||
AppEvent::UpdateReasoningEffort(_)
|
||||
| AppEvent::ApplyAdvancedReasoning { .. }
|
||||
| AppEvent::PersistModelSelection { .. }
|
||||
)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auto_model_advertising_advanced_effort_opens_reasoning_picker() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||
preset.id = "codex-auto-test".to_string();
|
||||
preset.model = "codex-auto-test".to_string();
|
||||
preset.display_name = "codex-auto-test".to_string();
|
||||
preset.default_reasoning_effort = ReasoningEffortConfig::Medium;
|
||||
preset
|
||||
.supported_reasoning_efforts
|
||||
.push(ReasoningEffortPreset {
|
||||
effort: ReasoningEffortConfig::Ultra,
|
||||
description: "Ultra reasoning".to_string(),
|
||||
});
|
||||
chat.open_model_popup_with_presets(vec![preset]);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::<Vec<_>>();
|
||||
assert!(events.iter().all(|event| !matches!(
|
||||
event,
|
||||
AppEvent::UpdateReasoningEffort(_)
|
||||
| AppEvent::ApplyAdvancedReasoning { .. }
|
||||
| AppEvent::PersistModelSelection { .. }
|
||||
)));
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|event| matches!(event, AppEvent::OpenReasoningPopup { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn feedback_selection_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
Reference in new issue
Block a user