Detailed changes
@@ -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",
@@ -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",
@@ -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",
@@ -24,7 +24,7 @@
},
},
{
- "context": "InlineAssistEditor",
+ "context": "InlineAssistant > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-backspace": "editor::Cancel",
@@ -24,7 +24,7 @@
},
},
{
- "context": "InlineAssistEditor",
+ "context": "InlineAssistant > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-backspace": "editor::Cancel",
@@ -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.
@@ -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 {
@@ -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)]
@@ -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 {
@@ -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>,
@@ -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>,
@@ -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>,
@@ -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(),
},
@@ -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");
+ }
+ }
+ }
}
}
@@ -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()
}
});
@@ -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 {
@@ -1370,6 +1370,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
env: Some(HashMap::default()),
default_mode: None,
default_model: None,
+ favorite_models: vec![],
},
);
}
@@ -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,
)
@@ -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);
- }
- });
-}
@@ -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()
@@ -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(
@@ -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()
}
});
@@ -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,
+ )),
+ )
+ })
+ }
+}
@@ -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();
@@ -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>,
},
}