agent_ui: Expand model favoriting feature to external agents (#45528)

Danilo Leal created

This PR adds the ability to favorite models for external agentsβ€”writing
to the settings in the `agent_servers` keyβ€”as well as a handful of other
improvements:

- Make the cycling keybinding `alt-enter` work for the inline assistant
as well as previous user messages
- Better organized the keybinding files removing some outdated
agent-related keybinding definitions
- Renamed the inline assistant key context to "InlineAssistant" as
"PromptEditor" is old and confusing
- Made the keybindings to rate an inline assistant response visible in
the thumbs up/down button's tooltip
- Created a unified component for the model selector tooltip given we
had 3 different places creating the same element
- Make the "Cycle Favorited Models" row in the tooltip visible only if
there is more than one favorite models

Release Notes:

- agent: External agents also now support the favoriting model feature,
which comes with a handy keybinding to cycle through the favorite list.

Change summary

assets/keymaps/default-linux.json                   |  57 +---
assets/keymaps/default-macos.json                   |  58 +---
assets/keymaps/default-windows.json                 |  58 +---
assets/keymaps/linux/cursor.json                    |   2 
assets/keymaps/macos/cursor.json                    |   2 
crates/acp_thread/src/connection.rs                 |   6 
crates/agent/src/agent.rs                           |   4 
crates/agent/src/native_agent_server.rs             |  36 ++
crates/agent_servers/src/agent_servers.rs           |  30 +
crates/agent_servers/src/claude.rs                  |  43 +++
crates/agent_servers/src/codex.rs                   |  43 +++
crates/agent_servers/src/custom.rs                  |  63 ++++
crates/agent_servers/src/e2e_tests.rs               |   2 
crates/agent_ui/src/acp/model_selector.rs           | 197 +++++++++-----
crates/agent_ui/src/acp/model_selector_popover.rs   |  55 ---
crates/agent_ui/src/acp/thread_view.rs              |  62 ++--
crates/agent_ui/src/agent_configuration.rs          |   1 
crates/agent_ui/src/agent_model_selector.rs         |  22 +
crates/agent_ui/src/favorite_models.rs              |  29 --
crates/agent_ui/src/inline_prompt_editor.rs         |  46 ++
crates/agent_ui/src/language_model_selector.rs      |  15 
crates/agent_ui/src/text_thread_editor.rs           |  51 --
crates/agent_ui/src/ui/model_selector_components.rs |  68 ++++
crates/project/src/agent_server_store.rs            |  27 ++
crates/settings/src/settings_content/agent.rs       |  21 +
25 files changed, 612 insertions(+), 386 deletions(-)

Detailed changes

assets/keymaps/default-linux.json πŸ”—

@@ -241,6 +241,7 @@
       "ctrl-alt-l": "agent::OpenRulesLibrary",
       "ctrl-i": "agent::ToggleProfileSelector",
       "ctrl-alt-/": "agent::ToggleModelSelector",
+      "alt-tab": "agent::CycleFavoriteModels",
       "ctrl-shift-j": "agent::ToggleNavigationMenu",
       "ctrl-alt-i": "agent::ToggleOptionsMenu",
       "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
@@ -253,7 +254,6 @@
       "ctrl-y": "agent::AllowOnce",
       "ctrl-alt-y": "agent::AllowAlways",
       "ctrl-alt-z": "agent::RejectOnce",
-      "alt-tab": "agent::CycleFavoriteModels",
     },
   },
   {
@@ -286,31 +286,7 @@
     },
   },
   {
-    "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
-    "bindings": {
-      "enter": "agent::Chat",
-      "ctrl-enter": "agent::ChatWithFollow",
-      "ctrl-i": "agent::ToggleProfileSelector",
-      "shift-ctrl-r": "agent::OpenAgentDiff",
-      "ctrl-shift-y": "agent::KeepAll",
-      "ctrl-shift-n": "agent::RejectAll",
-      "ctrl-shift-v": "agent::PasteRaw",
-    },
-  },
-  {
-    "context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
-    "bindings": {
-      "ctrl-enter": "agent::Chat",
-      "enter": "editor::Newline",
-      "ctrl-i": "agent::ToggleProfileSelector",
-      "shift-ctrl-r": "agent::OpenAgentDiff",
-      "ctrl-shift-y": "agent::KeepAll",
-      "ctrl-shift-n": "agent::RejectAll",
-      "ctrl-shift-v": "agent::PasteRaw",
-    },
-  },
-  {
-    "context": "EditMessageEditor > Editor",
+    "context": "AgentFeedbackMessageEditor > Editor",
     "bindings": {
       "escape": "menu::Cancel",
       "enter": "menu::Confirm",
@@ -318,17 +294,23 @@
     },
   },
   {
-    "context": "AgentFeedbackMessageEditor > Editor",
+    "context": "AcpThread > ModeSelector",
     "bindings": {
-      "escape": "menu::Cancel",
-      "enter": "menu::Confirm",
-      "alt-enter": "editor::Newline",
+      "ctrl-enter": "menu::Confirm",
     },
   },
   {
-    "context": "AcpThread > ModeSelector",
+    "context": "AcpThread > Editor",
+    "use_key_equivalents": true,
     "bindings": {
-      "ctrl-enter": "menu::Confirm",
+      "ctrl-enter": "agent::ChatWithFollow",
+      "ctrl-i": "agent::ToggleProfileSelector",
+      "ctrl-shift-r": "agent::OpenAgentDiff",
+      "ctrl-shift-y": "agent::KeepAll",
+      "ctrl-shift-n": "agent::RejectAll",
+      "ctrl-shift-v": "agent::PasteRaw",
+      "shift-tab": "agent::CycleModeSelector",
+      "alt-tab": "agent::CycleFavoriteModels",
     },
   },
   {
@@ -336,9 +318,6 @@
     "use_key_equivalents": true,
     "bindings": {
       "enter": "agent::Chat",
-      "shift-ctrl-r": "agent::OpenAgentDiff",
-      "ctrl-shift-y": "agent::KeepAll",
-      "ctrl-shift-n": "agent::RejectAll",
     },
   },
   {
@@ -346,11 +325,7 @@
     "use_key_equivalents": true,
     "bindings": {
       "ctrl-enter": "agent::Chat",
-      "shift-ctrl-r": "agent::OpenAgentDiff",
-      "ctrl-shift-y": "agent::KeepAll",
-      "ctrl-shift-n": "agent::RejectAll",
-      "shift-tab": "agent::CycleModeSelector",
-      "alt-tab": "agent::CycleFavoriteModels",
+      "enter": "editor::Newline",
     },
   },
   {
@@ -817,7 +792,7 @@
     },
   },
   {
-    "context": "PromptEditor",
+    "context": "InlineAssistant",
     "bindings": {
       "ctrl-[": "agent::CyclePreviousInlineAssist",
       "ctrl-]": "agent::CycleNextInlineAssist",

assets/keymaps/default-macos.json πŸ”—

@@ -282,6 +282,7 @@
       "cmd-alt-p": "agent::ManageProfiles",
       "cmd-i": "agent::ToggleProfileSelector",
       "cmd-alt-/": "agent::ToggleModelSelector",
+      "alt-tab": "agent::CycleFavoriteModels",
       "cmd-shift-j": "agent::ToggleNavigationMenu",
       "cmd-alt-m": "agent::ToggleOptionsMenu",
       "cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
@@ -294,7 +295,6 @@
       "cmd-y": "agent::AllowOnce",
       "cmd-alt-y": "agent::AllowAlways",
       "cmd-alt-z": "agent::RejectOnce",
-      "alt-tab": "agent::CycleFavoriteModels",
     },
   },
   {
@@ -326,41 +326,6 @@
       "cmd-alt-t": "agent::NewThread",
     },
   },
