This commit is contained in:
Bennet Bo Fenner
2025-12-16 18:16:55 +01:00
parent d78e58783d
commit 5151b22e2e
9 changed files with 159 additions and 221 deletions

View File

@@ -34,8 +34,7 @@ pub struct AgentSettings {
pub commit_message_model: Option<LanguageModelSelection>,
pub thread_summary_model: Option<LanguageModelSelection>,
pub inline_alternatives: Vec<LanguageModelSelection>,
pub favorite_models_as_selections: Vec<LanguageModelSelection>,
pub favorite_models_as_ids: Arc<HashSet<ModelId>>,
pub favorite_models: Vec<LanguageModelSelection>,
pub default_profile: AgentProfileId,
pub default_view: DefaultAgentView,
pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
@@ -99,6 +98,13 @@ impl AgentSettings {
pub fn set_message_editor_max_lines(&self) -> usize {
self.message_editor_min_lines * 2
}
pub fn favorite_model_ids(&self) -> HashSet<ModelId> {
self.favorite_models
.iter()
.map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model)))
.collect()
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
@@ -167,16 +173,7 @@ impl Settings for AgentSettings {
commit_message_model: agent.commit_message_model,
thread_summary_model: agent.thread_summary_model,
inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
favorite_models_as_selections: agent.favorite_models,
favorite_models_as_ids: Arc::new(
content
.agent
.as_ref()
.iter()
.flat_map(|agent| &agent.favorite_models)
.map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model)))
.collect(),
),
favorite_models: agent.favorite_models,
default_profile: AgentProfileId(agent.default_profile.unwrap()),
default_view: agent.default_view.unwrap(),
profiles: agent

View File

