use std::{cmp::Reverse, rc::Rc, sync::Arc}; use acp_thread::{ AgentModelIcon, AgentModelId, AgentModelInfo, AgentModelList, AgentModelSelector,}; use anyhow::Result;use collections::{HashSet, IndexMap};use futures::FutureExt;use fuzzy::{StringMatchCandidate, match_strings};use gpui::{ Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task, TaskExt, WeakEntity,};use itertools::Itertools;use ordered_float::OrderedFloat;use picker::{Picker, PickerDelegate};use settings::SettingsStore;use ui::{DocumentationAside, IntoElement, prelude::*};use util::ResultExt;use zed_actions::agent::OpenSettings; use crate::ui::{ ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem, documentation_aside_side,}; pub type ModelSelector = Picker<ModelPickerDelegate>; pub fn acp_model_selector( selector: Rc<dyn AgentModelSelector>, focus_handle: FocusHandle, window: &mut Window, cx: &mut Context<ModelSelector>,) -> ModelSelector { let delegate = ModelPickerDelegate::new(selector, focus_handle, window, cx); Picker::list(delegate, window, cx) .show_scrollbar(true) .initial_width(rems(20.))} enum ModelPickerEntry { Separator(SharedString), Model(AgentModelInfo, bool),} pub struct ModelPickerDelegate { selector: Rc<dyn AgentModelSelector>, filtered_entries: Vec<ModelPickerEntry>, models: Option<AgentModelList>, selected_index: usize, selected_description: Option<(usize, SharedString)>, selected_model: Option<AgentModelInfo>, favorites: HashSet<AgentModelId>, _refresh_models_task: Task<()>, _settings_subscription: Subscription, focus_handle: FocusHandle,} impl ModelPickerDelegate { fn new( selector: Rc<dyn AgentModelSelector>, focus_handle: FocusHandle, window: &mut Window, cx: &mut Context<ModelSelector>, ) -> Self { let rx = selector.watch(cx); let refresh_models_task = { cx.spawn_in(window, { async move |this, cx| { async fn refresh( this: &WeakEntity<Picker<ModelPickerDelegate>>, cx: &mut AsyncWindowContext, ) -> Result<()> { let (models_task, selected_model_task) = this.update(cx, |this, cx| { ( this.delegate.selector.list_models(cx), this.delegate.selector.selected_model(cx), ) })?; let (models, selected_model) = futures::join!(models_task, selected_model_task); this.update_in(cx, |this, window, cx| { this.delegate.models = models.ok(); this.delegate.selected_model = selected_model.ok(); this.refresh(window, cx) }) } refresh(&this, cx).await.log_err(); if let Some(mut rx) = rx { while let Ok(()) = rx.recv().await { refresh(&this, cx).await.log_err(); } } } }) }; let selector_for_subscription = selector.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 = selector_for_subscription.favorite_model_ids(cx); if new_favorites != picker.delegate.favorites { picker.delegate.favorites = new_favorites; picker.refresh(window, cx); } }); let favorites = selector.favorite_model_ids(cx); Self { selector, filtered_entries: Vec::new(), models: None, selected_model: None, selected_index: 0, selected_description: None, favorites, _refresh_models_task: refresh_models_task, _settings_subscription: settings_subscription, focus_handle, } } pub fn active_model(&self) -> Option<&AgentModelInfo> { self.selected_model.as_ref() } pub fn favorites_count(&self) -> usize { self.favorites.len() } 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 else { return; }; 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: Vec<_> = all_models .into_iter() .filter(|model| self.favorites.contains(&model.id)) .unique_by(|model| &model.id) .collect(); if favorite_models.is_empty() { return; } let current_id = self.selected_model.as_ref().map(|m| &m.id); let current_index_in_favorites = current_id .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, ModelPickerEntry::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 ModelPickerDelegate { type ListItem = AnyElement; fn name() -> &'static str { "model selector" } fn match_count(&self) -> usize { self.filtered_entries.len() } fn selected_index(&self) -> usize { self.selected_index } fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) { self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1)); cx.notify(); } fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool { match self.filtered_entries.get(ix) { Some(ModelPickerEntry::Model(_, _)) => true, Some(ModelPickerEntry::Separator(_)) | None => false, } } fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> { "Select a model…".into() } fn update_matches( &mut self, query: String, window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Task<()> { let favorites = self.favorites.clone(); cx.spawn_in(window, async move |this, cx| { let filtered_models = match this .read_with(cx, |this, cx| { this.delegate.models.clone().map(move |models| { fuzzy_search(models, query, cx.background_executor().clone()) }) }) .ok() .flatten() { Some(task) => task.await, None => AgentModelList::Flat(vec![]), }; this.update_in(cx, |this, window, cx| { this.delegate.filtered_entries = info_list_to_picker_entries(filtered_models, &favorites); // Finds the currently selected model in the list let new_index = this .delegate .selected_model .as_ref() .and_then(|selected| { this.delegate.filtered_entries.iter().position(|entry| { if let ModelPickerEntry::Model(model_info, _) = entry { model_info.id == selected.id } else { false } }) }) .unwrap_or(0); this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx); cx.notify(); }) .ok(); }) } fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) { if let Some(ModelPickerEntry::Model(model_info, _)) = self.filtered_entries.get(self.selected_index) && model_info.disabled.is_none() { self.selector .select_model(model_info.id.clone(), cx) .detach_and_log_err(cx); self.selected_model = Some(model_info.clone()); let current_index = self.selected_index; self.set_selected_index(current_index, window, cx); cx.emit(DismissEvent); } } fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) { cx.defer_in(window, |picker, window, cx| { picker.set_query("", window, cx); }); } fn render_match( &self, ix: usize, selected: bool, _: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Option<Self::ListItem> { match self.filtered_entries.get(ix)? { ModelPickerEntry::Separator(title) => { Some(ModelSelectorHeader::new(title, ix > 1).into_any_element()) } ModelPickerEntry::Model(model_info, is_favorite) => { let is_selected = Some(model_info) == self.selected_model.as_ref(); let is_favorite = *is_favorite; let handle_action_click = { let model_id = model_info.id.clone(); let selector = self.selector.clone(); cx.listener(move |_, _, _, cx| { selector.toggle_favorite_model(model_id.clone(), !is_favorite, cx); }) }; let model_cost = model_info.cost.clone(); Some( div() .id(("model-picker-menu-child", ix)) .when_some(model_info.description.clone(), |this, description| { this.on_hover(cx.listener(move |menu, hovered, _, cx| { if *hovered { menu.delegate.selected_description = Some((ix, description.clone())); } else if matches!(menu.delegate.selected_description, Some((id, _)) if id == ix) { menu.delegate.selected_description = None; } cx.notify(); })) }) .child( ModelSelectorListItem::new(ix, model_info.name.clone()) .map(|this| match &model_info.icon { Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()), Some(AgentModelIcon::Named(icon)) => this.icon(*icon), None => this, }) .disabled(model_info.disabled.clone()) .is_selected(is_selected) .is_focused(selected) .is_latest(model_info.is_latest) .is_favorite(is_favorite) .on_toggle_favorite(handle_action_click) .cost_info(model_cost) ) .into_any_element(), ) } } } fn documentation_aside( &self, _window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Option<ui::DocumentationAside> { self.selected_description.as_ref().map(|(_, description)| { let description = description.clone(); let side = documentation_aside_side(cx); DocumentationAside::new( side, Rc::new(move |_| Label::new(description.clone()).into_any_element()), ) }) } fn documentation_aside_index(&self) -> Option<usize> { self.selected_description.as_ref().map(|(ix, _)| *ix) } fn render_footer( &self, _window: &mut Window, _cx: &mut Context<Picker<Self>>, ) -> Option<AnyElement> { let focus_handle = self.focus_handle.clone(); if !self.selector.should_render_footer() { return None; } Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element()) }} fn info_list_to_picker_entries( model_list: AgentModelList, favorites: &HashSet<AgentModelId>,) -> Vec<ModelPickerEntry> { 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 favorite_models: Vec<_> = all_models .iter() .filter(|m| favorites.contains(&m.id)) .unique_by(|m| &m.id) .collect(); let has_favorites = !favorite_models.is_empty(); if has_favorites { entries.push(ModelPickerEntry::Separator("Favorite".into())); for model in favorite_models { entries.push(ModelPickerEntry::Model((*model).clone(), true)); } } match model_list { AgentModelList::Flat(list) => { if has_favorites { entries.push(ModelPickerEntry::Separator("All".into())); } for model in list { let is_favorite = favorites.contains(&model.id); entries.push(ModelPickerEntry::Model(model, is_favorite)); } } AgentModelList::Grouped(index_map) => { for (group_name, models) in index_map { entries.push(ModelPickerEntry::Separator(group_name.0)); for model in models { let is_favorite = favorites.contains(&model.id); entries.push(ModelPickerEntry::Model(model, is_favorite)); } } } } entries} async fn fuzzy_search( model_list: AgentModelList, query: String, executor: BackgroundExecutor,) -> AgentModelList { async fn fuzzy_search_list( model_list: Vec<AgentModelInfo>, query: &str, executor: BackgroundExecutor, ) -> Vec<AgentModelInfo> { let candidates = model_list .iter() .enumerate() .map(|(ix, model)| StringMatchCandidate::new(ix, model.name.as_ref())) .collect::<Vec<_>>(); let mut matches = match_strings( &candidates, query, false, true, 100, &Default::default(), executor, ) .await; matches.sort_unstable_by_key(|mat| { let candidate = &candidates[mat.candidate_id]; (Reverse(OrderedFloat(mat.score)), candidate.id) }); matches .into_iter() .map(|mat| model_list[mat.candidate_id].clone()) .collect() } match model_list { AgentModelList::Flat(model_list) => { AgentModelList::Flat(fuzzy_search_list(model_list, &query, executor).await) } AgentModelList::Grouped(index_map) => { let groups = futures::future::join_all(index_map.into_iter().map(|(group_name, models)| { fuzzy_search_list(models, &query, executor.clone()) .map(|results| (group_name, results)) })) .await; AgentModelList::Grouped(IndexMap::from_iter( groups .into_iter() .filter(|(_, results)| !results.is_empty()), )) } }} #[cfg(test)]mod tests { use gpui::{App, TestAppContext, VisualTestContext}; use std::cell::RefCell; use super::*; Lines 1–500 of 895. Load more
use std::{cmp::Reverse, rc::Rc, sync::Arc}; use acp_thread::{ AgentModelIcon, AgentModelId, AgentModelInfo, AgentModelList, AgentModelSelector, }; use anyhow::Result; use collections::{HashSet, IndexMap}; use futures::FutureExt; use fuzzy::{StringMatchCandidate, match_strings}; use gpui::{ Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task, TaskExt, WeakEntity, }; use itertools::Itertools; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; use settings::SettingsStore; use ui::{DocumentationAside, IntoElement, prelude::*}; use util::ResultExt; use zed_actions::agent::OpenSettings; use crate::ui::{ ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem, documentation_aside_side, }; pub type ModelSelector = Picker<ModelPickerDelegate>; pub fn acp_model_selector( selector: Rc<dyn AgentModelSelector>, focus_handle: FocusHandle, window: &mut Window, cx: &mut Context<ModelSelector>, ) -> ModelSelector { let delegate = ModelPickerDelegate::new(selector, focus_handle, window, cx); Picker::list(delegate, window, cx) .show_scrollbar(true) .initial_width(rems(20.)) } enum ModelPickerEntry { Separator(SharedString), Model(AgentModelInfo, bool), } pub struct ModelPickerDelegate { selector: Rc<dyn AgentModelSelector>, filtered_entries: Vec<ModelPickerEntry>, models: Option<AgentModelList>, selected_index: usize, selected_description: Option<(usize, SharedString)>, selected_model: Option<AgentModelInfo>, favorites: HashSet<AgentModelId>, _refresh_models_task: Task<()>, _settings_subscription: Subscription, focus_handle: FocusHandle, } impl ModelPickerDelegate { fn new( selector: Rc<dyn AgentModelSelector>, focus_handle: FocusHandle, window: &mut Window, cx: &mut Context<ModelSelector>, ) -> Self { let rx = selector.watch(cx); let refresh_models_task = { cx.spawn_in(window, { async move |this, cx| { async fn refresh( this: &WeakEntity<Picker<ModelPickerDelegate>>, cx: &mut AsyncWindowContext, ) -> Result<()> { let (models_task, selected_model_task) = this.update(cx, |this, cx| { ( this.delegate.selector.list_models(cx), this.delegate.selector.selected_model(cx), ) })?; let (models, selected_model) = futures::join!(models_task, selected_model_task); this.update_in(cx, |this, window, cx| { this.delegate.models = models.ok(); this.delegate.selected_model = selected_model.ok(); this.refresh(window, cx) }) } refresh(&this, cx).await.log_err(); if let Some(mut rx) = rx { while let Ok(()) = rx.recv().await { refresh(&this, cx).await.log_err(); } } } }) }; let selector_for_subscription = selector.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 = selector_for_subscription.favorite_model_ids(cx); if new_favorites != picker.delegate.favorites { picker.delegate.favorites = new_favorites; picker.refresh(window, cx); } }); let favorites = selector.favorite_model_ids(cx); Self { selector, filtered_entries: Vec::new(), models: None, selected_model: None, selected_index: 0, selected_description: None, favorites, _refresh_models_task: refresh_models_task, _settings_subscription: settings_subscription, focus_handle, } } pub fn active_model(&self) -> Option<&AgentModelInfo> { self.selected_model.as_ref() } pub fn favorites_count(&self) -> usize { self.favorites.len() } 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 else { return; }; 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: Vec<_> = all_models .into_iter() .filter(|model| self.favorites.contains(&model.id)) .unique_by(|model| &model.id) .collect(); if favorite_models.is_empty() { return; } let current_id = self.selected_model.as_ref().map(|m| &m.id); let current_index_in_favorites = current_id .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, ModelPickerEntry::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 ModelPickerDelegate { type ListItem = AnyElement; fn name() -> &'static str { "model selector" } fn match_count(&self) -> usize { self.filtered_entries.len() } fn selected_index(&self) -> usize { self.selected_index } fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) { self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1)); cx.notify(); } fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool { match self.filtered_entries.get(ix) { Some(ModelPickerEntry::Model(_, _)) => true, Some(ModelPickerEntry::Separator(_)) | None => false, } } fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> { "Select a model…".into() } fn update_matches( &mut self, query: String, window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Task<()> { let favorites = self.favorites.clone(); cx.spawn_in(window, async move |this, cx| { let filtered_models = match this .read_with(cx, |this, cx| { this.delegate.models.clone().map(move |models| { fuzzy_search(models, query, cx.background_executor().clone()) }) }) .ok() .flatten() { Some(task) => task.await, None => AgentModelList::Flat(vec![]), }; this.update_in(cx, |this, window, cx| { this.delegate.filtered_entries = info_list_to_picker_entries(filtered_models, &favorites); // Finds the currently selected model in the list let new_index = this .delegate .selected_model .as_ref() .and_then(|selected| { this.delegate.filtered_entries.iter().position(|entry| { if let ModelPickerEntry::Model(model_info, _) = entry { model_info.id == selected.id } else { false } }) }) .unwrap_or(0); this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx); cx.notify(); }) .ok(); }) } fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) { if let Some(ModelPickerEntry::Model(model_info, _)) = self.filtered_entries.get(self.selected_index) && model_info.disabled.is_none() { self.selector .select_model(model_info.id.clone(), cx) .detach_and_log_err(cx); self.selected_model = Some(model_info.clone()); let current_index = self.selected_index; self.set_selected_index(current_index, window, cx); cx.emit(DismissEvent); } } fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) { cx.defer_in(window, |picker, window, cx| { picker.set_query("", window, cx); }); } fn render_match( &self, ix: usize, selected: bool, _: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Option<Self::ListItem> { match self.filtered_entries.get(ix)? { ModelPickerEntry::Separator(title) => { Some(ModelSelectorHeader::new(title, ix > 1).into_any_element()) } ModelPickerEntry::Model(model_info, is_favorite) => { let is_selected = Some(model_info) == self.selected_model.as_ref(); let is_favorite = *is_favorite; let handle_action_click = { let model_id = model_info.id.clone(); let selector = self.selector.clone(); cx.listener(move |_, _, _, cx| { selector.toggle_favorite_model(model_id.clone(), !is_favorite, cx); }) }; let model_cost = model_info.cost.clone(); Some( div() .id(("model-picker-menu-child", ix)) .when_some(model_info.description.clone(), |this, description| { this.on_hover(cx.listener(move |menu, hovered, _, cx| { if *hovered { menu.delegate.selected_description = Some((ix, description.clone())); } else if matches!(menu.delegate.selected_description, Some((id, _)) if id == ix) { menu.delegate.selected_description = None; } cx.notify(); })) }) .child( ModelSelectorListItem::new(ix, model_info.name.clone()) .map(|this| match &model_info.icon { Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()), Some(AgentModelIcon::Named(icon)) => this.icon(*icon), None => this, }) .disabled(model_info.disabled.clone()) .is_selected(is_selected) .is_focused(selected) .is_latest(model_info.is_latest) .is_favorite(is_favorite) .on_toggle_favorite(handle_action_click) .cost_info(model_cost) ) .into_any_element(), ) } } } fn documentation_aside( &self, _window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Option<ui::DocumentationAside> { self.selected_description.as_ref().map(|(_, description)| { let description = description.clone(); let side = documentation_aside_side(cx); DocumentationAside::new( side, Rc::new(move |_| Label::new(description.clone()).into_any_element()), ) }) } fn documentation_aside_index(&self) -> Option<usize> { self.selected_description.as_ref().map(|(ix, _)| *ix) } fn render_footer( &self, _window: &mut Window, _cx: &mut Context<Picker<Self>>, ) -> Option<AnyElement> { let focus_handle = self.focus_handle.clone(); if !self.selector.should_render_footer() { return None; } Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element()) } } fn info_list_to_picker_entries( model_list: AgentModelList, favorites: &HashSet<AgentModelId>, ) -> Vec<ModelPickerEntry> { 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 favorite_models: Vec<_> = all_models .iter() .filter(|m| favorites.contains(&m.id)) .unique_by(|m| &m.id) .collect(); let has_favorites = !favorite_models.is_empty(); if has_favorites { entries.push(ModelPickerEntry::Separator("Favorite".into())); for model in favorite_models { entries.push(ModelPickerEntry::Model((*model).clone(), true)); } } match model_list { AgentModelList::Flat(list) => { if has_favorites { entries.push(ModelPickerEntry::Separator("All".into())); } for model in list { let is_favorite = favorites.contains(&model.id); entries.push(ModelPickerEntry::Model(model, is_favorite)); } } AgentModelList::Grouped(index_map) => { for (group_name, models) in index_map { entries.push(ModelPickerEntry::Separator(group_name.0)); for model in models { let is_favorite = favorites.contains(&model.id); entries.push(ModelPickerEntry::Model(model, is_favorite)); } } } } entries } async fn fuzzy_search( model_list: AgentModelList, query: String, executor: BackgroundExecutor, ) -> AgentModelList { async fn fuzzy_search_list( model_list: Vec<AgentModelInfo>, query: &str, executor: BackgroundExecutor, ) -> Vec<AgentModelInfo> { let candidates = model_list .iter() .enumerate() .map(|(ix, model)| StringMatchCandidate::new(ix, model.name.as_ref())) .collect::<Vec<_>>(); let mut matches = match_strings( &candidates, query, false, true, 100, &Default::default(), executor, ) .await; matches.sort_unstable_by_key(|mat| { let candidate = &candidates[mat.candidate_id]; (Reverse(OrderedFloat(mat.score)), candidate.id) }); matches .into_iter() .map(|mat| model_list[mat.candidate_id].clone()) .collect() } match model_list { AgentModelList::Flat(model_list) => { AgentModelList::Flat(fuzzy_search_list(model_list, &query, executor).await) } AgentModelList::Grouped(index_map) => { let groups = futures::future::join_all(index_map.into_iter().map(|(group_name, models)| { fuzzy_search_list(models, &query, executor.clone()) .map(|results| (group_name, results)) })) .await; AgentModelList::Grouped(IndexMap::from_iter( groups .into_iter() .filter(|(_, results)| !results.is_empty()), )) } } } #[cfg(test)] mod tests { use gpui::{App, TestAppContext, VisualTestContext}; use std::cell::RefCell; use super::*;