agent: Respect favorite model settings and sync UI changes back to settings (#54318)

Closes #54313

**Before:**
- Favoriting a model only stored `provider`, `model`, and hardcoded
`enable_thinking` to `false`.
- Selecting a favorited model would not restore your preferred
`enable_thinking`, `effort`, or `speed` settings.

This means that if you'd like to use, say, GPT 5.4 your favorite model
on `xhigh` effort every single time, switching to it would set effort to
`medium` (the default for the model) instead.

**After:**
- Favoriting a model captures your current `enable_thinking`, `effort`,
and `speed` when it matches the currently-selected model. Otherwise, it
falls back to the model's own defaults, i.e. thinking-capable models are
no longer favorited with `enable_thinking` forced to `false`.
- Selecting a favorited model applies its stored thinking / effort /
speed.
- Toggling thinking, changing effort, or toggling fast mode on a
favorited model updates the favorite entry in settings (along with the
existing `default_model` setting), so the preference doesn't drift.

Release Notes:

- Agent favorite models now remember and restore per-model thinking,
effort level, and fast mode preferences.
This commit is contained in:
Smit Barmase 2026-04-20 22:40:00 +05:30 committed by GitHub
parent cd12d5f98d
commit 68da244d37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 194 additions and 48 deletions

View file

@ -47,7 +47,7 @@ use prompt_store::{
WorktreeContext,
};
use serde::{Deserialize, Serialize};
use settings::{LanguageModelSelection, update_settings_file};
use settings::{LanguageModelSelection, Settings as _, update_settings_file};
use std::any::Any;
use std::path::PathBuf;
use std::rc::Rc;
@ -1423,16 +1423,29 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
return Task::ready(Err(anyhow!("Invalid model ID {}", model_id)));
};
// We want to reset the effort level when switching models, as the currently-selected effort level may
// not be compatible.
let effort = model
.default_effort_level()
.map(|effort_level| effort_level.value.to_string());
let favorite = agent_settings::AgentSettings::get_global(cx)
.favorite_models
.iter()
.find(|favorite| {
favorite.provider.0 == model.provider_id().0.as_ref()
&& favorite.model == model.id().0.as_ref()
})
.cloned();
let LanguageModelSelection {
enable_thinking,
effort,
speed,
..
} = agent_settings::language_model_to_selection(&model, favorite.as_ref());
thread.update(cx, |thread, cx| {
thread.set_model(model.clone(), cx);
thread.set_thinking_effort(effort.clone(), cx);
thread.set_thinking_enabled(model.supports_thinking(), cx);
thread.set_thinking_enabled(enable_thinking, cx);
if let Some(speed) = speed {
thread.set_speed(speed, cx);
}
});
update_settings_file(

View file

@ -2,11 +2,12 @@ use std::{any::Any, rc::Rc, sync::Arc};
use agent_client_protocol as acp;
use agent_servers::{AgentServer, AgentServerDelegate};
use agent_settings::AgentSettings;
use agent_settings::{AgentSettings, language_model_to_selection};
use anyhow::Result;
use collections::HashSet;
use fs::Fs;
use gpui::{App, Entity, Task};
use language_model::{LanguageModelId, LanguageModelProviderId, LanguageModelRegistry};
use project::{AgentId, Project};
use prompt_store::PromptStore;
use settings::{LanguageModelSelection, Settings as _, update_settings_file};
@ -76,7 +77,7 @@ impl AgentServer for NativeAgentServer {
fs: Arc<dyn Fs>,
cx: &App,
) {
let selection = model_id_to_selection(&model_id);
let selection = model_id_to_selection(&model_id, cx);
update_settings_file(fs, cx, move |settings, _| {
let agent = settings.agent.get_or_insert_default();
if should_be_favorite {
@ -89,16 +90,41 @@ impl AgentServer for NativeAgentServer {
}
/// Convert a ModelId (e.g. "anthropic/claude-3-5-sonnet") to a LanguageModelSelection.
fn model_id_to_selection(model_id: &acp::ModelId) -> LanguageModelSelection {
fn model_id_to_selection(model_id: &acp::ModelId, cx: &App) -> LanguageModelSelection {
let id = model_id.0.as_ref();
let (provider, model) = id.split_once('/').unwrap_or(("", id));
LanguageModelSelection {
provider: provider.to_owned().into(),
model: model.to_owned(),
enable_thinking: false,
effort: None,
speed: None,
}
let provider_id = LanguageModelProviderId(provider.to_string().into());
let model_id_typed = LanguageModelId(model.to_string().into());
let resolved = LanguageModelRegistry::global(cx)
.read(cx)
.provider(&provider_id)
.and_then(|p| {
p.provided_models(cx)
.into_iter()
.find(|m| m.id() == model_id_typed)
});
let Some(resolved) = resolved else {
return LanguageModelSelection {
provider: provider.to_owned().into(),
model: model.to_owned(),
enable_thinking: false,
effort: None,
speed: None,
};
};
let current_user_selection = AgentSettings::get_global(cx)
.default_model
.as_ref()
.filter(|selection| {
selection.provider.0 == resolved.provider_id().0.as_ref()
&& selection.model == resolved.id().0.as_ref()
})
.cloned();
language_model_to_selection(&resolved, current_user_selection.as_ref())
}
#[cfg(test)]

View file

@ -210,7 +210,48 @@ impl AgentSettings {
.map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model)))
.collect()
}
}
pub fn language_model_to_selection(
model: &Arc<dyn LanguageModel>,
override_selection: Option<&LanguageModelSelection>,
) -> LanguageModelSelection {
let provider = model.provider_id().0.to_string().into();
let model_name = model.id().0.to_string();
match override_selection {
Some(current) => LanguageModelSelection {
provider,
model: model_name,
enable_thinking: current.enable_thinking && model.supports_thinking(),
effort: current
.effort
.clone()
.filter(|value| {
model
.supported_effort_levels()
.iter()
.any(|level| level.value.as_ref() == value.as_str())
})
.or_else(|| {
model
.default_effort_level()
.map(|effort| effort.value.to_string())
}),
speed: current.speed.filter(|_| model.supports_fast_mode()),
},
None => LanguageModelSelection {
provider,
model: model_name,
enable_thinking: model.supports_thinking(),
effort: model
.default_effort_level()
.map(|effort| effort.value.to_string()),
speed: None,
},
}
}
impl AgentSettings {
pub fn get_layout(cx: &App) -> WindowLayout {
let store = cx.global::<SettingsStore>();
let merged = store.merged_settings();

View file

@ -3842,12 +3842,22 @@ impl ThreadView {
let enable_thinking = !thread.thinking_enabled();
thread.set_thinking_enabled(enable_thinking, cx);
let favorite_key = thread.model().map(|model| {
(model.provider_id().0.to_string(), model.id().0.to_string())
});
let fs = thread.project().read(cx).fs().clone();
update_settings_file(fs, cx, move |settings, _| {
if let Some(agent) = settings.agent.as_mut()
&& let Some(default_model) = agent.default_model.as_mut()
{
default_model.enable_thinking = enable_thinking;
if let Some(agent) = settings.agent.as_mut() {
if let Some(default_model) = agent.default_model.as_mut() {
default_model.enable_thinking = enable_thinking;
}
if let Some((provider_id, model_id)) = &favorite_key {
agent.update_favorite_model(
provider_id,
model_id,
|favorite| favorite.enable_thinking = enable_thinking,
);
}
}
});
});
@ -3978,14 +3988,33 @@ impl ThreadView {
cx,
);
let favorite_key = thread.model().map(|model| {
(
model.provider_id().0.to_string(),
model.id().0.to_string(),
)
});
let fs = thread.project().read(cx).fs().clone();
update_settings_file(fs, cx, move |settings, _| {
if let Some(agent) = settings.agent.as_mut()
&& let Some(default_model) =
if let Some(agent) = settings.agent.as_mut() {
if let Some(default_model) =
agent.default_model.as_mut()
{
default_model.effort =
Some(effort.to_string());
{
default_model.effort =
Some(effort.to_string());
}
if let Some((provider_id, model_id)) =
&favorite_key
{
agent.update_favorite_model(
provider_id,
model_id,
|favorite| {
favorite.effort =
Some(effort.to_string())
},
);
}
}
});
});
@ -8881,12 +8910,20 @@ impl ThreadView {
.unwrap_or(Speed::Fast);
thread.set_speed(new_speed, cx);
let favorite_key = thread
.model()
.map(|model| (model.provider_id().0.to_string(), model.id().0.to_string()));
let fs = thread.project().read(cx).fs().clone();
update_settings_file(fs, cx, move |settings, _| {
if let Some(agent) = settings.agent.as_mut()
&& let Some(default_model) = agent.default_model.as_mut()
{
default_model.speed = Some(new_speed);
if let Some(agent) = settings.agent.as_mut() {
if let Some(default_model) = agent.default_model.as_mut() {
default_model.speed = Some(new_speed);
}
if let Some((provider_id, model_id)) = &favorite_key {
agent.update_favorite_model(provider_id, model_id, |favorite| {
favorite.speed = Some(new_speed)
});
}
}
});
});
@ -8927,12 +8964,20 @@ impl ThreadView {
thread.update(cx, |thread, cx| {
thread.set_thinking_effort(Some(next_effort.clone()), cx);
let favorite_key = thread
.model()
.map(|model| (model.provider_id().0.to_string(), model.id().0.to_string()));
let fs = thread.project().read(cx).fs().clone();
update_settings_file(fs, cx, move |settings, _| {
if let Some(agent) = settings.agent.as_mut()
&& let Some(default_model) = agent.default_model.as_mut()
{
default_model.effort = Some(next_effort);
if let Some(agent) = settings.agent.as_mut() {
if let Some(default_model) = agent.default_model.as_mut() {
default_model.effort = Some(next_effort.clone());
}
if let Some((provider_id, model_id)) = &favorite_key {
agent.update_favorite_model(provider_id, model_id, |favorite| {
favorite.effort = Some(next_effort)
});
}
}
});
});

View file

@ -1,27 +1,27 @@
use std::sync::Arc;
use agent_settings::{AgentSettings, language_model_to_selection};
use fs::Fs;
use language_model::LanguageModel;
use settings::{LanguageModelSelection, update_settings_file};
use settings::{Settings as _, update_settings_file};
use ui::App;
fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelSelection {
LanguageModelSelection {
provider: model.provider_id().to_string().into(),
model: model.id().0.to_string(),
enable_thinking: false,
effort: None,
speed: None,
}
}
pub fn toggle_in_settings(
model: Arc<dyn LanguageModel>,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &mut App,
) {
let selection = language_model_to_selection(&model);
let current_user_selection = AgentSettings::get_global(cx)
.default_model
.as_ref()
.filter(|selection| {
selection.provider.0 == model.provider_id().0.as_ref()
&& selection.model == model.id().0.as_ref()
})
.cloned();
let selection = language_model_to_selection(&model, current_user_selection.as_ref());
update_settings_file(fs, cx, move |settings, _| {
let agent = settings.agent.get_or_insert_default();
if should_be_favorite {

View file

@ -275,13 +275,34 @@ impl AgentSettingsContent {
}
pub fn add_favorite_model(&mut self, model: LanguageModelSelection) {
if !self.favorite_models.contains(&model) {
// Note: this is intentional to not compare using `PartialEq`here.
// Full equality would treat entries that differ just in thinking/effort/speed
// as distinct and silently produce duplicates.
if !self
.favorite_models
.iter()
.any(|m| m.provider == model.provider && m.model == model.model)
{
self.favorite_models.push(model);
}
}
pub fn remove_favorite_model(&mut self, model: &LanguageModelSelection) {
self.favorite_models.retain(|m| m != model);
self.favorite_models
.retain(|m| !(m.provider == model.provider && m.model == model.model));
}
pub fn update_favorite_model<F>(&mut self, provider: &str, model: &str, f: F)
where
F: FnOnce(&mut LanguageModelSelection),
{
if let Some(entry) = self
.favorite_models
.iter_mut()
.find(|m| m.provider.0 == provider && m.model == model)
{
f(entry);
}
}
pub fn set_tool_default_permission(&mut self, tool_id: &str, mode: ToolPermissionMode) {