-  {
-    "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
-    "use_key_equivalents": true,
-    "bindings": {
-      "enter": "agent::Chat",
-      "cmd-enter": "agent::ChatWithFollow",
-      "cmd-i": "agent::ToggleProfileSelector",
-      "shift-ctrl-r": "agent::OpenAgentDiff",
-      "cmd-shift-y": "agent::KeepAll",
-      "cmd-shift-n": "agent::RejectAll",
-      "cmd-shift-v": "agent::PasteRaw",
-    },
-  },
-  {
-    "context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
-    "use_key_equivalents": true,
-    "bindings": {
-      "cmd-enter": "agent::Chat",
-      "enter": "editor::Newline",
-      "cmd-i": "agent::ToggleProfileSelector",
-      "shift-ctrl-r": "agent::OpenAgentDiff",
-      "cmd-shift-y": "agent::KeepAll",
-      "cmd-shift-n": "agent::RejectAll",
-      "cmd-shift-v": "agent::PasteRaw",
-    },
-  },
-  {
-    "context": "EditMessageEditor > Editor",
-    "use_key_equivalents": true,
-    "bindings": {
-      "escape": "menu::Cancel",
-      "enter": "menu::Confirm",
-      "alt-enter": "editor::Newline",
-    },
-  },
   {
     "context": "AgentFeedbackMessageEditor > Editor",
     "use_key_equivalents": true,
@@ -383,27 +348,32 @@
     },
   },
   {
-    "context": "AcpThread > Editor && !use_modifier_to_send",
+    "context": "AcpThread > Editor",
     "use_key_equivalents": true,
     "bindings": {
-      "enter": "agent::Chat",
       "shift-ctrl-r": "agent::OpenAgentDiff",
       "cmd-shift-y": "agent::KeepAll",
       "cmd-shift-n": "agent::RejectAll",
+      "cmd-enter": "agent::ChatWithFollow",
+      "cmd-shift-v": "agent::PasteRaw",
+      "cmd-i": "agent::ToggleProfileSelector",
       "shift-tab": "agent::CycleModeSelector",
       "alt-tab": "agent::CycleFavoriteModels",
     },
   },
+  {
+    "context": "AcpThread > Editor && !use_modifier_to_send",
+    "use_key_equivalents": true,
+    "bindings": {
+      "enter": "agent::Chat",
+    },
+  },
   {
     "context": "AcpThread > Editor && use_modifier_to_send",
     "use_key_equivalents": true,
     "bindings": {
       "cmd-enter": "agent::Chat",
-      "shift-ctrl-r": "agent::OpenAgentDiff",
-      "cmd-shift-y": "agent::KeepAll",
-      "cmd-shift-n": "agent::RejectAll",
-      "shift-tab": "agent::CycleModeSelector",
-      "alt-tab": "agent::CycleFavoriteModels",
+      "enter": "editor::Newline",
     },
   },
   {
@@ -883,7 +853,7 @@
     },
   },
   {
-    "context": "PromptEditor",
+    "context": "InlineAssistant > Editor",
     "use_key_equivalents": true,
     "bindings": {
       "cmd-alt-/": "agent::ToggleModelSelector",

assets/keymaps/default-windows.json πŸ”—

@@ -241,6 +241,7 @@
       "shift-alt-l": "agent::OpenRulesLibrary",
       "shift-alt-p": "agent::ManageProfiles",
       "ctrl-i": "agent::ToggleProfileSelector",
+      "alt-tab": "agent::CycleFavoriteModels",
       "shift-alt-/": "agent::ToggleModelSelector",
       "shift-alt-j": "agent::ToggleNavigationMenu",
       "shift-alt-i": "agent::ToggleOptionsMenu",
@@ -254,7 +255,6 @@
       "shift-alt-a": "agent::AllowOnce",
       "ctrl-alt-y": "agent::AllowAlways",
       "shift-alt-z": "agent::RejectOnce",
-      "alt-tab": "agent::CycleFavoriteModels",
     },
   },
   {
@@ -287,41 +287,6 @@
       "ctrl-alt-t": "agent::NewThread",
     },
   },
-  {
-    "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
-    "use_key_equivalents": true,
-    "bindings": {
-      "enter": "agent::Chat",
-      "ctrl-enter": "agent::ChatWithFollow",
-      "ctrl-i": "agent::ToggleProfileSelector",
-      "ctrl-shift-r": "agent::OpenAgentDiff",
-      "ctrl-shift-y": "agent::KeepAll",
-      "ctrl-shift-n": "agent::RejectAll",
-      "ctrl-shift-v": "agent::PasteRaw",
-    },
-  },
-  {
-    "context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
-    "use_key_equivalents": true,
-    "bindings": {
-      "ctrl-enter": "agent::Chat",
-      "enter": "editor::Newline",
-      "ctrl-i": "agent::ToggleProfileSelector",
-      "ctrl-shift-r": "agent::OpenAgentDiff",
-      "ctrl-shift-y": "agent::KeepAll",
-      "ctrl-shift-n": "agent::RejectAll",
-      "ctrl-shift-v": "agent::PasteRaw",
-    },
-  },
-  {
-    "context": "EditMessageEditor > Editor",
-    "use_key_equivalents": true,
-    "bindings": {
-      "escape": "menu::Cancel",
-      "enter": "menu::Confirm",
-      "alt-enter": "editor::Newline",
-    },
-  },
   {
     "context": "AgentFeedbackMessageEditor > Editor",
     "use_key_equivalents": true,
@@ -338,27 +303,32 @@
     },
   },
   {
-    "context": "AcpThread > Editor && !use_modifier_to_send",
+    "context": "AcpThread > Editor",
     "use_key_equivalents": true,
     "bindings": {
-      "enter": "agent::Chat",
+      "ctrl-enter": "agent::ChatWithFollow",
+      "ctrl-i": "agent::ToggleProfileSelector",
       "ctrl-shift-r": "agent::OpenAgentDiff",
       "ctrl-shift-y": "agent::KeepAll",
       "ctrl-shift-n": "agent::RejectAll",
+      "ctrl-shift-v": "agent::PasteRaw",
       "shift-tab": "agent::CycleModeSelector",
       "alt-tab": "agent::CycleFavoriteModels",
     },
   },
