mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
Reflect accepted cloud model selections in TUI status (#61350)
## Why After a cloud model selection was accepted for future turns, status displays still showed the session's serving model and reasoning effort. A reasoning-only `/model` command also resolved against that model instead of the accepted selection. ## What changed - Show the accepted model's display name and reasoning effort in the status line, terminal title, and `/status`. - Resolve reasoning-only `/model` commands against the selected model. - Share the session-only model change confirmation between cloud and local selections, including reasoning effort and service tier when present. ## Testing Extend cloud model admission coverage to check accepted selections in status output, reasoning-only commands, and unchanged status before acceptance and after session metadata clears the selection. GitOrigin-RevId: cc50c2b0a98e884138ad2c2339c79c28ee3d346a
This commit is contained in:
committed by
copyberry
parent
d87330d91e
commit
f8580db472
@@ -122,10 +122,8 @@ impl App {
|
||||
);
|
||||
return;
|
||||
};
|
||||
let label = Self::reasoning_label_for(&model, effort.as_ref())
|
||||
.map_or_else(String::new, |label| format!(" {label}"));
|
||||
let message =
|
||||
format!("Selected {model}{label} for the next new turn in this conversation");
|
||||
session_model_confirmation(&model, effort.as_ref(), service_tier.as_deref());
|
||||
match app_server.stage_cloud_model_selection(thread_id, preset, effort) {
|
||||
Ok(dropped_tier) => {
|
||||
self.chat_widget.sync_service_tier_commands();
|
||||
@@ -227,22 +225,7 @@ impl App {
|
||||
}
|
||||
self.sync_active_thread_service_tier_to_cached_session()
|
||||
.await;
|
||||
let mut message = format!("Model changed to {model}");
|
||||
if let Some(label) = Self::reasoning_label_for(&model, effort.as_ref()) {
|
||||
message.push(' ');
|
||||
message.push_str(&label);
|
||||
}
|
||||
if let Some(service_tier) = service_tier {
|
||||
message.push(' ');
|
||||
message.push_str(if service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE {
|
||||
"standard"
|
||||
} else if service_tier == ServiceTier::Fast.request_value() {
|
||||
"fast"
|
||||
} else {
|
||||
&service_tier
|
||||
});
|
||||
}
|
||||
message.push_str(" for this session only");
|
||||
let message = session_model_confirmation(&model, effort.as_ref(), service_tier.as_deref());
|
||||
self.chat_widget.add_info_message(message, /*hint*/ None);
|
||||
}
|
||||
|
||||
@@ -261,3 +244,27 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn session_model_confirmation(
|
||||
model: &str,
|
||||
effort: Option<&ReasoningEffort>,
|
||||
service_tier: Option<&str>,
|
||||
) -> String {
|
||||
let mut message = format!("Model changed to {model}");
|
||||
if let Some(label) = App::reasoning_label_for(model, effort) {
|
||||
message.push(' ');
|
||||
message.push_str(&label);
|
||||
}
|
||||
if let Some(service_tier) = service_tier {
|
||||
message.push(' ');
|
||||
message.push_str(if service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE {
|
||||
"standard"
|
||||
} else if service_tier == ServiceTier::Fast.request_value() {
|
||||
"fast"
|
||||
} else {
|
||||
service_tier
|
||||
});
|
||||
}
|
||||
message.push_str(" for this session only");
|
||||
message
|
||||
}
|
||||
|
||||
@@ -442,6 +442,10 @@ async fn cloud_model_selection_waits_for_native_admission() -> Result<()> {
|
||||
|
||||
app.app_server_target = crate::AppServerTarget::Cloud;
|
||||
app.bind_backend_workspace(&server, app.config.cwd.to_path_buf());
|
||||
app.chat_widget.setup_status_line(
|
||||
vec![crate::bottom_pane::StatusLineItem::ModelWithReasoning],
|
||||
/*use_theme_colors*/ false,
|
||||
);
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::UltrafastMode, /*enabled*/ true);
|
||||
app.chat_widget.open_model_popup();
|
||||
@@ -456,6 +460,7 @@ async fn cloud_model_selection_waits_for_native_admission() -> Result<()> {
|
||||
.iter_mut()
|
||||
.find(|model| model.model == "gpt-5.6-terra")
|
||||
.expect("Cloud model");
|
||||
selected.display_name = "Cloud Terra".into();
|
||||
selected.default_service_tier = Some("ultrafast".into());
|
||||
selected.service_tiers = vec![codex_protocol::openai_models::ModelServiceTier {
|
||||
id: "ultrafast".into(),
|
||||
@@ -465,6 +470,7 @@ async fn cloud_model_selection_waits_for_native_admission() -> Result<()> {
|
||||
assert!(app.chat_widget.on_models_loaded(request_id, Ok(models)));
|
||||
app.chat_widget.handle_key_event(KeyCode::Esc.into());
|
||||
while events.try_recv().is_ok() {}
|
||||
let original_status_line = app.chat_widget.status_line_text();
|
||||
|
||||
let event = AppEvent::ApplyModelSelection {
|
||||
model: "gpt-5.6-terra".into(),
|
||||
@@ -473,6 +479,7 @@ async fn cloud_model_selection_waits_for_native_admission() -> Result<()> {
|
||||
Box::pin(app.handle_event(&mut tui, &mut server, event)).await?;
|
||||
assert!(commands.try_recv().is_err());
|
||||
assert!(app.chat_widget.cloud_picker_selection.is_none());
|
||||
assert_eq!(app.chat_widget.status_line_text(), original_status_line);
|
||||
let messages = std::iter::from_fn(|| events.try_recv().ok())
|
||||
.filter_map(|event| match event {
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
@@ -484,7 +491,7 @@ async fn cloud_model_selection_waits_for_native_admission() -> Result<()> {
|
||||
insta::allow_duplicates! {
|
||||
insta::assert_snapshot!(
|
||||
messages.join("\n"),
|
||||
@"• Selected gpt-5.6-terra high for the next new turn in this conversation"
|
||||
@"• Model changed to gpt-5.6-terra high for this session only"
|
||||
);
|
||||
}
|
||||
app.chat_widget.apply_external_edit("continue".into());
|
||||
@@ -542,6 +549,45 @@ async fn cloud_model_selection_waits_for_native_admission() -> Result<()> {
|
||||
admitted.then_some(("gpt-5.6-terra", Some(ReasoningEffortConfig::High))),
|
||||
);
|
||||
if accepted.is_some() {
|
||||
let status_line = app.chat_widget.status_line_text().expect("status line");
|
||||
while events.try_recv().is_ok() {}
|
||||
app.chat_widget
|
||||
.dispatch_command(crate::slash_command::SlashCommand::Status);
|
||||
let AppEvent::InsertHistoryCell(status) = events.try_recv()? else {
|
||||
panic!("expected /status output");
|
||||
};
|
||||
let status = lines_to_single_string(&status.display_lines(/*width*/ 120));
|
||||
let model = status
|
||||
.lines()
|
||||
.find_map(|line| line.split_once("Model:"))
|
||||
.expect("model row in /status")
|
||||
.1
|
||||
.trim()
|
||||
.trim_end_matches('│')
|
||||
.trim_end();
|
||||
insta::allow_duplicates! {
|
||||
insta::assert_snapshot!(format!("{status_line}\nModel: {model}"), @r"
|
||||
Cloud Terra high
|
||||
Model: Cloud Terra (reasoning high)
|
||||
");
|
||||
}
|
||||
app.chat_widget
|
||||
.apply_external_edit("/model --reasoning=medium".into());
|
||||
app.chat_widget.handle_key_event(KeyCode::Esc.into());
|
||||
app.chat_widget.handle_key_event(KeyCode::Enter.into());
|
||||
let mut messages = Vec::new();
|
||||
while let Ok(event) = events.try_recv() {
|
||||
if let AppEvent::InsertHistoryCell(cell) = &event {
|
||||
messages.push(lines_to_single_string(&cell.transcript_lines(/*width*/ 80)));
|
||||
}
|
||||
Box::pin(app.handle_event(&mut tui, &mut server, event)).await?;
|
||||
}
|
||||
insta::allow_duplicates! {
|
||||
insta::assert_snapshot!(
|
||||
messages.join("\n"),
|
||||
@"• Model changed to gpt-5.6-terra medium for this session only"
|
||||
);
|
||||
}
|
||||
app.chat_widget.open_model_popup();
|
||||
let popup = render_bottom_popup(&app.chat_widget, /*width*/ 80);
|
||||
let label = popup.lines().find(|line| line.contains("Accepted for"));
|
||||
@@ -554,6 +600,7 @@ async fn cloud_model_selection_waits_for_native_admission() -> Result<()> {
|
||||
));
|
||||
assert!(app.chat_widget.cloud_picker_selection.is_none());
|
||||
}
|
||||
assert_eq!(app.chat_widget.status_line_text(), original_status_line);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -552,6 +552,7 @@ pub(crate) struct ChatWidget {
|
||||
has_codex_backend_auth: bool,
|
||||
model_catalog: Arc<ModelCatalog>,
|
||||
model_popup_request_id: Option<uuid::Uuid>,
|
||||
/// Cloud settings accepted for future turns. Serving model metadata comes from the session.
|
||||
pub(crate) cloud_picker_selection:
|
||||
Option<crate::app_server_session::user_message_submission::CloudModelSelection>,
|
||||
pub(crate) pending_cloud_turn_settings:
|
||||
|
||||
@@ -132,7 +132,7 @@ fn resolve_model_command(chat: &ChatWidget, args: &str) -> Result<ModelCommand,
|
||||
let model_name = args
|
||||
.model
|
||||
.clone()
|
||||
.unwrap_or_else(|| chat.current_model().to_owned());
|
||||
.unwrap_or_else(|| chat.selected_model().to_owned());
|
||||
let Some(model) =
|
||||
chat.model_catalog.models.iter().find(|model| {
|
||||
model.model == model_name && (args.model.is_none() || model.show_in_picker)
|
||||
|
||||
@@ -87,7 +87,7 @@ impl ChatWidget {
|
||||
.and_then(|id| self.pending_cloud_turn_settings.as_ref()?.model(id));
|
||||
let model = pending_model
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| self.service_tier_model());
|
||||
.unwrap_or_else(|| self.selected_model());
|
||||
self.model_catalog
|
||||
.try_list_models()
|
||||
.ok()
|
||||
@@ -136,7 +136,7 @@ impl ChatWidget {
|
||||
.find(|tier| tier.name.eq_ignore_ascii_case(SPEED_TIER_FAST))
|
||||
}
|
||||
|
||||
pub(super) fn service_tier_model(&self) -> &str {
|
||||
pub(super) fn selected_model(&self) -> &str {
|
||||
self.cloud_picker_selection.as_ref().map_or_else(
|
||||
|| self.current_model(),
|
||||
|selection| selection.model.as_str(),
|
||||
@@ -152,7 +152,7 @@ impl ChatWidget {
|
||||
self.effective_service_tier = service_tier_resolution::effective_service_tier(
|
||||
&self.config,
|
||||
&self.local_settings.notices,
|
||||
self.service_tier_model(),
|
||||
self.selected_model(),
|
||||
&self.model_catalog.try_list_models().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ impl ChatWidget {
|
||||
.map(|ti| &ti.total_token_usage)
|
||||
.unwrap_or(&default_usage);
|
||||
let collaboration_mode = self.collaboration_mode_label();
|
||||
let model = self.current_model().to_string();
|
||||
let model = self.selected_model().to_string();
|
||||
let model_default_reasoning_effort =
|
||||
self.model_catalog
|
||||
.try_list_models()
|
||||
@@ -231,9 +231,14 @@ impl ChatWidget {
|
||||
.find(|preset| preset.model == model)
|
||||
.map(|preset| preset.default_reasoning_effort)
|
||||
});
|
||||
let configured_reasoning_effort = if self.cloud_picker_selection.is_none() {
|
||||
self.config.model_reasoning_effort.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let reasoning_effort_override = Some(
|
||||
self.effective_reasoning_effort()
|
||||
.or_else(|| self.config.model_reasoning_effort.clone())
|
||||
self.status_reasoning_effort()
|
||||
.or(configured_reasoning_effort)
|
||||
.or(model_default_reasoning_effort),
|
||||
);
|
||||
let rate_limit_snapshots: Vec<RateLimitSnapshotDisplay> = self
|
||||
@@ -258,7 +263,7 @@ impl ChatWidget {
|
||||
rate_limit_snapshots.as_slice(),
|
||||
self.plan_type,
|
||||
Local::now(),
|
||||
self.model_display_name(),
|
||||
self.status_model_display_name(),
|
||||
collaboration_mode,
|
||||
reasoning_effort_override,
|
||||
agents_summary,
|
||||
|
||||
@@ -725,7 +725,7 @@ impl ChatWidget {
|
||||
/// git metadata.
|
||||
pub(super) fn status_line_value_for_item(&mut self, item: StatusLineItem) -> Option<String> {
|
||||
match item {
|
||||
StatusLineItem::ModelName => Some(self.model_display_name().to_string()),
|
||||
StatusLineItem::ModelName => Some(self.status_model_display_name().to_string()),
|
||||
StatusLineItem::ModelWithReasoning => {
|
||||
let service_tier = self.processing_tier_display_name().map(str::to_lowercase);
|
||||
Some(self.model_with_reasoning_display_name(service_tier.as_deref()))
|
||||
@@ -972,7 +972,7 @@ impl ChatWidget {
|
||||
self.status_line_value_for_item(StatusLineItem::Daybreak)
|
||||
}
|
||||
TerminalTitleItem::Model => Some(Self::truncate_terminal_title_part(
|
||||
self.model_display_name().to_string(),
|
||||
self.status_model_display_name().to_string(),
|
||||
/*max_chars*/ 32,
|
||||
)),
|
||||
TerminalTitleItem::ModelWithReasoning => Some(Self::truncate_terminal_title_part(
|
||||
@@ -987,17 +987,32 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn status_model_display_name(&self) -> &str {
|
||||
self.cloud_picker_selection.as_ref().map_or_else(
|
||||
|| self.model_display_name(),
|
||||
|selection| self.model_catalog.display_name(&selection.model),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn status_reasoning_effort(&self) -> Option<ReasoningEffortConfig> {
|
||||
self.cloud_picker_selection.as_ref().map_or_else(
|
||||
|| self.effective_reasoning_effort(),
|
||||
|selection| selection.effort.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn reasoning_display_name(&self) -> String {
|
||||
let effort = self.effective_reasoning_effort();
|
||||
let effort = self.status_reasoning_effort();
|
||||
Self::status_line_reasoning_effort_label(effort.as_ref())
|
||||
}
|
||||
|
||||
fn model_with_reasoning_display_name(&self, service_tier: Option<&str>) -> String {
|
||||
let model = self.status_model_display_name();
|
||||
let label = self.reasoning_display_name();
|
||||
let service_tier_label = service_tier
|
||||
.map(|tier| format!(" {tier}"))
|
||||
.unwrap_or_default();
|
||||
format!("{} {label}{service_tier_label}", self.model_display_name())
|
||||
format!("{model} {label}{service_tier_label}")
|
||||
}
|
||||
|
||||
fn processing_tier_display_name(&self) -> Option<&str> {
|
||||
@@ -1005,7 +1020,7 @@ impl ChatWidget {
|
||||
self.model_catalog
|
||||
.models
|
||||
.iter()
|
||||
.find(|preset| preset.model == self.service_tier_model())
|
||||
.find(|preset| preset.model == self.selected_model())
|
||||
.and_then(|preset| preset.service_tiers.iter().find(|tier| tier.id == id))
|
||||
.map(|tier| tier.name.as_str())
|
||||
.or_else(|| {
|
||||
|
||||
Reference in New Issue
Block a user