From 68da244d37694ca649fb3dcccd76169f2e3f8a22 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 20 Apr 2026 22:40:00 +0530 Subject: [PATCH] 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. --- crates/agent/src/agent.rs | 27 +++++-- crates/agent/src/native_agent_server.rs | 46 ++++++++--- crates/agent_settings/src/agent_settings.rs | 41 ++++++++++ .../src/conversation_view/thread_view.rs | 79 +++++++++++++++---- crates/agent_ui/src/favorite_models.rs | 24 +++--- crates/settings_content/src/agent.rs | 25 +++++- 6 files changed, 194 insertions(+), 48 deletions(-) diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 553858881a0144e1808c044592cb5b25b63229e0..160172c5c49d3af5548ccd76af1e441662fadfbb 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -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( diff --git a/crates/agent/src/native_agent_server.rs b/crates/agent/src/native_agent_server.rs index 305c4f51952b3bf7e2771a8372a5975cac9e9385..e11e823caee7ad3b8968372f1b089f053a5fe721 100644 --- a/crates/agent/src/native_agent_server.rs +++ b/crates/agent/src/native_agent_server.rs @@ -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, 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)] diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index a5706752f41324d2ffa5075621f6cb651694df86..e3a52555e70dba1a825889209f2ec7b9c1f689e3 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -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, + 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::(); let merged = store.merged_settings(); diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index ca4b261301f94b5c4299c77ce6ef82af1886bc79..b6bda7738d5424e842d66c4a625fe64684ce5c14 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -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) + }); + } } }); }); diff --git a/crates/agent_ui/src/favorite_models.rs b/crates/agent_ui/src/favorite_models.rs index aa48ca8d12459b2860982a6b204a5350e6c4ce4c..c655f9b6a55aecb7e7ce6e419702e3d0cc6983c2 100644 --- a/crates/agent_ui/src/favorite_models.rs +++ b/crates/agent_ui/src/favorite_models.rs @@ -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) -> 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, should_be_favorite: bool, fs: Arc, 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 { diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs index d5c9c54a222de727ad112779ced518e261714616..12756c9bad5d9bf15ca5d02a977d816e80822d8b 100644 --- a/crates/settings_content/src/agent.rs +++ b/crates/settings_content/src/agent.rs @@ -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(&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) {