+  {
+    "context": "AcpThread > Editor && !use_modifier_to_send",
+    "use_key_equivalents": true,
+    "bindings": {
+      "enter": "agent::Chat",
+    },
+  },
   {
     "context": "AcpThread > Editor && use_modifier_to_send",
     "use_key_equivalents": true,
     "bindings": {
       "ctrl-enter": "agent::Chat",
-      "ctrl-shift-r": "agent::OpenAgentDiff",
-      "ctrl-shift-y": "agent::KeepAll",
-      "ctrl-shift-n": "agent::RejectAll",
-      "shift-tab": "agent::CycleModeSelector",
-      "alt-tab": "agent::CycleFavoriteModels",
+      "enter": "editor::Newline",
     },
   },
   {
@@ -826,7 +796,7 @@
     },
   },
   {
-    "context": "PromptEditor",
+    "context": "InlineAssistant",
     "use_key_equivalents": true,
     "bindings": {
       "ctrl-[": "agent::CyclePreviousInlineAssist",

assets/keymaps/linux/cursor.json πŸ”—

@@ -24,7 +24,7 @@
     },
   },
   {
-    "context": "InlineAssistEditor",
+    "context": "InlineAssistant > Editor",
     "use_key_equivalents": true,
     "bindings": {
       "ctrl-shift-backspace": "editor::Cancel",

assets/keymaps/macos/cursor.json πŸ”—

@@ -24,7 +24,7 @@
     },
   },
   {
-    "context": "InlineAssistEditor",
+    "context": "InlineAssistant > Editor",
     "use_key_equivalents": true,
     "bindings": {
       "cmd-shift-backspace": "editor::Cancel",

crates/acp_thread/src/connection.rs πŸ”—

@@ -202,12 +202,6 @@ pub trait AgentModelSelector: 'static {
     fn should_render_footer(&self) -> bool {
         false
     }
-
-    /// Whether this selector supports the favorites feature.
-    /// Only the native agent uses the model ID format that maps to settings.
-    fn supports_favorites(&self) -> bool {
-        false
-    }
 }
 
 /// Icon for a model in the model selector.

crates/agent/src/agent.rs πŸ”—

@@ -1167,10 +1167,6 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
     fn should_render_footer(&self) -> bool {
         true
     }
-
-    fn supports_favorites(&self) -> bool {
-        true
-    }
 }
 
 impl acp_thread::AgentConnection for NativeAgentConnection {

crates/agent/src/native_agent_server.rs πŸ”—

@@ -1,10 +1,14 @@
 use std::{any::Any, path::Path, rc::Rc, sync::Arc};
 
+use agent_client_protocol as acp;
 use agent_servers::{AgentServer, AgentServerDelegate};
+use agent_settings::AgentSettings;
 use anyhow::Result;
+use collections::HashSet;
 use fs::Fs;
 use gpui::{App, Entity, SharedString, Task};
 use prompt_store::PromptStore;
+use settings::{LanguageModelSelection, Settings as _, update_settings_file};
 
 use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates};
 
@@ -71,6 +75,38 @@ impl AgentServer for NativeAgentServer {
     fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
         self
     }
+
+    fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
+        AgentSettings::get_global(cx).favorite_model_ids()
+    }
+
+    fn toggle_favorite_model(
+        &self,
+        model_id: acp::ModelId,
+        should_be_favorite: bool,
+        fs: Arc<dyn Fs>,
+        cx: &App,
+    ) {
+        let selection = model_id_to_selection(&model_id);
+        update_settings_file(fs, cx, move |settings, _| {
+            let agent = settings.agent.get_or_insert_default();
+            if should_be_favorite {
+                agent.add_favorite_model(selection.clone());
+            } else {
+                agent.remove_favorite_model(&selection);
+            }
+        });
+    }
+}
+
+/// Convert a ModelId (e.g. "anthropic/claude-3-5-sonnet") to a LanguageModelSelection.
+fn model_id_to_selection(model_id: &acp::ModelId) -> 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(),
+    }
 }
 
 #[cfg(test)]

crates/agent_servers/src/agent_servers.rs πŸ”—

@@ -4,6 +4,8 @@ mod codex;
 mod custom;
 mod gemini;
 
+use collections::HashSet;
+
 #[cfg(any(test, feature = "test-support"))]
 pub mod e2e_tests;
 
@@ -56,9 +58,19 @@ impl AgentServerDelegate {
 pub trait AgentServer: Send {
     fn logo(&self) -> ui::IconName;
     fn name(&self) -> SharedString;
+    fn connect(
+        &self,
+        root_dir: Option<&Path>,
+        delegate: AgentServerDelegate,
+        cx: &mut App,
+    ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
+
     fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
         None
     }
+
     fn set_default_mode(
         &self,
         _mode_id: Option<agent_client_protocol::SessionModeId>,
@@ -79,14 +91,18 @@ pub trait AgentServer: Send {
     ) {
     }
 
-    fn connect(
-        &self,
-        root_dir: Option<&Path>,
-        delegate: AgentServerDelegate,
-        cx: &mut App,
-    ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
+    fn favorite_model_ids(&self, _cx: &mut App) -> HashSet<agent_client_protocol::ModelId> {
+        HashSet::default()
+    }
 
-    fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
+    fn toggle_favorite_model(
+        &self,
+        _model_id: agent_client_protocol::ModelId,
+        _should_be_favorite: bool,
+        _fs: Arc<dyn Fs>,
+        _cx: &App,
+    ) {
+    }
 }
 
 impl dyn AgentServer {

crates/agent_servers/src/claude.rs πŸ”—

@@ -1,4 +1,5 @@
 use agent_client_protocol as acp;
+use collections::HashSet;
 use fs::Fs;
 use settings::{SettingsStore, update_settings_file};
 use std::path::Path;
@@ -72,6 +73,48 @@ impl AgentServer for ClaudeCode {
         });
     }
 
+    fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
+        let settings = cx.read_global(|settings: &SettingsStore, _| {
+            settings.get::<AllAgentServersSettings>(None).claude.clone()
+        });
+
+        settings
+            .as_ref()
+            .map(|s| {
+                s.favorite_models
+                    .iter()
+                    .map(|id| acp::ModelId::new(id.clone()))
+                    .collect()
+            })
+            .unwrap_or_default()
+    }
+
+    fn toggle_favorite_model(
+        &self,
+        model_id: acp::ModelId,
+        should_be_favorite: bool,
+        fs: Arc<dyn Fs>,
+        cx: &App,
+    ) {
+        update_settings_file(fs, cx, move |settings, _| {
+            let favorite_models = &mut settings
+                .agent_servers
+                .get_or_insert_default()
+                .claude
+                .get_or_insert_default()
+                .favorite_models;
+
+            let model_id_str = model_id.to_string();
+            if should_be_favorite {
+                if !favorite_models.contains(&model_id_str) {
+                    favorite_models.push(model_id_str);
+                }
+            } else {
+                favorite_models.retain(|id| id != &model_id_str);
+            }
+        });
+    }
+
     fn connect(
         &self,
         root_dir: Option<&Path>,

crates/agent_servers/src/codex.rs πŸ”—

@@ -5,6 +5,7 @@ use std::{any::Any, path::Path};
 use acp_thread::AgentConnection;
 use agent_client_protocol as acp;
 use anyhow::{Context as _, Result};
+use collections::HashSet;
 use fs::Fs;
 use gpui::{App, AppContext as _, SharedString, Task};
 use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
@@ -73,6 +74,48 @@ impl AgentServer for Codex {
         });
     }
 
+    fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
+        let settings = cx.read_global(|settings: &SettingsStore, _| {
+            settings.get::<AllAgentServersSettings>(None).codex.clone()
+        });
+
+        settings
+            .as_ref()
+            .map(|s| {
+                s.favorite_models
+                    .iter()
+                    .map(|id| acp::ModelId::new(id.clone()))
+                    .collect()
+            })
+            .unwrap_or_default()
+    }
+
+    fn toggle_favorite_model(
+        &self,
+        model_id: acp::ModelId,
+        should_be_favorite: bool,
+        fs: Arc<dyn Fs>,
+        cx: &App,
+    ) {
+        update_settings_file(fs, cx, move |settings, _| {
+            let favorite_models = &mut settings
+                .agent_servers
+                .get_or_insert_default()
+                .codex
+                .get_or_insert_default()
+                .favorite_models;
+
+            let model_id_str = model_id.to_string();
+            if should_be_favorite {
+                if !favorite_models.contains(&model_id_str) {
+                    favorite_models.push(model_id_str);
+                }
+            } else {
+                favorite_models.retain(|id| id != &model_id_str);
+            }
+        });
+    }
+
     fn connect(
         &self,
         root_dir: Option<&Path>,

crates/agent_servers/src/custom.rs πŸ”—

@@ -2,6 +2,7 @@ use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
 use acp_thread::AgentConnection;
 use agent_client_protocol as acp;
 use anyhow::{Context as _, Result};
+use collections::HashSet;
 use fs::Fs;
 use gpui::{App, AppContext as _, SharedString, Task};
 use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName};