@@ -45,24 +45,7 @@ pub fn acp_model_selector(
enum AcpModelPickerEntry {
Separator(SharedString),
Model(AgentModelInfo, AcpModelPickerEntryAction),
}
/// Corresponds to the action button shown on the model in the list.
#[derive(Copy, Clone)]
enum AcpModelPickerEntryAction {
Favorite,
Unfavorite,
}
impl AcpModelPickerEntryAction {
fn from_favorite_state(is_favorite: bool) -> Self {
if is_favorite {
Self::Unfavorite
} else {
Self::Favorite
}
}
Model(AgentModelInfo, ModelSelectorFavoriteAction),
}
pub struct AcpModelPickerDelegate {
@@ -180,7 +163,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let favorites = if self.selector.supports_favorites() {
AgentSettings::get_global(cx).favorite_models_as_ids.clone()
Arc::new(AgentSettings::get_global(cx).favorite_model_ids())
} else {
Default::default()
};
@@ -201,7 +184,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries =
info_list_to_picker_entries(filtered_models, favorites).collect();
info_list_to_picker_entries(filtered_models, favorites);
// Finds the currently selected model in the list
let new_index = this
.delegate
@@ -279,32 +262,17 @@ impl PickerDelegate for AcpModelPickerDelegate {
let supports_favorites = self.selector.supports_favorites();
let handle_action_click = {
let action = *action;
let is_favorite = matches!(action, ModelSelectorFavoriteAction::Unfavorite);
let model_id = model_info.id.clone();
let fs = self.fs.clone();
move |cx: &App| match action {
AcpModelPickerEntryAction::Favorite => {
crate::favorite_models::add_to_settings(
model_id.clone(),
fs.clone(),
cx,
)
}
AcpModelPickerEntryAction::Unfavorite => {
crate::favorite_models::remove_from_settings(
model_id.clone(),
fs.clone(),
cx,
)
}
}
};
let favorite_action = match action {
AcpModelPickerEntryAction::Favorite => ModelSelectorFavoriteAction::Favorite,
AcpModelPickerEntryAction::Unfavorite => {
ModelSelectorFavoriteAction::Unfavorite
move |cx: &App| {
crate::favorite_models::toggle_model_id_in_settings(
model_id.clone(),
!is_favorite,
fs.clone(),
cx,
);
}
};
@@ -328,7 +296,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
.is_selected(is_selected)
.is_focused(selected)
.when(supports_favorites, |this| {
this.favorite_action(favorite_action)
this.favorite_action(*action)
.on_favorite_action_click(handle_action_click)
}),
)
@@ -381,58 +349,56 @@ impl PickerDelegate for AcpModelPickerDelegate {
fn info_list_to_picker_entries(
model_list: AgentModelList,
favorites: Arc<HashSet<ModelId>>,
) -> impl Iterator<Item = AcpModelPickerEntry> {
let all_models = match &model_list {
AgentModelList::Flat(list) => itertools::Either::Left(list.iter()),
AgentModelList::Grouped(index_map) => {
itertools::Either::Right(index_map.values().flatten())
}
) -> Vec<AcpModelPickerEntry> {
let mut entries = Vec::new();
let all_models: Vec<_> = match &model_list {
AgentModelList::Flat(list) => list.iter().collect(),
AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
};
let favorites_entries = all_models
.filter(|model_info| favorites.contains(&model_info.id))
.unique_by(|model_info| &model_info.id)
.map(|model_info| {
AcpModelPickerEntry::Model(model_info.clone(), AcpModelPickerEntryAction::Unfavorite)
})
.collect_vec();
let favorite_models: Vec<_> = all_models
.iter()
.filter(|m| favorites.contains(&m.id))
.unique_by(|m| &m.id)
.collect();
let model_list_is_flat = model_list.is_flat();
let all_models_entries = match model_list {
AgentModelList::Flat(list) => {
itertools::Either::Left(list.into_iter().map(move |model_info| {
let action = AcpModelPickerEntryAction::from_favorite_state(
favorites.contains(&model_info.id),
);
AcpModelPickerEntry::Model(model_info, action)
}))
let has_favorites = !favorite_models.is_empty();
if has_favorites {
entries.push(AcpModelPickerEntry::Separator("Favorite".into()));
for model in favorite_models {
entries.push(AcpModelPickerEntry::Model(
(*model).clone(),
ModelSelectorFavoriteAction::Unfavorite,
));
}
AgentModelList::Grouped(index_map) => {
itertools::Either::Right(index_map.into_iter().flat_map(move |(group_name, models)| {
let favorites = favorites.clone();
std::iter::once(AcpModelPickerEntry::Separator(group_name.0)).chain(
models.into_iter().map(move |model_info| {
let action = AcpModelPickerEntryAction::from_favorite_state(
favorites.contains(&model_info.id),
);
AcpModelPickerEntry::Model(model_info, action)
}),
)
}))
}
};
if favorites_entries.is_empty() {
itertools::Either::Left(all_models_entries)
} else {
itertools::Either::Right(
std::iter::once(AcpModelPickerEntry::Separator("Favorite".into()))
.chain(favorites_entries)
.chain(model_list_is_flat.then_some(AcpModelPickerEntry::Separator("All".into())))
.chain(all_models_entries),
)
}
match model_list {
AgentModelList::Flat(list) => {
if has_favorites {
entries.push(AcpModelPickerEntry::Separator("All".into()));
}
for model in list {
let action =
ModelSelectorFavoriteAction::from_is_favorite(favorites.contains(&model.id));
entries.push(AcpModelPickerEntry::Model(model, action));
}
}
AgentModelList::Grouped(index_map) => {
for (group_name, models) in index_map {
entries.push(AcpModelPickerEntry::Separator(group_name.0));
for model in models {
let action = ModelSelectorFavoriteAction::from_is_favorite(
favorites.contains(&model.id),
);
entries.push(AcpModelPickerEntry::Model(model, action));
}
}
}
}
entries
}
async fn fuzzy_search(
@@ -591,7 +557,7 @@ mod tests {
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, favorites).collect_vec();
let entries = info_list_to_picker_entries(models, favorites);
assert!(matches!(
entries.first(),
@@ -607,7 +573,7 @@ mod tests {
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
let favorites = create_favorites(vec![]);
let entries = info_list_to_picker_entries(models, favorites).collect_vec();
let entries = info_list_to_picker_entries(models, favorites);
assert!(matches!(
entries.first(),
@@ -623,16 +589,16 @@ mod tests {
]);
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, favorites).collect_vec();
let entries = info_list_to_picker_entries(models, favorites);
for entry in &entries {
match entry {
AcpModelPickerEntry::Separator(_) => {}
AcpModelPickerEntry::Model(info, action) if info.id.0.as_ref() == "zed/claude" => {
assert!(matches!(action, AcpModelPickerEntryAction::Unfavorite));
assert!(matches!(action, ModelSelectorFavoriteAction::Unfavorite));
}
AcpModelPickerEntry::Model(_, action) => {
assert!(matches!(action, AcpModelPickerEntryAction::Favorite));
assert!(matches!(action, ModelSelectorFavoriteAction::Favorite));
}
}
}
@@ -646,7 +612,7 @@ mod tests {
]);
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
let entries = info_list_to_picker_entries(models, favorites).collect_vec();
let entries = info_list_to_picker_entries(models, favorites);
let model_ids = get_entry_model_ids(&entries);
assert_eq!(model_ids[0], "zed/gemini");
@@ -667,7 +633,7 @@ mod tests {
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, favorites).collect_vec();
let entries = info_list_to_picker_entries(models, favorites);
let labels = get_entry_labels(&entries);
assert_eq!(
@@ -707,7 +673,7 @@ mod tests {
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, favorites).collect_vec();
let entries = info_list_to_picker_entries(models, favorites);
assert!(matches!(
entries.first(),

View File

@@ -271,13 +271,15 @@ impl ManageProfilesModal {
},
{
let fs = fs.clone();
move |model, cx| {
crate::favorite_models::add_to_settings(model, fs.clone(), cx);
move |model, should_be_favorite, cx| {
crate::favorite_models::toggle_in_settings(
model,
should_be_favorite,
fs.clone(),
cx,
);
}
},
move |model, cx| {
crate::favorite_models::remove_from_settings(model, fs.clone(), cx);
},
false, // Do not use popover styles for the model picker
self.focus_handle.clone(),
window,

View File

@@ -53,13 +53,15 @@ impl AgentModelSelector {
},
{
let fs = fs.clone();
move |model, cx| {
crate::favorite_models::add_to_settings(model, fs.clone(), cx);
move |model, should_be_favorite, cx| {
crate::favorite_models::toggle_in_settings(
model,
should_be_favorite,
fs.clone(),
cx,
);
}
},
move |model, cx| {
crate::favorite_models::remove_from_settings(model, fs.clone(), cx);
},
true, // Use popover styles for picker
focus_handle_clone,
window,

View File

@@ -458,8 +458,7 @@ mod tests {
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: vec![],
favorite_models_as_selections: vec![],
favorite_models_as_ids: Arc::new(Default::default()),
favorite_models: vec![],
default_profile: AgentProfileId::default(),
default_view: DefaultAgentView::Thread,
profiles: Default::default(),

View File

@@ -6,52 +6,52 @@ use language_model::LanguageModel;
use settings::{LanguageModelSelection, update_settings_file};
use ui::App;
pub trait IntoLanguageModelSelection {
fn into_language_model_selection(self) -> LanguageModelSelection;
}
impl IntoLanguageModelSelection for Arc<dyn LanguageModel> {
fn into_language_model_selection(self) -> LanguageModelSelection {
LanguageModelSelection {
provider: self.provider_id().to_string().into(),
model: self.id().0.to_string(),
}
fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelSelection {
LanguageModelSelection {
provider: model.provider_id().to_string().into(),
model: model.id().0.to_string(),
}
}
impl IntoLanguageModelSelection for ModelId {
fn into_language_model_selection(self) -> LanguageModelSelection {
let model_id = self.0.as_ref();
let (provider, model) = model_id.split_once('/').unwrap_or(("", model_id));
LanguageModelSelection {
provider: provider.to_owned().into(),
model: model.to_owned(),
}
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 add_to_settings(
model: impl IntoLanguageModelSelection + Send + 'static,
pub fn toggle_in_settings(
model: Arc<dyn LanguageModel>,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent
.get_or_insert_default()
.add_favorite_model(model.into_language_model_selection())
});
}
pub fn remove_from_settings(
model: impl IntoLanguageModelSelection + Send + 'static,
fs: Arc<dyn Fs>,
cx: &App,
) {
update_settings_file(fs, cx, |settings, _| {
if let Some(ref mut agent_settings) = settings.agent {
agent_settings.remove_favorite_model(&model.into_language_model_selection())
let selection = language_model_to_selection(&model);
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);
}
});
}
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);
}
});
}

View File

@@ -22,14 +22,14 @@ use crate::ui::{
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>;
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_add_favorite_model: impl Fn(Arc<dyn LanguageModel>, &App) + 'static,
on_remove_favorite_model: impl Fn(Arc<dyn LanguageModel>, &App) + 'static,
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
popover_styles: bool,
focus_handle: FocusHandle,
window: &mut Window,
@@ -38,8 +38,7 @@ pub fn language_model_selector(
let delegate = LanguageModelPickerDelegate::new(
get_active_model,
on_model_changed,
on_add_favorite_model,
on_remove_favorite_model,
on_toggle_favorite,
popover_styles,
focus_handle,
window,
@@ -62,7 +61,7 @@ fn all_models(cx: &App) -> GroupedModels {
let mut favorites_index = FavoritesIndex::default();
for sel in &AgentSettings::get_global(cx).favorite_models_as_selections {
for sel in &AgentSettings::get_global(cx).favorite_models {
favorites_index
.entry(sel.provider.0.clone().into())
.or_default()
@@ -122,8 +121,7 @@ impl ModelInfo {
pub struct LanguageModelPickerDelegate {
on_model_changed: OnModelChanged,
get_active_model: GetActiveModel,
on_add_favorite_model: Arc<dyn Fn(Arc<dyn LanguageModel>, &App)>,
on_remove_favorite_model: Arc<dyn Fn(Arc<dyn LanguageModel>, &App)>,
on_toggle_favorite: OnToggleFavorite,
all_models: Arc<GroupedModels>,
filtered_entries: Vec<LanguageModelPickerEntry>,
selected_index: usize,
@@ -137,8 +135,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_add_favorite_model: impl Fn(Arc<dyn LanguageModel>, &App) + 'static,
on_remove_favorite_model: impl Fn(Arc<dyn LanguageModel>, &App) + 'static,
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
popover_styles: bool,
focus_handle: FocusHandle,
window: &mut Window,
@@ -154,8 +151,7 @@ impl LanguageModelPickerDelegate {
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
filtered_entries: entries,
get_active_model: Arc::new(get_active_model),
on_add_favorite_model: Arc::new(on_add_favorite_model),
on_remove_favorite_model: Arc::new(on_remove_favorite_model),
on_toggle_favorite: Arc::new(on_toggle_favorite),
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
_subscriptions: vec![cx.subscribe_in(
&LanguageModelRegistry::global(cx),
@@ -296,7 +292,7 @@ impl GroupedModels {
entries.extend(self.favorites.iter().map(|info| {
LanguageModelPickerEntry::Model(
info.clone(),
LanguageModelPickerEntryAction::Unfavorite,
ModelSelectorFavoriteAction::Unfavorite,
)
}));
}
@@ -306,7 +302,7 @@ impl GroupedModels {
entries.extend(self.recommended.iter().map(|info| {
LanguageModelPickerEntry::Model(
info.clone(),
LanguageModelPickerEntryAction::from_favorite_state(info.is_favorite),
ModelSelectorFavoriteAction::from_is_favorite(info.is_favorite),
)
}));
}
@@ -321,7 +317,7 @@ impl GroupedModels {
entries.extend(models.iter().map(|info| {
LanguageModelPickerEntry::Model(
info.clone(),
LanguageModelPickerEntryAction::from_favorite_state(info.is_favorite),
ModelSelectorFavoriteAction::from_is_favorite(info.is_favorite),
)
}));
}
@@ -330,27 +326,10 @@ impl GroupedModels {
}
enum LanguageModelPickerEntry {
Model(ModelInfo, LanguageModelPickerEntryAction),
Model(ModelInfo, ModelSelectorFavoriteAction),
Separator(SharedString),
}
/// Corresponds to the action button shown on the model in the list.
#[derive(Copy, Clone)]
enum LanguageModelPickerEntryAction {
Favorite,
Unfavorite,
}
impl LanguageModelPickerEntryAction {
fn from_favorite_state(is_favorite: bool) -> Self {
if is_favorite {
Self::Unfavorite
} else {
Self::Favorite
}
}
}
struct ModelMatcher {
models: Vec<ModelInfo>,
bg_executor: BackgroundExecutor,
@@ -554,27 +533,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
&& Some(model_info.model.id()) == active_model_id;
let handle_action_click = {
let action = *action;
let is_favorite = model_info.is_favorite;
let model = model_info.model.clone();
let on_add_favorite_model = self.on_add_favorite_model.clone();
let on_remove_favorite_model = self.on_remove_favorite_model.clone();
move |cx: &App| match action {
LanguageModelPickerEntryAction::Favorite => {
on_add_favorite_model(model.clone(), cx)
}
LanguageModelPickerEntryAction::Unfavorite => {
on_remove_favorite_model(model.clone(), cx)
}
}
};
let favorite_action = match action {
LanguageModelPickerEntryAction::Favorite => {
ModelSelectorFavoriteAction::Favorite
}
LanguageModelPickerEntryAction::Unfavorite => {
ModelSelectorFavoriteAction::Unfavorite
}
let on_toggle_favorite = self.on_toggle_favorite.clone();
move |cx: &App| on_toggle_favorite(model.clone(), !is_favorite, cx)
};
Some(
@@ -582,7 +544,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.icon(model_info.icon)
.is_selected(is_selected)
.is_focused(selected)
.favorite_action(favorite_action)
.favorite_action(*action)
.on_favorite_action_click(handle_action_click)
.into_any_element(),
)
@@ -915,13 +877,13 @@ mod tests {
LanguageModelPickerEntry::Model(info, action)
if !in_favorites_section && info.model.telemetry_id() == "zed/claude" =>
{
assert!(matches!(action, LanguageModelPickerEntryAction::Unfavorite));
assert!(matches!(action, ModelSelectorFavoriteAction::Unfavorite));
}
LanguageModelPickerEntry::Model(info, action) => {
if info.is_favorite {
assert!(matches!(action, LanguageModelPickerEntryAction::Unfavorite));
assert!(matches!(action, ModelSelectorFavoriteAction::Unfavorite));
} else {
assert!(matches!(action, LanguageModelPickerEntryAction::Favorite));
assert!(matches!(action, ModelSelectorFavoriteAction::Favorite));
}
}
}

View File

@@ -321,13 +321,15 @@ impl TextThreadEditor {
},
{
let fs = fs.clone();
move |model, cx| {
crate::favorite_models::add_to_settings(model, fs.clone(), cx);
move |model, should_be_favorite, cx| {
crate::favorite_models::toggle_in_settings(
model,
should_be_favorite,
fs.clone(),
cx,
);
}
},
move |model, cx| {
crate::favorite_models::remove_from_settings(model, fs.clone(), cx);
},
true, // Use popover styles for picker
focus_handle,
window,

View File

@@ -42,6 +42,14 @@ pub enum ModelSelectorFavoriteAction {
}
impl ModelSelectorFavoriteAction {
pub fn from_is_favorite(is_favorite: bool) -> Self {
if is_favorite {
Self::Unfavorite
} else {
Self::Favorite
}
}
pub fn icon_name(&self) -> IconName {
match self {
Self::Favorite => IconName::Star,