1use std::{cmp::Reverse, rc::Rc, sync::Arc};
2
3use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
4use agent_client_protocol::ModelId;
5use agent_servers::AgentServer;
6use agent_settings::AgentSettings;
7use anyhow::Result;
8use collections::{HashSet, IndexMap};
9use fs::Fs;
10use futures::FutureExt;
11use fuzzy::{StringMatchCandidate, match_strings};
12use gpui::{
13 Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
14};
15use itertools::Itertools;
16use ordered_float::OrderedFloat;
17use picker::{Picker, PickerDelegate};
18use settings::Settings;
19use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, prelude::*};
20use util::ResultExt;
21use zed_actions::agent::OpenSettings;
22
23use crate::ui::{HoldForDefault, ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem};
24
25pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
26
27pub fn acp_model_selector(
28 selector: Rc<dyn AgentModelSelector>,
29 agent_server: Rc<dyn AgentServer>,
30 fs: Arc<dyn Fs>,
31 focus_handle: FocusHandle,
32 window: &mut Window,
33 cx: &mut Context<AcpModelSelector>,
34) -> AcpModelSelector {
35 let delegate =
36 AcpModelPickerDelegate::new(selector, agent_server, fs, focus_handle, window, cx);
37 Picker::list(delegate, window, cx)
38 .show_scrollbar(true)
39 .width(rems(20.))
40 .max_height(Some(rems(20.).into()))
41}
42
43enum AcpModelPickerEntry {
44 Separator(SharedString),
45 Model(AgentModelInfo, bool),
46}
47
48pub struct AcpModelPickerDelegate {
49 selector: Rc<dyn AgentModelSelector>,
50 agent_server: Rc<dyn AgentServer>,
51 fs: Arc<dyn Fs>,
52 filtered_entries: Vec<AcpModelPickerEntry>,
53 models: Option<AgentModelList>,
54 selected_index: usize,
55 selected_description: Option<(usize, SharedString, bool)>,
56 selected_model: Option<AgentModelInfo>,
57 _refresh_models_task: Task<()>,
58 focus_handle: FocusHandle,
59}
60
61impl AcpModelPickerDelegate {
62 fn new(
63 selector: Rc<dyn AgentModelSelector>,
64 agent_server: Rc<dyn AgentServer>,
65 fs: Arc<dyn Fs>,
66 focus_handle: FocusHandle,
67 window: &mut Window,
68 cx: &mut Context<AcpModelSelector>,
69 ) -> Self {
70 let rx = selector.watch(cx);
71 let refresh_models_task = {
72 cx.spawn_in(window, {
73 async move |this, cx| {
74 async fn refresh(
75 this: &WeakEntity<Picker<AcpModelPickerDelegate>>,
76 cx: &mut AsyncWindowContext,
77 ) -> Result<()> {
78 let (models_task, selected_model_task) = this.update(cx, |this, cx| {
79 (
80 this.delegate.selector.list_models(cx),
81 this.delegate.selector.selected_model(cx),
82 )
83 })?;
84
85 let (models, selected_model) =
86 futures::join!(models_task, selected_model_task);
87
88 this.update_in(cx, |this, window, cx| {
89 this.delegate.models = models.ok();
90 this.delegate.selected_model = selected_model.ok();
91 this.refresh(window, cx)
92 })
93 }
94
95 refresh(&this, cx).await.log_err();
96 if let Some(mut rx) = rx {
97 while let Ok(()) = rx.recv().await {
98 refresh(&this, cx).await.log_err();
99 }
100 }
101 }
102 })
103 };
104
105 Self {
106 selector,
107 agent_server,
108 fs,
109 filtered_entries: Vec::new(),
110 models: None,
111 selected_model: None,
112 selected_index: 0,
113 selected_description: None,
114 _refresh_models_task: refresh_models_task,
115 focus_handle,
116 }
117 }
118
119 pub fn active_model(&self) -> Option<&AgentModelInfo> {
120 self.selected_model.as_ref()
121 }
122
123 pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
124 if !self.selector.supports_favorites() {
125 return;
126 }
127
128 let favorites = AgentSettings::get_global(cx).favorite_model_ids();
129
130 if favorites.is_empty() {
131 return;
132 }
133
134 let Some(models) = self.models.clone() else {
135 return;
136 };
137
138 let all_models: Vec<AgentModelInfo> = match models {
139 AgentModelList::Flat(list) => list,
140 AgentModelList::Grouped(index_map) => index_map
141 .into_values()
142 .flatten()
143 .collect::<Vec<AgentModelInfo>>(),
144 };
145
146 let favorite_models = all_models
147 .iter()
148 .filter(|model| favorites.contains(&model.id))
149 .unique_by(|model| &model.id)
150 .cloned()
151 .collect::<Vec<_>>();
152
153 let current_id = self.selected_model.as_ref().map(|m| m.id.clone());
154
155 let current_index_in_favorites = current_id
156 .as_ref()
157 .and_then(|id| favorite_models.iter().position(|m| &m.id == id))
158 .unwrap_or(usize::MAX);
159
160 let next_index = if current_index_in_favorites == usize::MAX {
161 0
162 } else {
163 (current_index_in_favorites + 1) % favorite_models.len()
164 };
165
166 let next_model = favorite_models[next_index].clone();
167
168 self.selector
169 .select_model(next_model.id.clone(), cx)
170 .detach_and_log_err(cx);
171
172 self.selected_model = Some(next_model);
173
174 // Keep the picker selection aligned with the newly-selected model
175 if let Some(new_index) = self.filtered_entries.iter().position(|entry| {
176 matches!(entry, AcpModelPickerEntry::Model(model_info, _) if self.selected_model.as_ref().is_some_and(|selected| model_info.id == selected.id))
177 }) {
178 self.set_selected_index(new_index, window, cx);
179 } else {
180 cx.notify();
181 }
182 }
183}
184
185impl PickerDelegate for AcpModelPickerDelegate {
186 type ListItem = AnyElement;
187
188 fn match_count(&self) -> usize {
189 self.filtered_entries.len()
190 }
191
192 fn selected_index(&self) -> usize {
193 self.selected_index
194 }
195
196 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
197 self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
198 cx.notify();
199 }
200
201 fn can_select(
202 &mut self,
203 ix: usize,
204 _window: &mut Window,
205 _cx: &mut Context<Picker<Self>>,
206 ) -> bool {
207 match self.filtered_entries.get(ix) {
208 Some(AcpModelPickerEntry::Model(_, _)) => true,
209 Some(AcpModelPickerEntry::Separator(_)) | None => false,
210 }
211 }
212
213 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
214 "Select a model…".into()
215 }
216
217 fn update_matches(
218 &mut self,
219 query: String,
220 window: &mut Window,
221 cx: &mut Context<Picker<Self>>,
222 ) -> Task<()> {
223 let favorites = if self.selector.supports_favorites() {
224 Arc::new(AgentSettings::get_global(cx).favorite_model_ids())
225 } else {
226 Default::default()
227 };
228
229 cx.spawn_in(window, async move |this, cx| {
230 let filtered_models = match this
231 .read_with(cx, |this, cx| {
232 this.delegate.models.clone().map(move |models| {
233 fuzzy_search(models, query, cx.background_executor().clone())
234 })
235 })
236 .ok()
237 .flatten()
238 {
239 Some(task) => task.await,
240 None => AgentModelList::Flat(vec![]),
241 };
242
243 this.update_in(cx, |this, window, cx| {
244 this.delegate.filtered_entries =
245 info_list_to_picker_entries(filtered_models, favorites);
246 // Finds the currently selected model in the list
247 let new_index = this
248 .delegate
249 .selected_model
250 .as_ref()
251 .and_then(|selected| {
252 this.delegate.filtered_entries.iter().position(|entry| {
253 if let AcpModelPickerEntry::Model(model_info, _) = entry {
254 model_info.id == selected.id
255 } else {
256 false
257 }
258 })
259 })
260 .unwrap_or(0);
261 this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
262 cx.notify();
263 })
264 .ok();
265 })
266 }
267
268 fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
269 if let Some(AcpModelPickerEntry::Model(model_info, _)) =
270 self.filtered_entries.get(self.selected_index)
271 {
272 if window.modifiers().secondary() {
273 let default_model = self.agent_server.default_model(cx);
274 let is_default = default_model.as_ref() == Some(&model_info.id);
275
276 self.agent_server.set_default_model(
277 if is_default {
278 None
279 } else {
280 Some(model_info.id.clone())
281 },
282 self.fs.clone(),
283 cx,
284 );
285 }
286
287 self.selector
288 .select_model(model_info.id.clone(), cx)
289 .detach_and_log_err(cx);
290 self.selected_model = Some(model_info.clone());
291 let current_index = self.selected_index;
292 self.set_selected_index(current_index, window, cx);
293
294 cx.emit(DismissEvent);
295 }
296 }
297
298 fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
299 cx.defer_in(window, |picker, window, cx| {
300 picker.set_query("", window, cx);
301 });
302 }
303
304 fn render_match(
305 &self,
306 ix: usize,
307 selected: bool,
308 _: &mut Window,
309 cx: &mut Context<Picker<Self>>,
310 ) -> Option<Self::ListItem> {
311 match self.filtered_entries.get(ix)? {
312 AcpModelPickerEntry::Separator(title) => {
313 Some(ModelSelectorHeader::new(title, ix > 1).into_any_element())
314 }
315 AcpModelPickerEntry::Model(model_info, is_favorite) => {
316 let is_selected = Some(model_info) == self.selected_model.as_ref();
317 let default_model = self.agent_server.default_model(cx);
318 let is_default = default_model.as_ref() == Some(&model_info.id);
319
320 let supports_favorites = self.selector.supports_favorites();
321
322 let is_favorite = *is_favorite;
323 let handle_action_click = {
324 let model_id = model_info.id.clone();
325 let fs = self.fs.clone();
326
327 move |cx: &App| {
328 crate::favorite_models::toggle_model_id_in_settings(
329 model_id.clone(),
330 !is_favorite,
331 fs.clone(),
332 cx,
333 );
334 }
335 };
336
337 Some(
338 div()
339 .id(("model-picker-menu-child", ix))
340 .when_some(model_info.description.clone(), |this, description| {
341 this.on_hover(cx.listener(move |menu, hovered, _, cx| {
342 if *hovered {
343 menu.delegate.selected_description =
344 Some((ix, description.clone(), is_default));
345 } else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) {
346 menu.delegate.selected_description = None;
347 }
348 cx.notify();
349 }))
350 })
351 .child(
352 ModelSelectorListItem::new(ix, model_info.name.clone())
353 .map(|this| match &model_info.icon {
354 Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()),
355 Some(AgentModelIcon::Named(icon)) => this.icon(*icon),
356 None => this,
357 })
358 .is_selected(is_selected)
359 .is_focused(selected)
360 .when(supports_favorites, |this| {
361 this.is_favorite(is_favorite)
362 .on_toggle_favorite(handle_action_click)
363 }),
364 )
365 .into_any_element(),
366 )
367 }
368 }
369 }
370
371 fn documentation_aside(
372 &self,
373 _window: &mut Window,
374 _cx: &mut Context<Picker<Self>>,
375 ) -> Option<ui::DocumentationAside> {
376 self.selected_description
377 .as_ref()
378 .map(|(_, description, is_default)| {
379 let description = description.clone();
380 let is_default = *is_default;
381
382 DocumentationAside::new(
383 DocumentationSide::Left,
384 DocumentationEdge::Top,
385 Rc::new(move |_| {
386 v_flex()
387 .gap_1()
388 .child(Label::new(description.clone()))
389 .child(HoldForDefault::new(is_default))
390 .into_any_element()
391 }),
392 )
393 })
394 }
395
396 fn render_footer(
397 &self,
398 _window: &mut Window,
399 _cx: &mut Context<Picker<Self>>,
400 ) -> Option<AnyElement> {
401 let focus_handle = self.focus_handle.clone();
402
403 if !self.selector.should_render_footer() {
404 return None;
405 }
406
407 Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())
408 }
409}
410
411fn info_list_to_picker_entries(
412 model_list: AgentModelList,
413 favorites: Arc<HashSet<ModelId>>,
414) -> Vec<AcpModelPickerEntry> {
415 let mut entries = Vec::new();
416
417 let all_models: Vec<_> = match &model_list {
418 AgentModelList::Flat(list) => list.iter().collect(),
419 AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
420 };
421
422 let favorite_models: Vec<_> = all_models
423 .iter()
424 .filter(|m| favorites.contains(&m.id))
425 .unique_by(|m| &m.id)
426 .collect();
427
428 let has_favorites = !favorite_models.is_empty();
429 if has_favorites {
430 entries.push(AcpModelPickerEntry::Separator("Favorite".into()));
431 for model in favorite_models {
432 entries.push(AcpModelPickerEntry::Model((*model).clone(), true));
433 }
434 }
435
436 match model_list {
437 AgentModelList::Flat(list) => {
438 if has_favorites {
439 entries.push(AcpModelPickerEntry::Separator("All".into()));
440 }
441 for model in list {
442 let is_favorite = favorites.contains(&model.id);
443 entries.push(AcpModelPickerEntry::Model(model, is_favorite));
444 }
445 }
446 AgentModelList::Grouped(index_map) => {
447 for (group_name, models) in index_map {
448 entries.push(AcpModelPickerEntry::Separator(group_name.0));
449 for model in models {
450 let is_favorite = favorites.contains(&model.id);
451 entries.push(AcpModelPickerEntry::Model(model, is_favorite));
452 }
453 }
454 }
455 }
456
457 entries
458}
459
460async fn fuzzy_search(
461 model_list: AgentModelList,
462 query: String,
463 executor: BackgroundExecutor,
464) -> AgentModelList {
465 async fn fuzzy_search_list(
466 model_list: Vec<AgentModelInfo>,
467 query: &str,
468 executor: BackgroundExecutor,
469 ) -> Vec<AgentModelInfo> {
470 let candidates = model_list
471 .iter()
472 .enumerate()
473 .map(|(ix, model)| StringMatchCandidate::new(ix, model.name.as_ref()))
474 .collect::<Vec<_>>();
475 let mut matches = match_strings(
476 &candidates,
477 query,
478 false,
479 true,
480 100,
481 &Default::default(),
482 executor,
483 )
484 .await;
485
486 matches.sort_unstable_by_key(|mat| {
487 let candidate = &candidates[mat.candidate_id];
488 (Reverse(OrderedFloat(mat.score)), candidate.id)
489 });
490
491 matches
492 .into_iter()
493 .map(|mat| model_list[mat.candidate_id].clone())
494 .collect()
495 }
496
497 match model_list {
498 AgentModelList::Flat(model_list) => {
499 AgentModelList::Flat(fuzzy_search_list(model_list, &query, executor).await)
500 }
501 AgentModelList::Grouped(index_map) => {
502 let groups =
503 futures::future::join_all(index_map.into_iter().map(|(group_name, models)| {
504 fuzzy_search_list(models, &query, executor.clone())
505 .map(|results| (group_name, results))
506 }))
507 .await;
508 AgentModelList::Grouped(IndexMap::from_iter(
509 groups
510 .into_iter()
511 .filter(|(_, results)| !results.is_empty()),
512 ))
513 }
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use agent_client_protocol as acp;
520 use gpui::TestAppContext;
521
522 use super::*;
523
524 fn create_model_list(grouped_models: Vec<(&str, Vec<&str>)>) -> AgentModelList {
525 AgentModelList::Grouped(IndexMap::from_iter(grouped_models.into_iter().map(
526 |(group, models)| {
527 (
528 acp_thread::AgentModelGroupName(group.to_string().into()),
529 models
530 .into_iter()
531 .map(|model| acp_thread::AgentModelInfo {
532 id: acp::ModelId::new(model.to_string()),
533 name: model.to_string().into(),
534 description: None,
535 icon: None,
536 })
537 .collect::<Vec<_>>(),
538 )
539 },
540 )))
541 }
542
543 fn assert_models_eq(result: AgentModelList, expected: Vec<(&str, Vec<&str>)>) {
544 let AgentModelList::Grouped(groups) = result else {
545 panic!("Expected LanguageModelInfoList::Grouped, got {:?}", result);
546 };
547
548 assert_eq!(
549 groups.len(),
550 expected.len(),
551 "Number of groups doesn't match"
552 );
553
554 for (i, (expected_group, expected_models)) in expected.iter().enumerate() {
555 let (actual_group, actual_models) = groups.get_index(i).unwrap();
556 assert_eq!(
557 actual_group.0.as_ref(),
558 *expected_group,
559 "Group at position {} doesn't match expected group",
560 i
561 );
562 assert_eq!(
563 actual_models.len(),
564 expected_models.len(),
565 "Number of models in group {} doesn't match",
566 expected_group
567 );
568
569 for (j, expected_model_name) in expected_models.iter().enumerate() {
570 assert_eq!(
571 actual_models[j].name, *expected_model_name,
572 "Model at position {} in group {} doesn't match expected model",
573 j, expected_group
574 );
575 }
576 }
577 }
578
579 fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
580 Arc::new(
581 models
582 .into_iter()
583 .map(|m| ModelId::new(m.to_string()))
584 .collect(),
585 )
586 }
587
588 fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
589 entries
590 .iter()
591 .filter_map(|entry| match entry {
592 AcpModelPickerEntry::Model(info, _) => Some(info.id.0.as_ref()),
593 _ => None,
594 })
595 .collect()
596 }
597
598 fn get_entry_labels(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
599 entries
600 .iter()
601 .map(|entry| match entry {
602 AcpModelPickerEntry::Model(info, _) => info.id.0.as_ref(),
603 AcpModelPickerEntry::Separator(s) => &s,
604 })
605 .collect()
606 }
607
608 #[gpui::test]
609 fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
610 let models = create_model_list(vec![
611 ("zed", vec!["zed/claude", "zed/gemini"]),
612 ("openai", vec!["openai/gpt-5"]),
613 ]);
614 let favorites = create_favorites(vec!["zed/gemini"]);
615
616 let entries = info_list_to_picker_entries(models, favorites);
617
618 assert!(matches!(
619 entries.first(),
620 Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
621 ));
622
623 let model_ids = get_entry_model_ids(&entries);
624 assert_eq!(model_ids[0], "zed/gemini");
625 }
626
627 #[gpui::test]
628 fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
629 let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
630 let favorites = create_favorites(vec![]);
631
632 let entries = info_list_to_picker_entries(models, favorites);
633
634 assert!(matches!(
635 entries.first(),
636 Some(AcpModelPickerEntry::Separator(s)) if s == "zed"
637 ));
638 }
639
640 #[gpui::test]
641 fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
642 let models = create_model_list(vec![
643 ("zed", vec!["zed/claude", "zed/gemini"]),
644 ("openai", vec!["openai/gpt-5"]),
645 ]);
646 let favorites = create_favorites(vec!["zed/claude"]);
647
648 let entries = info_list_to_picker_entries(models, favorites);
649
650 for entry in &entries {
651 if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
652 if info.id.0.as_ref() == "zed/claude" {
653 assert!(is_favorite, "zed/claude should be a favorite");
654 } else {
655 assert!(!is_favorite, "{} should not be a favorite", info.id.0);
656 }
657 }
658 }
659 }
660
661 #[gpui::test]
662 fn test_favorites_appear_in_both_sections(_cx: &mut TestAppContext) {
663 let models = create_model_list(vec![
664 ("zed", vec!["zed/claude", "zed/gemini"]),
665 ("openai", vec!["openai/gpt-5", "openai/gpt-4"]),
666 ]);
667 let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
668
669 let entries = info_list_to_picker_entries(models, favorites);
670 let model_ids = get_entry_model_ids(&entries);
671
672 assert_eq!(model_ids[0], "zed/gemini");
673 assert_eq!(model_ids[1], "openai/gpt-5");
674
675 assert!(model_ids[2..].contains(&"zed/gemini"));
676 assert!(model_ids[2..].contains(&"openai/gpt-5"));
677 }
678
679 #[gpui::test]
680 fn test_favorites_are_not_duplicated_when_repeated_in_other_sections(_cx: &mut TestAppContext) {
681 let models = create_model_list(vec![
682 ("Recommended", vec!["zed/claude", "anthropic/claude"]),
683 ("Zed", vec!["zed/claude", "zed/gpt-5"]),
684 ("Antropic", vec!["anthropic/claude"]),
685 ("OpenAI", vec!["openai/gpt-5"]),
686 ]);
687
688 let favorites = create_favorites(vec!["zed/claude"]);
689
690 let entries = info_list_to_picker_entries(models, favorites);
691 let labels = get_entry_labels(&entries);
692
693 assert_eq!(
694 labels,
695 vec![
696 "Favorite",
697 "zed/claude",
698 "Recommended",
699 "zed/claude",
700 "anthropic/claude",
701 "Zed",
702 "zed/claude",
703 "zed/gpt-5",
704 "Antropic",
705 "anthropic/claude",
706 "OpenAI",
707 "openai/gpt-5"
708 ]
709 );
710 }
711
712 #[gpui::test]
713 fn test_flat_model_list_with_favorites(_cx: &mut TestAppContext) {
714 let models = AgentModelList::Flat(vec![
715 acp_thread::AgentModelInfo {
716 id: acp::ModelId::new("zed/claude".to_string()),
717 name: "Claude".into(),
718 description: None,
719 icon: None,
720 },
721 acp_thread::AgentModelInfo {
722 id: acp::ModelId::new("zed/gemini".to_string()),
723 name: "Gemini".into(),
724 description: None,
725 icon: None,
726 },
727 ]);
728 let favorites = create_favorites(vec!["zed/gemini"]);
729
730 let entries = info_list_to_picker_entries(models, favorites);
731
732 assert!(matches!(
733 entries.first(),
734 Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
735 ));
736
737 assert!(entries.iter().any(|e| matches!(
738 e,
739 AcpModelPickerEntry::Separator(s) if s == "All"
740 )));
741 }
742
743 #[gpui::test]
744 async fn test_fuzzy_match(cx: &mut TestAppContext) {
745 let models = create_model_list(vec![
746 (
747 "zed",
748 vec![
749 "Claude 3.7 Sonnet",
750 "Claude 3.7 Sonnet Thinking",
751 "gpt-4.1",
752 "gpt-4.1-nano",
753 ],
754 ),
755 ("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]),
756 ("ollama", vec!["mistral", "deepseek"]),
757 ]);
758
759 // Results should preserve models order whenever possible.
760 // In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical
761 // similarity scores, but `zed/gpt-4.1` was higher in the models list,
762 // so it should appear first in the results.
763 let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await;
764 assert_models_eq(
765 results,
766 vec![
767 ("zed", vec!["gpt-4.1", "gpt-4.1-nano"]),
768 ("openai", vec!["gpt-4.1", "gpt-4.1-nano"]),
769 ],
770 );
771
772 // Fuzzy search
773 let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await;
774 assert_models_eq(
775 results,
776 vec![
777 ("zed", vec!["gpt-4.1-nano"]),
778 ("openai", vec!["gpt-4.1-nano"]),
779 ],
780 );
781 }
782}