@@ -54,6 +55,7 @@ impl AgentServer for CustomAgentServer {
                 .or_insert_with(|| settings::CustomAgentServerSettings::Extension {
                     default_model: None,
                     default_mode: None,
+                    favorite_models: Vec::new(),
                 });
 
             match settings {
@@ -90,6 +92,7 @@ impl AgentServer for CustomAgentServer {
                 .or_insert_with(|| settings::CustomAgentServerSettings::Extension {
                     default_model: None,
                     default_mode: None,
+                    favorite_models: Vec::new(),
                 });
 
             match settings {
@@ -101,6 +104,66 @@ impl AgentServer for CustomAgentServer {
         });
     }
 
+    fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
+        let settings = cx.read_global(|settings: &SettingsStore, _| {
+            settings
+                .get::<AllAgentServersSettings>(None)
+                .custom
+                .get(&self.name())
+                .cloned()
+        });
+
+        settings
+            .as_ref()
+            .map(|s| {
+                s.favorite_models()
+                    .iter()
+                    .map(|id| acp::ModelId::new(id.clone()))
+                    .collect()
+            })
+            .unwrap_or_default()
+    }
+
+    fn toggle_favorite_model(
+        &self,
+        model_id: acp::ModelId,
+        should_be_favorite: bool,
+        fs: Arc<dyn Fs>,
+        cx: &App,
+    ) {
+        let name = self.name();
+        update_settings_file(fs, cx, move |settings, _| {
+            let settings = settings
+                .agent_servers
+                .get_or_insert_default()
+                .custom
+                .entry(name.clone())
+                .or_insert_with(|| settings::CustomAgentServerSettings::Extension {
+                    default_model: None,
+                    default_mode: None,
+                    favorite_models: Vec::new(),
+                });
+
+            let favorite_models = match settings {
+                settings::CustomAgentServerSettings::Custom {
+                    favorite_models, ..
+                }
+                | settings::CustomAgentServerSettings::Extension {
+                    favorite_models, ..
+                } => favorite_models,
+            };
+
+            let model_id_str = model_id.to_string();
+            if should_be_favorite {
+                if !favorite_models.contains(&model_id_str) {
+                    favorite_models.push(model_id_str);
+                }
+            } else {
+                favorite_models.retain(|id| id != &model_id_str);
+            }
+        });
+    }
+
     fn connect(
         &self,
         root_dir: Option<&Path>,

crates/agent_servers/src/e2e_tests.rs πŸ”—

@@ -460,6 +460,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
                     ignore_system_version: None,
                     default_mode: None,
                     default_model: None,
+                    favorite_models: vec![],
                 }),
                 gemini: Some(crate::gemini::tests::local_command().into()),
                 codex: Some(BuiltinAgentServerSettings {
@@ -469,6 +470,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
                     ignore_system_version: None,
                     default_mode: None,
                     default_model: None,
+                    favorite_models: vec![],
                 }),
                 custom: collections::HashMap::default(),
             },

crates/agent_ui/src/acp/model_selector.rs πŸ”—

@@ -3,19 +3,19 @@ use std::{cmp::Reverse, rc::Rc, sync::Arc};
 use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
 use agent_client_protocol::ModelId;
 use agent_servers::AgentServer;
-use agent_settings::AgentSettings;
 use anyhow::Result;
 use collections::{HashSet, IndexMap};
 use fs::Fs;
 use futures::FutureExt;
 use fuzzy::{StringMatchCandidate, match_strings};
 use gpui::{
-    Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
+    Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
+    WeakEntity,
 };
 use itertools::Itertools;
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate};
-use settings::Settings;
+use settings::SettingsStore;
 use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, prelude::*};
 use util::ResultExt;
 use zed_actions::agent::OpenSettings;
@@ -54,7 +54,9 @@ pub struct AcpModelPickerDelegate {
     selected_index: usize,
     selected_description: Option<(usize, SharedString, bool)>,
     selected_model: Option<AgentModelInfo>,
+    favorites: HashSet<ModelId>,
     _refresh_models_task: Task<()>,
+    _settings_subscription: Subscription,
     focus_handle: FocusHandle,
 }
 
@@ -102,6 +104,19 @@ impl AcpModelPickerDelegate {
             })
         };
 
+        let agent_server_for_subscription = agent_server.clone();
+        let settings_subscription =
+            cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
+                // Only refresh if the favorites actually changed to avoid redundant work
+                // when other settings are modified (e.g., user editing settings.json)
+                let new_favorites = agent_server_for_subscription.favorite_model_ids(cx);
+                if new_favorites != picker.delegate.favorites {
+                    picker.delegate.favorites = new_favorites;
+                    picker.refresh(window, cx);
+                }
+            });
+        let favorites = agent_server.favorite_model_ids(cx);
+
         Self {
             selector,
             agent_server,
@@ -111,7 +126,9 @@ impl AcpModelPickerDelegate {
             selected_model: None,
             selected_index: 0,
             selected_description: None,
+            favorites,
             _refresh_models_task: refresh_models_task,
+            _settings_subscription: settings_subscription,
             focus_handle,
         }
     }
@@ -120,40 +137,37 @@ impl AcpModelPickerDelegate {
         self.selected_model.as_ref()
     }
 
-    pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        if !self.selector.supports_favorites() {
-            return;
-        }
-
-        let favorites = AgentSettings::get_global(cx).favorite_model_ids();
+    pub fn favorites_count(&self) -> usize {
+        self.favorites.len()
+    }
 
