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:
Felipe Coury authored and copyberry committed 2026-07-13 09:40:51 +00:00
1 parent 2a4942c081
commit 769a5de257
27 files changed
+1447 -92

No files matched your search

+70 -1
View File
@@ -37,7 +37,10 @@ pub fn rollout_item_affects_thread_metadata(item: &RolloutItem) -> bool {
match item {
RolloutItem::SessionMeta(_) | RolloutItem::TurnContext(_) => true,
RolloutItem::EventMsg(
EventMsg::TokenCount(_) | EventMsg::UserMessage(_) | EventMsg::ThreadGoalUpdated(_),
EventMsg::TokenCount(_)
| EventMsg::UserMessage(_)
| EventMsg::ThreadGoalUpdated(_)
| EventMsg::ThreadSettingsApplied(_),
) => true,
RolloutItem::EventMsg(EventMsg::ItemCompleted(event))
if matches!(event.item, TurnItem::UserMessage(_)) =>
@@ -114,6 +117,16 @@ fn apply_event_msg(metadata: &mut ThreadMetadata, event: &EventMsg) {
set_preview_if_empty(metadata, Some(objective.to_string()));
}
}
EventMsg::ThreadSettingsApplied(event) => {
let settings = &event.thread_settings;
metadata.model = Some(settings.model.clone());
metadata.model_provider = settings.model_provider_id.clone();
metadata.reasoning_effort = settings.reasoning_effort.clone();
metadata.cwd = settings.cwd.clone().into_path_buf();
metadata.sandbox_policy =
serde_json::to_string(&settings.permission_profile).unwrap_or_default();
metadata.approval_mode = enum_to_string(&settings.approval_policy);
}
_ => {}
}
}
@@ -151,10 +164,16 @@ pub(crate) fn enum_to_string<T: Serialize>(value: &T) -> String {
#[cfg(test)]
mod tests {
use super::apply_rollout_item;
use super::rollout_item_affects_thread_metadata;
use crate::model::ThreadMetadata;
use chrono::DateTime;
use chrono::Utc;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings;
use codex_protocol::items::TurnItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::models::ContentItem;
@@ -173,6 +192,8 @@ mod tests {
use codex_protocol::protocol::ThreadGoalStatus;
use codex_protocol::protocol::ThreadGoalUpdatedEvent;
use codex_protocol::protocol::ThreadHistoryMode;
use codex_protocol::protocol::ThreadSettingsAppliedEvent;
use codex_protocol::protocol::ThreadSettingsSnapshot;
use codex_protocol::protocol::TurnContextItem;
use codex_protocol::protocol::USER_MESSAGE_BEGIN;
use codex_protocol::protocol::UserMessageEvent;
@@ -530,6 +551,54 @@ mod tests {
assert_eq!(metadata.reasoning_effort, Some(ReasoningEffort::High));
}
#[test]
fn thread_settings_applied_updates_resume_metadata() {
let mut metadata = metadata_for_test();
let permission_profile = PermissionProfile::workspace_write();
let cwd = std::env::current_dir()
.expect("current directory")
.join("updated/workspace");
let item = RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(
ThreadSettingsAppliedEvent {
thread_settings: ThreadSettingsSnapshot {
model: "gpt-5.2-codex".to_string(),
model_provider_id: "updated-provider".to_string(),
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: permission_profile.clone(),
active_permission_profile: None,
cwd: cwd.clone().try_into().expect("absolute settings cwd"),
reasoning_effort: Some(ReasoningEffort::Ultra),
reasoning_summary: Some(ReasoningSummary::Auto),
personality: None,
collaboration_mode: CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
model: "gpt-5.2-codex".to_string(),
reasoning_effort: Some(ReasoningEffort::Ultra),
developer_instructions: None,
},
},
},
},
));
assert!(rollout_item_affects_thread_metadata(&item));
apply_rollout_item(&mut metadata, &item, "test-provider");
assert_eq!(metadata.model.as_deref(), Some("gpt-5.2-codex"));
assert_eq!(metadata.model_provider, "updated-provider");
assert_eq!(metadata.reasoning_effort, Some(ReasoningEffort::Ultra));
assert_eq!(metadata.cwd, cwd);
assert_eq!(metadata.approval_mode, "never");
assert_eq!(
metadata.sandbox_policy,
serde_json::to_string(&permission_profile)
.expect("permission profile should serialize")
);
}
#[test]
fn session_meta_does_not_set_model_or_reasoning_effort() {
let mut metadata = metadata_for_test();