Detailed changes
@@ -252,6 +252,7 @@
"ctrl-y": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"ctrl-alt-z": "agent::RejectOnce",
+ "alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -346,6 +347,7 @@
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
+ "alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -266,6 +266,7 @@
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-k l": "agent::OpenRulesLibrary",
+ "alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -292,6 +293,7 @@
"cmd-y": "agent::AllowOnce",
"cmd-alt-y": "agent::AllowAlways",
"cmd-alt-z": "agent::RejectOnce",
+ "alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -386,6 +388,7 @@
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
+ "alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -397,6 +400,7 @@
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
+ "alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -880,6 +884,7 @@
"use_key_equivalents": true,
"bindings": {
"cmd-alt-/": "agent::ToggleModelSelector",
+ "alt-tab": "agent::CycleFavoriteModels",
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist",
"cmd-shift-enter": "inline_assistant::ThumbsUpResult",
@@ -253,6 +253,7 @@
"shift-alt-a": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"shift-alt-z": "agent::RejectOnce",
+ "alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -342,6 +343,7 @@
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
+ "alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -353,6 +355,7 @@
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
+ "alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -119,6 +119,67 @@ impl AcpModelPickerDelegate {
pub fn active_model(&self) -> Option<&AgentModelInfo> {
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();
+
+ if favorites.is_empty() {
+ return;
+ }
+
+ let Some(models) = self.models.clone() 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 favorite_models = all_models
+ .iter()
+ .filter(|model| favorites.contains(&model.id))
+ .unique_by(|model| &model.id)
+ .cloned()
+ .collect::<Vec<_>>();
+
+ let current_id = self.selected_model.as_ref().map(|m| m.id.clone());
+
+ let current_index_in_favorites = current_id
+ .as_ref()
+ .and_then(|id| favorite_models.iter().position(|m| &m.id == id))
+ .unwrap_or(usize::MAX);
+
+ let next_index = if current_index_in_favorites == usize::MAX {
+ 0
+ } else {
+ (current_index_in_favorites + 1) % favorite_models.len()
+ };
+
+ let next_model = favorite_models[next_index].clone();
+
+ self.selector
+ .select_model(next_model.id.clone(), cx)
+ .detach_and_log_err(cx);
+
+ self.selected_model = Some(next_model);
+
+ // Keep the picker selection aligned with the newly-selected model
+ if let Some(new_index) = self.filtered_entries.iter().position(|entry| {
+ matches!(entry, AcpModelPickerEntry::Model(model_info, _) if self.selected_model.as_ref().is_some_and(|selected| model_info.id == selected.id))
+ }) {
+ self.set_selected_index(new_index, window, cx);
+ } else {
+ cx.notify();
+ }
+ }
}
impl PickerDelegate for AcpModelPickerDelegate {
@@ -3,15 +3,15 @@ use std::sync::Arc;
use acp_thread::{AgentModelInfo, AgentModelSelector};
use agent_servers::AgentServer;
+use agent_settings::AgentSettings;
use fs::Fs;
use gpui::{Entity, FocusHandle};
use picker::popover_menu::PickerPopoverMenu;
-use ui::{
- ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, TintColor, Tooltip, Window,
- prelude::*,
-};
+use settings::Settings as _;
+use ui::{ButtonLike, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
use zed_actions::agent::ToggleModelSelector;
+use crate::CycleFavoriteModels;
use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
pub struct AcpModelSelectorPopover {
@@ -54,6 +54,12 @@ impl AcpModelSelectorPopover {
pub fn active_model<'a>(&self, cx: &'a App) -> Option<&'a AgentModelInfo> {
self.selector.read(cx).delegate.active_model()
}
+
+ 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 AcpModelSelectorPopover {
@@ -74,6 +80,46 @@ 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();
+
+ 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()
+ }
+ });
+
PickerPopoverMenu::new(
self.selector.clone(),
ButtonLike::new("active-model")
@@ -88,9 +134,7 @@ impl Render for AcpModelSelectorPopover {
.ml_0p5(),
)
.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)),
- move |_window, cx| {
- Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
- },
+ tooltip,
gpui::Corner::BottomRight,
cx,
)
@@ -66,8 +66,8 @@ use crate::profile_selector::{ProfileProvider, ProfileSelector};
use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip, UsageCallout};
use crate::{
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
- CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory,
- RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector,
+ CycleFavoriteModels, CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread,
+ OpenAgentDiff, OpenHistory, RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector,
};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -4293,6 +4293,13 @@ impl AcpThreadView {
.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()
@@ -68,6 +68,8 @@ actions!(
ToggleProfileSelector,
/// Cycles through available session modes.
CycleModeSelector,
+ /// Cycles through favorited models in the ACP model selector.
+ CycleFavoriteModels,
/// Expands the message editor to full size.
ExpandMessageEditor,
/// Removes all thread history.
@@ -249,6 +249,41 @@ impl LanguageModelPickerDelegate {
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
(self.get_active_model)(cx)
}
+
+ pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ if self.all_models.favorites.is_empty() {
+ return;
+ }
+
+ let active_model = (self.get_active_model)(cx);
+ let active_provider_id = active_model.as_ref().map(|m| m.provider.id());
+ let active_model_id = active_model.as_ref().map(|m| m.model.id());
+
+ let current_index = self
+ .all_models
+ .favorites
+ .iter()
+ .position(|info| {
+ Some(info.model.provider_id()) == active_provider_id
+ && Some(info.model.id()) == active_model_id
+ })
+ .unwrap_or(usize::MAX);
+
+ let next_index = if current_index == usize::MAX {
+ 0
+ } else {
+ (current_index + 1) % self.all_models.favorites.len()
+ };
+
+ let next_model = self.all_models.favorites[next_index].model.clone();
+
+ (self.on_model_changed)(next_model, cx);
+
+ // Align the picker selection with the newly-active model
+ let new_index =
+ Self::get_active_model_index(&self.filtered_entries, (self.get_active_model)(cx));
+ self.set_selected_index(new_index, window, cx);
+ }
}
struct GroupedModels {
@@ -2,7 +2,7 @@ use crate::{
language_model_selector::{LanguageModelSelector, language_model_selector},
ui::BurnModeTooltip,
};
-use agent_settings::CompletionMode;
+use agent_settings::{AgentSettings, CompletionMode};
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
@@ -73,6 +73,8 @@ use workspace::{
};
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
+use crate::CycleFavoriteModels;
+
use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
use assistant_text_thread::{
CacheStatus, Content, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
@@ -2209,12 +2211,53 @@ impl TextThreadEditor {
};
let focus_handle = self.editor().focus_handle(cx);
+
let (color, icon) = if self.language_model_selector_menu_handle.is_deployed() {
(Color::Accent, IconName::ChevronUp)
} else {
(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();
+
+ 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()
+ }
+ });
+
PickerPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
@@ -2231,9 +2274,7 @@ impl TextThreadEditor {
)
.child(Icon::new(icon).color(color).size(IconSize::XSmall)),
),
- move |_window, cx| {
- Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
- },
+ tooltip,
gpui::Corner::BottomRight,
cx,
)
@@ -2593,6 +2634,11 @@ impl Render for TextThreadEditor {
.on_action(move |_: &ToggleModelSelector, window, cx| {
language_model_selector.toggle(window, cx);
})
+ .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
+ this.language_model_selector.update(cx, |selector, cx| {
+ selector.delegate.cycle_favorite_models(window, cx);
+ });
+ }))
.size_full()
.child(
div()
@@ -113,13 +113,9 @@ impl RenderOnce for ModelSelectorListItem {
.child(Label::new(self.title).truncate()),
)
.end_slot(div().pr_2().when(self.is_selected, |this| {
- this.child(
- Icon::new(IconName::Check)
- .color(Color::Accent)
- .size(IconSize::Small),
- )
+ this.child(Icon::new(IconName::Check).color(Color::Accent))
}))
- .end_hover_slot(div().pr_2().when_some(self.on_toggle_favorite, {
+ .end_hover_slot(div().pr_1p5().when_some(self.on_toggle_favorite, {
|this, handle_click| {
let (icon, color, tooltip) = if is_favorite {
(IconName::StarFilled, Color::Accent, "Unfavorite Model")