-        if favorites.is_empty() {
+    pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        if self.favorites.is_empty() {
             return;
         }
 
-        let Some(models) = self.models.clone() else {
+        let Some(models) = &self.models else {
             return;
         };
 
-        let all_models: Vec<AgentModelInfo> = match models {
-            AgentModelList::Flat(list) => list,
-            AgentModelList::Grouped(index_map) => index_map
-                .into_values()
-                .flatten()
-                .collect::<Vec<AgentModelInfo>>(),
+        let all_models: Vec<&AgentModelInfo> = match models {
+            AgentModelList::Flat(list) => list.iter().collect(),
+            AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
         };
 
-        let favorite_models = all_models
-            .iter()
-            .filter(|model| favorites.contains(&model.id))
+        let favorite_models: Vec<_> = all_models
+            .into_iter()
+            .filter(|model| self.favorites.contains(&model.id))
             .unique_by(|model| &model.id)
-            .cloned()
-            .collect::<Vec<_>>();
+            .collect();
+
+        if favorite_models.is_empty() {
+            return;
+        }
 
-        let current_id = self.selected_model.as_ref().map(|m| m.id.clone());
+        let current_id = self.selected_model.as_ref().map(|m| &m.id);
 
         let current_index_in_favorites = current_id
-            .as_ref()
             .and_then(|id| favorite_models.iter().position(|m| &m.id == id))
             .unwrap_or(usize::MAX);
 
@@ -220,11 +234,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Task<()> {
-        let favorites = if self.selector.supports_favorites() {
-            AgentSettings::get_global(cx).favorite_model_ids()
-        } else {
-            Default::default()
-        };
+        let favorites = self.favorites.clone();
 
         cx.spawn_in(window, async move |this, cx| {
             let filtered_models = match this
@@ -317,21 +327,20 @@ impl PickerDelegate for AcpModelPickerDelegate {
                 let default_model = self.agent_server.default_model(cx);
                 let is_default = default_model.as_ref() == Some(&model_info.id);
 
-                let supports_favorites = self.selector.supports_favorites();
-
                 let is_favorite = *is_favorite;
                 let handle_action_click = {
                     let model_id = model_info.id.clone();
                     let fs = self.fs.clone();
+                    let agent_server = self.agent_server.clone();
 
-                    move |cx: &App| {
-                        crate::favorite_models::toggle_model_id_in_settings(
+                    cx.listener(move |_, _, _, cx| {
+                        agent_server.toggle_favorite_model(
                             model_id.clone(),
                             !is_favorite,
                             fs.clone(),
                             cx,
                         );
-                    }
+                    })
                 };
 
                 Some(
@@ -357,10 +366,8 @@ impl PickerDelegate for AcpModelPickerDelegate {
                                 })
                                 .is_selected(is_selected)
                                 .is_focused(selected)
-                                .when(supports_favorites, |this| {
-                                    this.is_favorite(is_favorite)
-                                        .on_toggle_favorite(handle_action_click)
-                                }),
+                                .is_favorite(is_favorite)
+                                .on_toggle_favorite(handle_action_click),
                         )
                         .into_any_element(),
                 )
@@ -603,6 +610,46 @@ mod tests {
             .collect()
     }
 
+    #[gpui::test]
+    async fn test_fuzzy_match(cx: &mut TestAppContext) {
+        let models = create_model_list(vec![
+            (
+                "zed",
+                vec![
+                    "Claude 3.7 Sonnet",
+                    "Claude 3.7 Sonnet Thinking",
+                    "gpt-4.1",
+                    "gpt-4.1-nano",
+                ],
+            ),
+            ("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]),
+            ("ollama", vec!["mistral", "deepseek"]),
+        ]);
+
+        // Results should preserve models order whenever possible.
+        // In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical
+        // similarity scores, but `zed/gpt-4.1` was higher in the models list,
+        // so it should appear first in the results.
+        let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await;
+        assert_models_eq(
+            results,
+            vec![
+                ("zed", vec!["gpt-4.1", "gpt-4.1-nano"]),
+                ("openai", vec!["gpt-4.1", "gpt-4.1-nano"]),
+            ],
+        );
+
+        // Fuzzy search
+        let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await;
+        assert_models_eq(
+            results,
+            vec![
+                ("zed", vec!["gpt-4.1-nano"]),
+                ("openai", vec!["gpt-4.1-nano"]),
+            ],
+        );
+    }
+
     #[gpui::test]
     fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
         let models = create_model_list(vec![
@@ -739,42 +786,48 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_fuzzy_match(cx: &mut TestAppContext) {
-        let models = create_model_list(vec![
-            (
-                "zed",
-                vec![
-                    "Claude 3.7 Sonnet",
-                    "Claude 3.7 Sonnet Thinking",
-                    "gpt-4.1",
-                    "gpt-4.1-nano",
-                ],
-            ),
-            ("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]),
-            ("ollama", vec!["mistral", "deepseek"]),
+    fn test_favorites_count_returns_correct_count(_cx: &mut TestAppContext) {
+        let empty_favorites: HashSet<ModelId> = HashSet::default();
+        assert_eq!(empty_favorites.len(), 0);
+
+        let one_favorite = create_favorites(vec!["model-a"]);
+        assert_eq!(one_favorite.len(), 1);
+
+        let multiple_favorites = create_favorites(vec!["model-a", "model-b", "model-c"]);
+        assert_eq!(multiple_favorites.len(), 3);
+
+        let with_duplicates = create_favorites(vec!["model-a", "model-a", "model-b"]);
+        assert_eq!(with_duplicates.len(), 2);
+    }
+
+    #[gpui::test]
+    fn test_is_favorite_flag_set_correctly_in_entries(_cx: &mut TestAppContext) {
+        let models = AgentModelList::Flat(vec![
+            acp_thread::AgentModelInfo {
+                id: acp::ModelId::new("favorite-model".to_string()),
+                name: "Favorite".into(),
+                description: None,
+                icon: None,
+            },
+            acp_thread::AgentModelInfo {
+                id: acp::ModelId::new("regular-model".to_string()),
+                name: "Regular".into(),
+                description: None,
+                icon: None,
+            },
         ]);
+        let favorites = create_favorites(vec!["favorite-model"]);
 
-        // Results should preserve models order whenever possible.
-        // In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical
-        // similarity scores, but `zed/gpt-4.1` was higher in the models list,
-        // so it should appear first in the results.
-        let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await;
-        assert_models_eq(
-            results,
-            vec![
-                ("zed", vec!["gpt-4.1", "gpt-4.1-nano"]),
-                ("openai", vec!["gpt-4.1", "gpt-4.1-nano"]),
-            ],
-        );
+        let entries = info_list_to_picker_entries(models, &favorites);
 
-        // Fuzzy search
-        let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await;
-        assert_models_eq(
-            results,
-            vec![
-                ("zed", vec!["gpt-4.1-nano"]),
-                ("openai", vec!["gpt-4.1-nano"]),
-            ],
-        );
+        for entry in &entries {
+            if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
+                if info.id.0.as_ref() == "favorite-model" {
+                    assert!(*is_favorite, "favorite-model should have is_favorite=true");
+                } else if info.id.0.as_ref() == "regular-model" {
+                    assert!(!*is_favorite, "regular-model should have is_favorite=false");
+                }
+            }
+        }
     }
 }

crates/agent_ui/src/acp/model_selector_popover.rs πŸ”—

@@ -2,17 +2,13 @@ use std::rc::Rc;
 use std::sync::Arc;
 
 use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
-use agent_servers::AgentServer;
-use agent_settings::AgentSettings;
 use fs::Fs;
 use gpui::{Entity, FocusHandle};
 use picker::popover_menu::PickerPopoverMenu;
-use settings::Settings as _;
-use ui::{ButtonLike, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
-use zed_actions::agent::ToggleModelSelector;
+use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
 
-use crate::CycleFavoriteModels;
 use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
+use crate::ui::ModelSelectorTooltip;
 
 pub struct AcpModelSelectorPopover {
     selector: Entity<AcpModelSelector>,
@@ -23,7 +19,7 @@ pub struct AcpModelSelectorPopover {
 impl AcpModelSelectorPopover {
     pub(crate) fn new(
         selector: Rc<dyn AgentModelSelector>,
-        agent_server: Rc<dyn AgentServer>,
+        agent_server: Rc<dyn agent_servers::AgentServer>,
         fs: Arc<dyn Fs>,
         menu_handle: PopoverMenuHandle<AcpModelSelector>,
         focus_handle: FocusHandle,
@@ -64,7 +60,8 @@ impl AcpModelSelectorPopover {
 
 impl Render for AcpModelSelectorPopover {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let model = self.selector.read(cx).delegate.active_model();
+        let selector = self.selector.read(cx);
+        let model = selector.delegate.active_model();
         let model_name = model
             .as_ref()
             .map(|model| model.name.clone())
@@ -80,43 +77,13 @@ impl Render for AcpModelSelectorPopover {
             (Color::Muted, IconName::ChevronDown)
         };
 
-        let tooltip = Tooltip::element({
-            move |_, cx| {
-                let focus_handle = focus_handle.clone();
-                let should_show_cycle_row = !AgentSettings::get_global(cx)
-                    .favorite_model_ids()
-                    .is_empty();
+        let show_cycle_row = selector.delegate.favorites_count() > 1;
 
-                v_flex()
-                    .gap_1()
-                    .child(
-                        h_flex()
-                            .gap_2()
-                            .justify_between()
-                            .child(Label::new("Change Model"))
-                            .child(KeyBinding::for_action_in(
-                                &ToggleModelSelector,
-                                &focus_handle,
-                                cx,
-                            )),
-                    )
-                    .when(should_show_cycle_row, |this| {
-                        this.child(
-                            h_flex()
-                                .pt_1()
-                                .gap_2()
-                                .border_t_1()
-                                .border_color(cx.theme().colors().border_variant)
-                                .justify_between()
-                                .child(Label::new("Cycle Favorited Models"))
-                                .child(KeyBinding::for_action_in(
-                                    &CycleFavoriteModels,
-                                    &focus_handle,
-                                    cx,
-                                )),
-                        )
-                    })
-                    .into_any()
+        let tooltip = Tooltip::element({
+            move |_, _cx| {
+                ModelSelectorTooltip::new(focus_handle.clone())
+                    .show_cycle_row(show_cycle_row)
+                    .into_any_element()
             }
         });
 

crates/agent_ui/src/acp/thread_view.rs πŸ”—

@@ -4288,37 +4288,6 @@ impl AcpThreadView {
 
         v_flex()
             .on_action(cx.listener(Self::expand_message_editor))
-            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
-                if let Some(profile_selector) = this.profile_selector.as_ref() {
-                    profile_selector.read(cx).menu_handle().toggle(window, cx);
-                } else if let Some(mode_selector) = this.mode_selector() {
-                    mode_selector.read(cx).menu_handle().toggle(window, cx);
-                }
-            }))
-            .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
-                if let Some(profile_selector) = this.profile_selector.as_ref() {
-                    profile_selector.update(cx, |profile_selector, cx| {
-                        profile_selector.cycle_profile(cx);
-                    });
-                } else if let Some(mode_selector) = this.mode_selector() {
-                    mode_selector.update(cx, |mode_selector, cx| {
-                        mode_selector.cycle_mode(window, cx);
-                    });
-                }
-            }))
-            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
-                if let Some(model_selector) = this.model_selector.as_ref() {
-                    model_selector
-                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
-                }
-            }))
-            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
-                if let Some(model_selector) = this.model_selector.as_ref() {
-                    model_selector.update(cx, |model_selector, cx| {
-                        model_selector.cycle_favorite_models(window, cx);
-                    });
-                }
-            }))
             .p_2()
             .gap_2()
             .border_t_1()
@@ -6005,6 +5974,37 @@ impl Render for AcpThreadView {
             .on_action(cx.listener(Self::allow_always))
             .on_action(cx.listener(Self::allow_once))
             .on_action(cx.listener(Self::reject_once))
+            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
+                if let Some(profile_selector) = this.profile_selector.as_ref() {
+                    profile_selector.read(cx).menu_handle().toggle(window, cx);
+                } else if let Some(mode_selector) = this.mode_selector() {
+                    mode_selector.read(cx).menu_handle().toggle(window, cx);
+                }
+            }))
+            .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
+                if let Some(profile_selector) = this.profile_selector.as_ref() {
+                    profile_selector.update(cx, |profile_selector, cx| {
+                        profile_selector.cycle_profile(cx);
+                    });
+                } else if let Some(mode_selector) = this.mode_selector() {
+                    mode_selector.update(cx, |mode_selector, cx| {
+                        mode_selector.cycle_mode(window, cx);
+                    });
+                }
+            }))
+            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
+                if let Some(model_selector) = this.model_selector.as_ref() {
+                    model_selector
+                        .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
+                }
+            }))
+            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
+                if let Some(model_selector) = this.model_selector.as_ref() {
+                    model_selector.update(cx, |model_selector, cx| {
+                        model_selector.cycle_favorite_models(window, cx);
+                    });
+                }
+            }))
             .track_focus(&self.focus_handle)
             .bg(cx.theme().colors().panel_background)
             .child(match &self.thread_state {

crates/agent_ui/src/agent_model_selector.rs πŸ”—

@@ -1,6 +1,7 @@
 use crate::{
     ModelUsageContext,
     language_model_selector::{LanguageModelSelector, language_model_selector},
+    ui::ModelSelectorTooltip,
 };
 use fs::Fs;
 use gpui::{Entity, FocusHandle, SharedString};
@@ -9,7 +10,6 @@ use picker::popover_menu::PickerPopoverMenu;
 use settings::update_settings_file;
 use std::sync::Arc;
 use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
-use zed_actions::agent::ToggleModelSelector;
 
 pub struct AgentModelSelector {
     selector: Entity<LanguageModelSelector>,
@@ -81,6 +81,12 @@ impl AgentModelSelector {
     pub fn active_model(&self, cx: &App) -> Option<language_model::ConfiguredModel> {
         self.selector.read(cx).delegate.active_model(cx)
     }
+
+    pub fn cycle_favorite_models(&self, window: &mut Window, cx: &mut Context<Self>) {
+        self.selector.update(cx, |selector, cx| {
+            selector.delegate.cycle_favorite_models(window, cx);
+        });
+    }
 }
 
 impl Render for AgentModelSelector {
@@ -98,8 +104,18 @@ impl Render for AgentModelSelector {
             Color::Muted
         };
 
+        let show_cycle_row = self.selector.read(cx).delegate.favorites_count() > 1;
+
         let focus_handle = self.focus_handle.clone();
 
+        let tooltip = Tooltip::element({
+            move |_, _cx| {
+                ModelSelectorTooltip::new(focus_handle.clone())
+                    .show_cycle_row(show_cycle_row)
+                    .into_any_element()
+            }
+        });
+
         PickerPopoverMenu::new(
             self.selector.clone(),
             ButtonLike::new("active-model")
@@ -125,9 +141,7 @@ impl Render for AgentModelSelector {
                         .color(color)
                         .size(IconSize::XSmall),
                 ),
-            move |_window, cx| {
-                Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
-            },
+            tooltip,
             gpui::Corner::TopRight,
             cx,
         )

crates/agent_ui/src/favorite_models.rs πŸ”—

@@ -1,6 +1,5 @@
 use std::sync::Arc;
 
-use agent_client_protocol::ModelId;
 use fs::Fs;
 use language_model::LanguageModel;
 use settings::{LanguageModelSelection, update_settings_file};
@@ -13,20 +12,11 @@ fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelS
     }
 }
 
-fn model_id_to_selection(model_id: &ModelId) -> 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(),
-    }
-}
-
 pub fn toggle_in_settings(
     model: Arc<dyn LanguageModel>,
     should_be_favorite: bool,
     fs: Arc<dyn Fs>,
-    cx: &App,
+    cx: &mut App,
 ) {
     let selection = language_model_to_selection(&model);
     update_settings_file(fs, cx, move |settings, _| {
@@ -38,20 +28,3 @@ pub fn toggle_in_settings(
         }
     });
 }
