diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 465c7d86aeaff23bdebe65792304ac2963edaaa7..e2a0bd89b54110209777857e8690ab123d92e3bb 100644 --- a/assets/keymaps/default-linux.json +++ b/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", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 7ff00c41d5d6108b2a0b9fa0de85c511fab1f6e0..2524d98e3778684080775f00a82d0764bfd0361b 100644 --- a/assets/keymaps/default-macos.json +++ b/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", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 445933c950cbc9ef72eb2cca90ab8115471f1e6f..039ae408219909006e5d84bb12822444330acd83 100644 --- a/assets/keymaps/default-windows.json +++ b/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", diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json index 58a7309cf902a3f69f949830cace2200f41fb0fe..e1eeade9db16d178fb2ce0ec4b2ec03f0ac2c221 100644 --- a/assets/keymaps/linux/cursor.json +++ b/assets/keymaps/linux/cursor.json @@ -24,7 +24,7 @@ }, }, { - "context": "InlineAssistEditor", + "context": "InlineAssistant > Editor", "use_key_equivalents": true, "bindings": { "ctrl-shift-backspace": "editor::Cancel", diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json index 93e259db37ac718d2e0258d83e4de436a0a378fd..2824575a445ad0c870a59cb516441dc6f1421f31 100644 --- a/assets/keymaps/macos/cursor.json +++ b/assets/keymaps/macos/cursor.json @@ -24,7 +24,7 @@ }, }, { - "context": "InlineAssistEditor", + "context": "InlineAssistant > Editor", "use_key_equivalents": true, "bindings": { "cmd-shift-backspace": "editor::Cancel", diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 598d0428174eb2fc124739a18ddeff1098521cb7..fa15a339f7db67a90144b645177f1146c97334b4 100644 --- a/crates/acp_thread/src/connection.rs +++ b/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. diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 4baa7f4ea4004d2137b5cddb255346fa91523091..612360fe887e11859635844c79ee5cf25515c2a1 100644 --- a/crates/agent/src/agent.rs +++ b/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 { diff --git a/crates/agent/src/native_agent_server.rs b/crates/agent/src/native_agent_server.rs index a9ade8141a678329e0dd8dad9808e55eee3c382b..95312fd32536b99059e2ebc6ebd0a9ea522f94be 100644 --- a/crates/agent/src/native_agent_server.rs +++ b/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) -> Rc { self } + + fn favorite_model_ids(&self, cx: &mut App) -> HashSet { + AgentSettings::get_global(cx).favorite_model_ids() + } + + fn toggle_favorite_model( + &self, + model_id: acp::ModelId, + should_be_favorite: bool, + fs: Arc, + 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)] diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 46e8508e44f07e4fb3d613e30387d5afd3f38423..c6e66688dd6af6748a97dcd4569827fd7fa32493 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/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, Option)>>; + + fn into_any(self: Rc) -> Rc; + fn default_mode(&self, _cx: &mut App) -> Option { None } + fn set_default_mode( &self, _mode_id: Option, @@ -79,14 +91,18 @@ pub trait AgentServer: Send { ) { } - fn connect( - &self, - root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task, Option)>>; + fn favorite_model_ids(&self, _cx: &mut App) -> HashSet { + HashSet::default() + } - fn into_any(self: Rc) -> Rc; + fn toggle_favorite_model( + &self, + _model_id: agent_client_protocol::ModelId, + _should_be_favorite: bool, + _fs: Arc, + _cx: &App, + ) { + } } impl dyn AgentServer { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index e67ddd5c0698758fdec7c7796b26a1351e9990e5..30ef39af953e66fc983c3d7f189042b6577e84c0 100644 --- a/crates/agent_servers/src/claude.rs +++ b/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 { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(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, + 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>, diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs index c2b308e48b7a984b0374272c0059286e933916b3..15dc4688294da979149f331ea52e8163ae5d3093 100644 --- a/crates/agent_servers/src/codex.rs +++ b/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 { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(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, + 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>, diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index 6b981ce8b8198b275e5d9aa05b6fb66431d22e08..f58948190266adeb0e5509d2ec2825a48d503f50 100644 --- a/crates/agent_servers/src/custom.rs +++ b/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 { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings + .get::(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, + 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>, diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 9db7535b5e55d88d6856774c20365bbac46fc81e..975bb7e373c5ddd13df6c6bc951096fced668355 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -460,6 +460,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { 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 { ignore_system_version: None, default_mode: None, default_model: None, + favorite_models: vec![], }), custom: collections::HashMap::default(), }, diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs index 903d5fe425d99389aae0e2a8028d9a31b986fbb3..c8ed636e4fead9f10e4763904e17d36a5eb6bbb6 100644 --- a/crates/agent_ui/src/acp/model_selector.rs +++ b/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, + favorites: HashSet, _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::(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>) { - 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>) { + if self.favorites.is_empty() { return; } - let Some(models) = self.models.clone() else { + let Some(models) = &self.models else { return; }; - let all_models: Vec = match models { - AgentModelList::Flat(list) => list, - AgentModelList::Grouped(index_map) => index_map - .into_values() - .flatten() - .collect::>(), + 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::>(); + .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>, ) -> 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 = 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"); + } + } + } } } diff --git a/crates/agent_ui/src/acp/model_selector_popover.rs b/crates/agent_ui/src/acp/model_selector_popover.rs index a15c01445dd8e9845f6744e795ed90a1ede6c7fc..34b77704cc87c963fff16ca9f90959dbe0f6d35f 100644 --- a/crates/agent_ui/src/acp/model_selector_popover.rs +++ b/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, @@ -23,7 +19,7 @@ pub struct AcpModelSelectorPopover { impl AcpModelSelectorPopover { pub(crate) fn new( selector: Rc, - agent_server: Rc, + agent_server: Rc, fs: Arc, menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, @@ -64,7 +60,8 @@ impl AcpModelSelectorPopover { impl Render for AcpModelSelectorPopover { fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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() } }); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 72d5536ce28641d6a7b830346542beece52bf6e0..709217fe9f8e532aafa8ac8426473c6c5dacb93d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/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 { diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 562976453d963db65f9033536e528000de2b510f..fb2d50863c002cba0c7b0d63c2a5a4cc73224b4d 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -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![], }, ); } diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs index 45cefbf2b9f8d4b1639a9849f2ee2e4468e530b1..caeba3d26d892aefbb879f6a4e0c9dad603e478f 100644 --- a/crates/agent_ui/src/agent_model_selector.rs +++ b/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, @@ -81,6 +81,12 @@ impl AgentModelSelector { pub fn active_model(&self, cx: &App) -> Option { self.selector.read(cx).delegate.active_model(cx) } + + pub fn cycle_favorite_models(&self, window: &mut Window, cx: &mut Context) { + 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, ) diff --git a/crates/agent_ui/src/favorite_models.rs b/crates/agent_ui/src/favorite_models.rs index d8d4db976fc9916973eedd9174925fba75a06b2b..d11bf5dda00d29f6668559348dc1f4abd937571f 100644 --- a/crates/agent_ui/src/favorite_models.rs +++ b/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) -> 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, should_be_favorite: bool, fs: Arc, - 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, - 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); - } - }); -} diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 8d96d56ea67cc9366df420b23e2221636d3450fb..3542959dbf146d39d40a5851b6fa9ce00a5014cd 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/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 Render for PromptEditor { .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 Render for PromptEditor { .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 Render for PromptEditor { .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 PromptEditor { .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 PromptEditor { ) }) } 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 PromptEditor { .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 PromptEditor { ) }) } 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 PromptEditor { let colors = cx.theme().colors(); div() - .key_context("InlineAssistEditor") .size_full() .p_2() .pl_1() diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 704e340ace35f33f757ab7708f96ffc940a8eb91..64b1397b02ca4a7fc9c20fdfc8abf10141712bbb 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -20,14 +20,14 @@ use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem} type OnModelChanged = Arc, &mut App) + 'static>; type GetActiveModel = Arc Option + 'static>; -type OnToggleFavorite = Arc, bool, &App) + 'static>; +type OnToggleFavorite = Arc, bool, &mut App) + 'static>; pub type LanguageModelSelector = Picker; pub fn language_model_selector( get_active_model: impl Fn(&App) -> Option + 'static, on_model_changed: impl Fn(Arc, &mut App) + 'static, - on_toggle_favorite: impl Fn(Arc, bool, &App) + 'static, + on_toggle_favorite: impl Fn(Arc, 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 + 'static, on_model_changed: impl Fn(Arc, &mut App) + 'static, - on_toggle_favorite: impl Fn(Arc, bool, &App) + 'static, + on_toggle_favorite: impl Fn(Arc, 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>) { 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( diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 514f45528427af89eeccf85512abf850a7a1be05..3a790dd354afb9ae21cc49687da08256f167b19d 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/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() } }); diff --git a/crates/agent_ui/src/ui/model_selector_components.rs b/crates/agent_ui/src/ui/model_selector_components.rs index beb0c13d761aa9e7e41c2ac4e35a8cfcc7e8d869..de4036b8dec27b735e13a3b4f0de80cfa11111df 100644 --- a/crates/agent_ui/src/ui/model_selector_components.rs +++ b/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>, + on_toggle_favorite: Option>, } 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, + )), + ) + }) + } +} diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index 1443e4d877d4e288fb379a02fee8a351075d8db8..8829befaac7f21a1262ce0bf1410bce71546a3ed 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -1868,6 +1868,7 @@ pub struct BuiltinAgentServerSettings { pub ignore_system_version: Option, pub default_mode: Option, pub default_model: Option, + pub favorite_models: Vec, } impl BuiltinAgentServerSettings { @@ -1891,6 +1892,7 @@ impl From 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, + /// The favorite models for this agent. + /// + /// Default: [] + favorite_models: Vec, }, Extension { /// The default mode to use for this agent. @@ -1936,6 +1942,10 @@ pub enum CustomAgentServerSettings { /// /// Default: None default_model: Option, + /// The favorite models for this agent. + /// + /// Default: [] + favorite_models: Vec, }, } @@ -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 for CustomAgentServerSettings { @@ -1973,6 +1994,7 @@ impl From 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 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(); diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs index d3a8e40084fc5db7fd348908b1b721617c7c8206..2abf00777af2308c4c2339bd180db47fb3d5e02f 100644 --- a/crates/settings/src/settings_content/agent.rs +++ b/crates/settings/src/settings_content/agent.rs @@ -363,6 +363,13 @@ pub struct BuiltinAgentServerSettings { /// /// Default: None pub default_model: Option, + /// The favorite models for this agent. + /// + /// These are the model IDs as reported by the agent. + /// + /// Default: [] + #[serde(default)] + pub favorite_models: Vec, } #[with_fallible_options] @@ -387,6 +394,13 @@ pub enum CustomAgentServerSettings { /// /// Default: None default_model: Option, + /// The favorite models for this agent. + /// + /// These are the model IDs as reported by the agent. + /// + /// Default: [] + #[serde(default)] + favorite_models: Vec, }, Extension { /// The default mode to use for this agent. @@ -401,5 +415,12 @@ pub enum CustomAgentServerSettings { /// /// Default: None default_model: Option, + /// The favorite models for this agent. + /// + /// These are the model IDs as reported by the agent. + /// + /// Default: [] + #[serde(default)] + favorite_models: Vec, }, }