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

Smit Barmase created

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.

Change summary

crates/agent/src/agent.rs                            | 27 +++-
crates/agent/src/native_agent_server.rs              | 46 ++++++-
crates/agent_settings/src/agent_settings.rs          | 41 +++++++
crates/agent_ui/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(-)

Detailed changes

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(

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<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)]

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<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();

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)
+                        });
+                    }
                 }
             });
         });

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<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 {

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<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) {