-
-pub fn toggle_model_id_in_settings(
-    model_id: ModelId,
-    should_be_favorite: bool,
-    fs: Arc<dyn Fs>,
-    cx: &App,
-) {
-    let selection = model_id_to_selection(&model_id);
-    update_settings_file(fs, cx, move |settings, _| {
-        let agent = settings.agent.get_or_insert_default();
-        if should_be_favorite {
-            agent.add_favorite_model(selection.clone());
-        } else {
-            agent.remove_favorite_model(&selection);
-        }
-    });
-}

crates/agent_ui/src/inline_prompt_editor.rs πŸ”—

@@ -40,7 +40,9 @@ use crate::completion_provider::{
 use crate::mention_set::paste_images_as_context;
 use crate::mention_set::{MentionSet, crease_for_mention};
 use crate::terminal_codegen::TerminalCodegen;
-use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
+use crate::{
+    CycleFavoriteModels, CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext,
+};
 
 actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]);
 
@@ -148,7 +150,7 @@ impl<T: 'static> Render for PromptEditor<T> {
             .into_any_element();
 
         v_flex()
-            .key_context("PromptEditor")
+            .key_context("InlineAssistant")
             .capture_action(cx.listener(Self::paste))
             .block_mouse_except_scroll()
             .size_full()
@@ -162,10 +164,6 @@ impl<T: 'static> Render for PromptEditor<T> {
             .bg(cx.theme().colors().editor_background)
             .child(
                 h_flex()
-                    .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
-                        this.model_selector
-                            .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
-                    }))
                     .on_action(cx.listener(Self::confirm))
                     .on_action(cx.listener(Self::cancel))
                     .on_action(cx.listener(Self::move_up))
@@ -174,6 +172,15 @@ impl<T: 'static> Render for PromptEditor<T> {
                     .on_action(cx.listener(Self::thumbs_down))
                     .capture_action(cx.listener(Self::cycle_prev))
                     .capture_action(cx.listener(Self::cycle_next))
+                    .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
+                        this.model_selector
+                            .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
+                    }))
+                    .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
+                        this.model_selector.update(cx, |model_selector, cx| {
+                            model_selector.cycle_favorite_models(window, cx);
+                        });
+                    }))
                     .child(
                         WithRemSize::new(ui_font_size)
                             .h_full()
@@ -855,7 +862,7 @@ impl<T: 'static> PromptEditor<T> {
                                         .map(|this| {
                                             if rated {
                                                 this.disabled(true)
-                                                    .icon_color(Color::Ignored)
+                                                    .icon_color(Color::Disabled)
                                                     .tooltip(move |_, cx| {
                                                         Tooltip::with_meta(
                                                             "Good Result",
@@ -865,8 +872,15 @@ impl<T: 'static> PromptEditor<T> {
                                                         )
                                                     })
                                             } else {
-                                                this.icon_color(Color::Muted)
-                                                    .tooltip(Tooltip::text("Good Result"))
+                                                this.icon_color(Color::Muted).tooltip(
+                                                    move |_, cx| {
+                                                        Tooltip::for_action(
+                                                            "Good Result",
+                                                            &ThumbsUpResult,
+                                                            cx,
+                                                        )
+                                                    },
+                                                )
                                             }
                                         })
                                         .on_click(cx.listener(|this, _, window, cx| {
@@ -879,7 +893,7 @@ impl<T: 'static> PromptEditor<T> {
                                         .map(|this| {
                                             if rated {
                                                 this.disabled(true)
-                                                    .icon_color(Color::Ignored)
+                                                    .icon_color(Color::Disabled)
                                                     .tooltip(move |_, cx| {
                                                         Tooltip::with_meta(
                                                             "Bad Result",
@@ -889,8 +903,15 @@ impl<T: 'static> PromptEditor<T> {
                                                         )
                                                     })
                                             } else {
-                                                this.icon_color(Color::Muted)
-                                                    .tooltip(Tooltip::text("Bad Result"))
+                                                this.icon_color(Color::Muted).tooltip(
+                                                    move |_, cx| {
+                                                        Tooltip::for_action(
+                                                            "Bad Result",
+                                                            &ThumbsDownResult,
+                                                            cx,
+                                                        )
+                                                    },
+                                                )
                                             }
                                         })
                                         .on_click(cx.listener(|this, _, window, cx| {
@@ -1088,7 +1109,6 @@ impl<T: 'static> PromptEditor<T> {
         let colors = cx.theme().colors();
 
         div()
-            .key_context("InlineAssistEditor")
             .size_full()
             .p_2()
             .pl_1()

crates/agent_ui/src/language_model_selector.rs πŸ”—

@@ -20,14 +20,14 @@ use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem}
 
 type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
 type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
-type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &App) + 'static>;
+type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static>;
 
 pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
 
 pub fn language_model_selector(
     get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
     on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
-    on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
+    on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
     popover_styles: bool,
     focus_handle: FocusHandle,
     window: &mut Window,
@@ -133,7 +133,7 @@ impl LanguageModelPickerDelegate {
     fn new(
         get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
         on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
-        on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
+        on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
         popover_styles: bool,
         focus_handle: FocusHandle,
         window: &mut Window,
@@ -250,6 +250,10 @@ impl LanguageModelPickerDelegate {
         (self.get_active_model)(cx)
     }
 
+    pub fn favorites_count(&self) -> usize {
+        self.all_models.favorites.len()
+    }
+
     pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
         if self.all_models.favorites.is_empty() {
             return;
@@ -561,7 +565,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
                 let handle_action_click = {
                     let model = model_info.model.clone();
                     let on_toggle_favorite = self.on_toggle_favorite.clone();
-                    move |cx: &App| on_toggle_favorite(model.clone(), !is_favorite, cx)
+                    cx.listener(move |picker, _, window, cx| {
+                        on_toggle_favorite(model.clone(), !is_favorite, cx);
+                        picker.refresh(window, cx);
+                    })
                 };
 
                 Some(

crates/agent_ui/src/text_thread_editor.rs πŸ”—

@@ -1,8 +1,8 @@
 use crate::{
     language_model_selector::{LanguageModelSelector, language_model_selector},
-    ui::BurnModeTooltip,
+    ui::{BurnModeTooltip, ModelSelectorTooltip},
 };
-use agent_settings::{AgentSettings, CompletionMode};
+use agent_settings::CompletionMode;
 use anyhow::Result;
 use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
 use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
@@ -2252,43 +2252,18 @@ impl TextThreadEditor {
         .color(color)
         .size(IconSize::XSmall);
 
+        let show_cycle_row = self
+            .language_model_selector
+            .read(cx)
+            .delegate
+            .favorites_count()
+            > 1;
+
         let tooltip = Tooltip::element({
-            move |_, cx| {
-                let focus_handle = focus_handle.clone();
-                let should_show_cycle_row = !AgentSettings::get_global(cx)
-                    .favorite_model_ids()
-                    .is_empty();
-
-                v_flex()
-                    .gap_1()
-                    .child(
-                        h_flex()
-                            .gap_2()
-                            .justify_between()
-                            .child(Label::new("Change Model"))
-                            .child(KeyBinding::for_action_in(
-                                &ToggleModelSelector,
-                                &focus_handle,
-                                cx,
-                            )),
-                    )
-                    .when(should_show_cycle_row, |this| {
-                        this.child(
-                            h_flex()
-                                .pt_1()
-                                .gap_2()
-                                .border_t_1()
-                                .border_color(cx.theme().colors().border_variant)
-                                .justify_between()
-                                .child(Label::new("Cycle Favorited Models"))
-                                .child(KeyBinding::for_action_in(
-                                    &CycleFavoriteModels,
-                                    &focus_handle,
-                                    cx,
-                                )),
-                        )
-                    })
-                    .into_any()
+            move |_, _cx| {
+                ModelSelectorTooltip::new(focus_handle.clone())
+                    .show_cycle_row(show_cycle_row)
+                    .into_any_element()
             }
         });
 

crates/agent_ui/src/ui/model_selector_components.rs πŸ”—

@@ -1,5 +1,8 @@
-use gpui::{Action, FocusHandle, prelude::*};
+use gpui::{Action, ClickEvent, FocusHandle, prelude::*};
 use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
+use zed_actions::agent::ToggleModelSelector;
+
+use crate::CycleFavoriteModels;
 
 enum ModelIcon {
     Name(IconName),
@@ -48,7 +51,7 @@ pub struct ModelSelectorListItem {
     is_selected: bool,
     is_focused: bool,
     is_favorite: bool,
-    on_toggle_favorite: Option<Box<dyn Fn(&App) + 'static>>,
+    on_toggle_favorite: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
 }
 
 impl ModelSelectorListItem {
@@ -89,7 +92,10 @@ impl ModelSelectorListItem {
         self
     }
 
-    pub fn on_toggle_favorite(mut self, handler: impl Fn(&App) + 'static) -> Self {
+    pub fn on_toggle_favorite(
+        mut self,
+        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+    ) -> Self {
         self.on_toggle_favorite = Some(Box::new(handler));
         self
     }
@@ -141,7 +147,7 @@ impl RenderOnce for ModelSelectorListItem {
                             .icon_color(color)
                             .icon_size(IconSize::Small)
                             .tooltip(Tooltip::text(tooltip))
-                            .on_click(move |_, _, cx| (handle_click)(cx)),
+                            .on_click(move |event, window, cx| (handle_click)(event, window, cx)),
                     )
                 }
             }))
@@ -187,3 +193,57 @@ impl RenderOnce for ModelSelectorFooter {
             )
     }
 }
+
+#[derive(IntoElement)]
+pub struct ModelSelectorTooltip {
+    focus_handle: FocusHandle,
+    show_cycle_row: bool,
+}
+
+impl ModelSelectorTooltip {
+    pub fn new(focus_handle: FocusHandle) -> Self {
+        Self {
+            focus_handle,
+            show_cycle_row: true,
+        }
+    }
+
+    pub fn show_cycle_row(mut self, show: bool) -> Self {
+        self.show_cycle_row = show;
+        self
+    }
+}
+
+impl RenderOnce for ModelSelectorTooltip {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        v_flex()
+            .gap_1()
+            .child(
+                h_flex()
+                    .gap_2()
+                    .justify_between()
+                    .child(Label::new("Change Model"))
+                    .child(KeyBinding::for_action_in(
+                        &ToggleModelSelector,
+                        &self.focus_handle,
+                        cx,
+                    )),
+            )
+            .when(self.show_cycle_row, |this| {
+                this.child(
+                    h_flex()
+                        .pt_1()
+                        .gap_2()
+                        .border_t_1()
+                        .border_color(cx.theme().colors().border_variant)
+                        .justify_between()
+                        .child(Label::new("Cycle Favorited Models"))
+                        .child(KeyBinding::for_action_in(
+                            &CycleFavoriteModels,
+                            &self.focus_handle,
+                            cx,
+                        )),
+                )
+            })
+    }
+}

crates/project/src/agent_server_store.rs πŸ”—

@@ -1868,6 +1868,7 @@ pub struct BuiltinAgentServerSettings {
     pub ignore_system_version: Option<bool>,
     pub default_mode: Option<String>,
     pub default_model: Option<String>,
+    pub favorite_models: Vec<String>,
 }
 
 impl BuiltinAgentServerSettings {
@@ -1891,6 +1892,7 @@ impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
             ignore_system_version: value.ignore_system_version,
             default_mode: value.default_mode,
             default_model: value.default_model,
+            favorite_models: value.favorite_models,
         }
     }
 }
@@ -1922,6 +1924,10 @@ pub enum CustomAgentServerSettings {
         ///
         /// Default: None
         default_model: Option<String>,
+        /// The favorite models for this agent.
+        ///
+        /// Default: []
+        favorite_models: Vec<String>,
     },
     Extension {
         /// The default mode to use for this agent.
@@ -1936,6 +1942,10 @@ pub enum CustomAgentServerSettings {
         ///
         /// Default: None
         default_model: Option<String>,
+        /// The favorite models for this agent.
+        ///
+        /// Default: []
+        favorite_models: Vec<String>,
     },
 }
 
@@ -1962,6 +1972,17 @@ impl CustomAgentServerSettings {
             }
         }
     }
+
+    pub fn favorite_models(&self) -> &[String] {
+        match self {
+            CustomAgentServerSettings::Custom {
+                favorite_models, ..
+            }
+            | CustomAgentServerSettings::Extension {
+                favorite_models, ..
+            } => favorite_models,
+        }
+    }
 }
 
 impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
@@ -1973,6 +1994,7 @@ impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
                 env,
                 default_mode,
                 default_model,
+                favorite_models,
             } => CustomAgentServerSettings::Custom {
                 command: AgentServerCommand {
                     path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
@@ -1981,13 +2003,16 @@ impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
                 },
                 default_mode,
                 default_model,
+                favorite_models,
             },
             settings::CustomAgentServerSettings::Extension {
                 default_mode,
                 default_model,
+                favorite_models,
             } => CustomAgentServerSettings::Extension {
                 default_mode,
                 default_model,
+                favorite_models,
             },
         }
     }
@@ -2313,6 +2338,7 @@ mod extension_agent_tests {
             ignore_system_version: None,
             default_mode: None,
             default_model: None,
+            favorite_models: vec![],
         };
 
         let BuiltinAgentServerSettings { path, .. } = settings.into();
@@ -2329,6 +2355,7 @@ mod extension_agent_tests {
             env: None,
             default_mode: None,
             default_model: None,
+            favorite_models: vec![],
         };
 
         let converted: CustomAgentServerSettings = settings.into();

crates/settings/src/settings_content/agent.rs πŸ”—

@@ -363,6 +363,13 @@ pub struct BuiltinAgentServerSettings {
     ///
     /// Default: None
     pub default_model: Option<String>,
+    /// The favorite models for this agent.
+    ///
+    /// These are the model IDs as reported by the agent.
+    ///
+    /// Default: []
+    #[serde(default)]
+    pub favorite_models: Vec<String>,
 }
 
 #[with_fallible_options]
@@ -387,6 +394,13 @@ pub enum CustomAgentServerSettings {
         ///
         /// Default: None
         default_model: Option<String>,
+        /// The favorite models for this agent.
+        ///
+        /// These are the model IDs as reported by the agent.
+        ///
+        /// Default: []
+        #[serde(default)]
+        favorite_models: Vec<String>,
     },
     Extension {
         /// The default mode to use for this agent.
@@ -401,5 +415,12 @@ pub enum CustomAgentServerSettings {
         ///
         /// Default: None
         default_model: Option<String>,
+        /// The favorite models for this agent.
+        ///
+        /// These are the model IDs as reported by the agent.
+        ///
+        /// Default: []
+        #[serde(default)]
+        favorite_models: Vec<String>,
     },
 }