config_options.rs

  1use std::{cmp::Reverse, rc::Rc, sync::Arc};
  2
  3use acp_thread::AgentSessionConfigOptions;
  4use agent_client_protocol as acp;
  5use agent_servers::AgentServer;
  6use agent_settings::AgentSettings;
  7use collections::HashSet;
  8use fs::Fs;
  9use fuzzy::StringMatchCandidate;
 10use gpui::{
 11    App, BackgroundExecutor, Context, DismissEvent, Entity, Subscription, Task, Window, prelude::*,
 12};
 13use ordered_float::OrderedFloat;
 14use picker::popover_menu::PickerPopoverMenu;
 15use picker::{Picker, PickerDelegate};
 16use settings::{Settings, SettingsStore};
 17use ui::{
 18    DocumentationSide, ElevationIndex, IconButton, ListItem, ListItemSpacing, PopoverMenuHandle,
 19    Tooltip, prelude::*,
 20};
 21use util::ResultExt as _;
 22
 23use crate::ui::HoldForDefault;
 24
 25const PICKER_THRESHOLD: usize = 5;
 26
 27pub struct ConfigOptionsView {
 28    config_options: Rc<dyn AgentSessionConfigOptions>,
 29    selectors: Vec<Entity<ConfigOptionSelector>>,
 30    agent_server: Rc<dyn AgentServer>,
 31    fs: Arc<dyn Fs>,
 32    config_option_ids: Vec<acp::SessionConfigId>,
 33    _refresh_task: Task<()>,
 34}
 35
 36impl ConfigOptionsView {
 37    pub fn new(
 38        config_options: Rc<dyn AgentSessionConfigOptions>,
 39        agent_server: Rc<dyn AgentServer>,
 40        fs: Arc<dyn Fs>,
 41        window: &mut Window,
 42        cx: &mut Context<Self>,
 43    ) -> Self {
 44        let selectors = Self::build_selectors(&config_options, &agent_server, &fs, window, cx);
 45        let config_option_ids = Self::config_option_ids(&config_options);
 46
 47        let rx = config_options.watch(cx);
 48        let refresh_task = cx.spawn_in(window, async move |this, cx| {
 49            if let Some(mut rx) = rx {
 50                while let Ok(()) = rx.recv().await {
 51                    this.update_in(cx, |this, window, cx| {
 52                        this.refresh_selectors_if_needed(window, cx);
 53                        cx.notify();
 54                    })
 55                    .log_err();
 56                }
 57            }
 58        });
 59
 60        Self {
 61            config_options,
 62            selectors,
 63            agent_server,
 64            fs,
 65            config_option_ids,
 66            _refresh_task: refresh_task,
 67        }
 68    }
 69
 70    pub fn toggle_category_picker(
 71        &mut self,
 72        category: acp::SessionConfigOptionCategory,
 73        window: &mut Window,
 74        cx: &mut Context<Self>,
 75    ) -> bool {
 76        let Some(config_id) = self.first_config_option_id(category) else {
 77            return false;
 78        };
 79
 80        let Some(selector) = self.selector_for_config_id(&config_id, cx) else {
 81            return false;
 82        };
 83
 84        selector.update(cx, |selector, cx| {
 85            selector.toggle_picker(window, cx);
 86        });
 87
 88        true
 89    }
 90
 91    pub fn cycle_category_option(
 92        &mut self,
 93        category: acp::SessionConfigOptionCategory,
 94        favorites_only: bool,
 95        cx: &mut Context<Self>,
 96    ) -> bool {
 97        let Some(config_id) = self.first_config_option_id(category) else {
 98            return false;
 99        };
100
101        let Some(next_value) = self.next_value_for_config(&config_id, favorites_only, cx) else {
102            return false;
103        };
104
105        let task = self
106            .config_options
107            .set_config_option(config_id, next_value, cx);
108
109        cx.spawn(async move |_, _| {
110            if let Err(err) = task.await {
111                log::error!("Failed to set config option: {:?}", err);
112            }
113        })
114        .detach();
115
116        true
117    }
118
119    fn first_config_option_id(
120        &self,
121        category: acp::SessionConfigOptionCategory,
122    ) -> Option<acp::SessionConfigId> {
123        self.config_options
124            .config_options()
125            .into_iter()
126            .find(|option| option.category.as_ref() == Some(&category))
127            .map(|option| option.id)
128    }
129
130    fn selector_for_config_id(
131        &self,
132        config_id: &acp::SessionConfigId,
133        cx: &App,
134    ) -> Option<Entity<ConfigOptionSelector>> {
135        self.selectors
136            .iter()
137            .find(|selector| selector.read(cx).config_id() == config_id)
138            .cloned()
139    }
140
141    fn next_value_for_config(
142        &self,
143        config_id: &acp::SessionConfigId,
144        favorites_only: bool,
145        cx: &mut Context<Self>,
146    ) -> Option<acp::SessionConfigValueId> {
147        let mut options = extract_options(&self.config_options, config_id);
148        if options.is_empty() {
149            return None;
150        }
151
152        if favorites_only {
153            let favorites = self
154                .agent_server
155                .favorite_config_option_value_ids(config_id, cx);
156            options.retain(|option| favorites.contains(&option.value));
157            if options.is_empty() {
158                return None;
159            }
160        }
161
162        let current_value = get_current_value(&self.config_options, config_id);
163        let current_index = current_value
164            .as_ref()
165            .and_then(|current| options.iter().position(|option| &option.value == current))
166            .unwrap_or(usize::MAX);
167
168        let next_index = if current_index == usize::MAX {
169            0
170        } else {
171            (current_index + 1) % options.len()
172        };
173
174        Some(options[next_index].value.clone())
175    }
176
177    fn config_option_ids(
178        config_options: &Rc<dyn AgentSessionConfigOptions>,
179    ) -> Vec<acp::SessionConfigId> {
180        config_options
181            .config_options()
182            .into_iter()
183            .map(|option| option.id)
184            .collect()
185    }
186
187    fn refresh_selectors_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
188        let current_ids = Self::config_option_ids(&self.config_options);
189        if current_ids != self.config_option_ids {
190            self.config_option_ids = current_ids;
191            self.rebuild_selectors(window, cx);
192        }
193    }
194
195    fn rebuild_selectors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
196        self.selectors = Self::build_selectors(
197            &self.config_options,
198            &self.agent_server,
199            &self.fs,
200            window,
201            cx,
202        );
203        cx.notify();
204    }
205
206    fn build_selectors(
207        config_options: &Rc<dyn AgentSessionConfigOptions>,
208        agent_server: &Rc<dyn AgentServer>,
209        fs: &Arc<dyn Fs>,
210        window: &mut Window,
211        cx: &mut Context<Self>,
212    ) -> Vec<Entity<ConfigOptionSelector>> {
213        config_options
214            .config_options()
215            .into_iter()
216            .map(|option| {
217                let config_options = config_options.clone();
218                let agent_server = agent_server.clone();
219                let fs = fs.clone();
220                cx.new(|cx| {
221                    ConfigOptionSelector::new(
222                        config_options,
223                        option.id.clone(),
224                        agent_server,
225                        fs,
226                        window,
227                        cx,
228                    )
229                })
230            })
231            .collect()
232    }
233}
234
235impl Render for ConfigOptionsView {
236    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
237        if self.selectors.is_empty() {
238            return div().into_any_element();
239        }
240
241        h_flex()
242            .gap_1()
243            .children(self.selectors.iter().cloned())
244            .into_any_element()
245    }
246}
247
248struct ConfigOptionSelector {
249    config_options: Rc<dyn AgentSessionConfigOptions>,
250    config_id: acp::SessionConfigId,
251    picker_handle: PopoverMenuHandle<Picker<ConfigOptionPickerDelegate>>,
252    picker: Entity<Picker<ConfigOptionPickerDelegate>>,
253    setting_value: bool,
254}
255
256impl ConfigOptionSelector {
257    pub fn new(
258        config_options: Rc<dyn AgentSessionConfigOptions>,
259        config_id: acp::SessionConfigId,
260        agent_server: Rc<dyn AgentServer>,
261        fs: Arc<dyn Fs>,
262        window: &mut Window,
263        cx: &mut Context<Self>,
264    ) -> Self {
265        let option_count = config_options
266            .config_options()
267            .iter()
268            .find(|opt| opt.id == config_id)
269            .map(count_config_options)
270            .unwrap_or(0);
271
272        let is_searchable = option_count >= PICKER_THRESHOLD;
273
274        let picker = {
275            let config_options = config_options.clone();
276            let config_id = config_id.clone();
277            let agent_server = agent_server.clone();
278            let fs = fs.clone();
279            cx.new(move |picker_cx| {
280                let delegate = ConfigOptionPickerDelegate::new(
281                    config_options,
282                    config_id,
283                    agent_server,
284                    fs,
285                    window,
286                    picker_cx,
287                );
288
289                if is_searchable {
290                    Picker::list(delegate, window, picker_cx)
291                } else {
292                    Picker::nonsearchable_list(delegate, window, picker_cx)
293                }
294                .show_scrollbar(true)
295                .width(rems(20.))
296                .max_height(Some(rems(20.).into()))
297            })
298        };
299
300        Self {
301            config_options,
302            config_id,
303            picker_handle: PopoverMenuHandle::default(),
304            picker,
305            setting_value: false,
306        }
307    }
308
309    fn current_option(&self) -> Option<acp::SessionConfigOption> {
310        self.config_options
311            .config_options()
312            .into_iter()
313            .find(|opt| opt.id == self.config_id)
314    }
315
316    fn config_id(&self) -> &acp::SessionConfigId {
317        &self.config_id
318    }
319
320    fn toggle_picker(&self, window: &mut Window, cx: &mut Context<Self>) {
321        self.picker_handle.toggle(window, cx);
322    }
323
324    fn current_value_name(&self) -> String {
325        let Some(option) = self.current_option() else {
326            return "Unknown".to_string();
327        };
328
329        match &option.kind {
330            acp::SessionConfigKind::Select(select) => {
331                find_option_name(&select.options, &select.current_value)
332                    .unwrap_or_else(|| "Unknown".to_string())
333            }
334            _ => "Unknown".to_string(),
335        }
336    }
337
338    fn render_trigger_button(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Button {
339        let Some(option) = self.current_option() else {
340            return Button::new("config-option-trigger", "Unknown")
341                .label_size(LabelSize::Small)
342                .color(Color::Muted)
343                .disabled(true);
344        };
345
346        let icon = if self.picker_handle.is_deployed() {
347            IconName::ChevronUp
348        } else {
349            IconName::ChevronDown
350        };
351
352        Button::new(
353            ElementId::Name(format!("config-option-{}", option.id.0).into()),
354            self.current_value_name(),
355        )
356        .label_size(LabelSize::Small)
357        .color(Color::Muted)
358        .icon(icon)
359        .icon_size(IconSize::XSmall)
360        .icon_position(IconPosition::End)
361        .icon_color(Color::Muted)
362        .disabled(self.setting_value)
363    }
364}
365
366impl Render for ConfigOptionSelector {
367    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
368        let Some(option) = self.current_option() else {
369            return div().into_any_element();
370        };
371
372        let trigger_button = self.render_trigger_button(window, cx);
373
374        let option_name = option.name.clone();
375        let option_description: Option<SharedString> = option.description.map(Into::into);
376
377        let tooltip = Tooltip::element(move |_window, _cx| {
378            let mut content = v_flex().gap_1().child(Label::new(option_name.clone()));
379            if let Some(desc) = option_description.as_ref() {
380                content = content.child(
381                    Label::new(desc.clone())
382                        .size(LabelSize::Small)
383                        .color(Color::Muted),
384                );
385            }
386            content.into_any()
387        });
388
389        PickerPopoverMenu::new(
390            self.picker.clone(),
391            trigger_button,
392            tooltip,
393            gpui::Corner::BottomRight,
394            cx,
395        )
396        .with_handle(self.picker_handle.clone())
397        .render(window, cx)
398        .into_any_element()
399    }
400}
401
402#[derive(Clone)]
403enum ConfigOptionPickerEntry {
404    Separator(SharedString),
405    Option(ConfigOptionValue),
406}
407
408#[derive(Clone)]
409struct ConfigOptionValue {
410    value: acp::SessionConfigValueId,
411    name: String,
412    description: Option<String>,
413    group: Option<String>,
414}
415
416struct ConfigOptionPickerDelegate {
417    config_options: Rc<dyn AgentSessionConfigOptions>,
418    config_id: acp::SessionConfigId,
419    agent_server: Rc<dyn AgentServer>,
420    fs: Arc<dyn Fs>,
421    filtered_entries: Vec<ConfigOptionPickerEntry>,
422    all_options: Vec<ConfigOptionValue>,
423    selected_index: usize,
424    selected_description: Option<(usize, SharedString, bool)>,
425    favorites: HashSet<acp::SessionConfigValueId>,
426    _settings_subscription: Subscription,
427}
428
429impl ConfigOptionPickerDelegate {
430    fn new(
431        config_options: Rc<dyn AgentSessionConfigOptions>,
432        config_id: acp::SessionConfigId,
433        agent_server: Rc<dyn AgentServer>,
434        fs: Arc<dyn Fs>,
435        window: &mut Window,
436        cx: &mut Context<Picker<Self>>,
437    ) -> Self {
438        let favorites = agent_server.favorite_config_option_value_ids(&config_id, cx);
439
440        let all_options = extract_options(&config_options, &config_id);
441        let filtered_entries = options_to_picker_entries(&all_options, &favorites);
442
443        let current_value = get_current_value(&config_options, &config_id);
444        let selected_index = current_value
445            .and_then(|current| {
446                filtered_entries.iter().position(|entry| {
447                    matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
448                })
449            })
450            .unwrap_or(0);
451
452        let agent_server_for_subscription = agent_server.clone();
453        let config_id_for_subscription = config_id.clone();
454        let settings_subscription =
455            cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
456                let new_favorites = agent_server_for_subscription
457                    .favorite_config_option_value_ids(&config_id_for_subscription, cx);
458                if new_favorites != picker.delegate.favorites {
459                    picker.delegate.favorites = new_favorites;
460                    picker.refresh(window, cx);
461                }
462            });
463
464        cx.notify();
465
466        Self {
467            config_options,
468            config_id,
469            agent_server,
470            fs,
471            filtered_entries,
472            all_options,
473            selected_index,
474            selected_description: None,
475            favorites,
476            _settings_subscription: settings_subscription,
477        }
478    }
479
480    fn current_value(&self) -> Option<acp::SessionConfigValueId> {
481        get_current_value(&self.config_options, &self.config_id)
482    }
483}
484
485impl PickerDelegate for ConfigOptionPickerDelegate {
486    type ListItem = AnyElement;
487
488    fn match_count(&self) -> usize {
489        self.filtered_entries.len()
490    }
491
492    fn selected_index(&self) -> usize {
493        self.selected_index
494    }
495
496    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
497        self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
498        cx.notify();
499    }
500
501    fn can_select(
502        &mut self,
503        ix: usize,
504        _window: &mut Window,
505        _cx: &mut Context<Picker<Self>>,
506    ) -> bool {
507        match self.filtered_entries.get(ix) {
508            Some(ConfigOptionPickerEntry::Option(_)) => true,
509            Some(ConfigOptionPickerEntry::Separator(_)) | None => false,
510        }
511    }
512
513    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
514        "Select an option…".into()
515    }
516
517    fn update_matches(
518        &mut self,
519        query: String,
520        window: &mut Window,
521        cx: &mut Context<Picker<Self>>,
522    ) -> Task<()> {
523        let all_options = self.all_options.clone();
524
525        cx.spawn_in(window, async move |this, cx| {
526            let filtered_options = match this
527                .read_with(cx, |_, cx| {
528                    if query.is_empty() {
529                        None
530                    } else {
531                        Some((all_options.clone(), query.clone(), cx.background_executor().clone()))
532                    }
533                })
534                .ok()
535                .flatten()
536            {
537                Some((options, q, executor)) => fuzzy_search_options(options, &q, executor).await,
538                None => all_options,
539            };
540
541            this.update_in(cx, |this, window, cx| {
542                this.delegate.filtered_entries =
543                    options_to_picker_entries(&filtered_options, &this.delegate.favorites);
544
545                let current_value = this.delegate.current_value();
546                let new_index = current_value
547                    .and_then(|current| {
548                        this.delegate.filtered_entries.iter().position(|entry| {
549                            matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
550                        })
551                    })
552                    .unwrap_or(0);
553
554                this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
555                cx.notify();
556            })
557            .ok();
558        })
559    }
560
561    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
562        if let Some(ConfigOptionPickerEntry::Option(option)) =
563            self.filtered_entries.get(self.selected_index)
564        {
565            if window.modifiers().secondary() {
566                let default_value = self
567                    .agent_server
568                    .default_config_option(self.config_id.0.as_ref(), cx);
569                let is_default = default_value.as_deref() == Some(&*option.value.0);
570
571                self.agent_server.set_default_config_option(
572                    self.config_id.0.as_ref(),
573                    if is_default {
574                        None
575                    } else {
576                        Some(option.value.0.as_ref())
577                    },
578                    self.fs.clone(),
579                    cx,
580                );
581            }
582
583            let task = self.config_options.set_config_option(
584                self.config_id.clone(),
585                option.value.clone(),
586                cx,
587            );
588
589            cx.spawn(async move |_, _| {
590                if let Err(err) = task.await {
591                    log::error!("Failed to set config option: {:?}", err);
592                }
593            })
594            .detach();
595
596            cx.emit(DismissEvent);
597        }
598    }
599
600    fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
601        cx.defer_in(window, |picker, window, cx| {
602            picker.set_query("", window, cx);
603        });
604    }
605
606    fn render_match(
607        &self,
608        ix: usize,
609        selected: bool,
610        _: &mut Window,
611        cx: &mut Context<Picker<Self>>,
612    ) -> Option<Self::ListItem> {
613        match self.filtered_entries.get(ix)? {
614            ConfigOptionPickerEntry::Separator(title) => Some(
615                div()
616                    .when(ix > 0, |this| this.mt_1())
617                    .child(
618                        div()
619                            .px_2()
620                            .py_1()
621                            .text_xs()
622                            .text_color(cx.theme().colors().text_muted)
623                            .child(title.clone()),
624                    )
625                    .into_any_element(),
626            ),
627            ConfigOptionPickerEntry::Option(option) => {
628                let current_value = self.current_value();
629                let is_selected = current_value.as_ref() == Some(&option.value);
630
631                let default_value = self
632                    .agent_server
633                    .default_config_option(self.config_id.0.as_ref(), cx);
634                let is_default = default_value.as_deref() == Some(&*option.value.0);
635
636                let is_favorite = self.favorites.contains(&option.value);
637
638                let option_name = option.name.clone();
639                let description = option.description.clone();
640
641                Some(
642                    div()
643                        .id(("config-option-picker-item", ix))
644                        .when_some(description, |this, desc| {
645                            let desc: SharedString = desc.into();
646                            this.on_hover(cx.listener(move |menu, hovered, _, cx| {
647                                if *hovered {
648                                    menu.delegate.selected_description =
649                                        Some((ix, desc.clone(), is_default));
650                                } else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix)
651                                {
652                                    menu.delegate.selected_description = None;
653                                }
654                                cx.notify();
655                            }))
656                        })
657                        .child(
658                            ListItem::new(ix)
659                                .inset(true)
660                                .spacing(ListItemSpacing::Sparse)
661                                .toggle_state(selected)
662                                .child(h_flex().w_full().child(Label::new(option_name).truncate()))
663                                .end_slot(div().pr_2().when(is_selected, |this| {
664                                    this.child(Icon::new(IconName::Check).color(Color::Accent))
665                                }))
666                                .end_hover_slot(div().pr_1p5().child({
667                                    let (icon, color, tooltip) = if is_favorite {
668                                        (IconName::StarFilled, Color::Accent, "Unfavorite")
669                                    } else {
670                                        (IconName::Star, Color::Default, "Favorite")
671                                    };
672
673                                    let config_id = self.config_id.clone();
674                                    let value_id = option.value.clone();
675                                    let agent_server = self.agent_server.clone();
676                                    let fs = self.fs.clone();
677
678                                    IconButton::new(("toggle-favorite-config-option", ix), icon)
679                                        .layer(ElevationIndex::ElevatedSurface)
680                                        .icon_color(color)
681                                        .icon_size(IconSize::Small)
682                                        .tooltip(Tooltip::text(tooltip))
683                                        .on_click(move |_, _, cx| {
684                                            agent_server.toggle_favorite_config_option_value(
685                                                config_id.clone(),
686                                                value_id.clone(),
687                                                !is_favorite,
688                                                fs.clone(),
689                                                cx,
690                                            );
691                                        })
692                                })),
693                        )
694                        .into_any_element(),
695                )
696            }
697        }
698    }
699
700    fn documentation_aside(
701        &self,
702        _window: &mut Window,
703        cx: &mut Context<Picker<Self>>,
704    ) -> Option<ui::DocumentationAside> {
705        self.selected_description
706            .as_ref()
707            .map(|(_, description, is_default)| {
708                let description = description.clone();
709                let is_default = *is_default;
710
711                let settings = AgentSettings::get_global(cx);
712                let side = match settings.dock {
713                    settings::DockPosition::Left => DocumentationSide::Right,
714                    settings::DockPosition::Bottom | settings::DockPosition::Right => {
715                        DocumentationSide::Left
716                    }
717                };
718
719                ui::DocumentationAside::new(
720                    side,
721                    Rc::new(move |_| {
722                        v_flex()
723                            .gap_1()
724                            .child(Label::new(description.clone()))
725                            .child(HoldForDefault::new(is_default))
726                            .into_any_element()
727                    }),
728                )
729            })
730    }
731
732    fn documentation_aside_index(&self) -> Option<usize> {
733        self.selected_description.as_ref().map(|(ix, _, _)| *ix)
734    }
735}
736
737fn extract_options(
738    config_options: &Rc<dyn AgentSessionConfigOptions>,
739    config_id: &acp::SessionConfigId,
740) -> Vec<ConfigOptionValue> {
741    let Some(option) = config_options
742        .config_options()
743        .into_iter()
744        .find(|opt| &opt.id == config_id)
745    else {
746        return Vec::new();
747    };
748
749    match &option.kind {
750        acp::SessionConfigKind::Select(select) => match &select.options {
751            acp::SessionConfigSelectOptions::Ungrouped(options) => options
752                .iter()
753                .map(|opt| ConfigOptionValue {
754                    value: opt.value.clone(),
755                    name: opt.name.clone(),
756                    description: opt.description.clone(),
757                    group: None,
758                })
759                .collect(),
760            acp::SessionConfigSelectOptions::Grouped(groups) => groups
761                .iter()
762                .flat_map(|group| {
763                    group.options.iter().map(|opt| ConfigOptionValue {
764                        value: opt.value.clone(),
765                        name: opt.name.clone(),
766                        description: opt.description.clone(),
767                        group: Some(group.name.clone()),
768                    })
769                })
770                .collect(),
771            _ => Vec::new(),
772        },
773        _ => Vec::new(),
774    }
775}
776
777fn get_current_value(
778    config_options: &Rc<dyn AgentSessionConfigOptions>,
779    config_id: &acp::SessionConfigId,
780) -> Option<acp::SessionConfigValueId> {
781    config_options
782        .config_options()
783        .into_iter()
784        .find(|opt| &opt.id == config_id)
785        .and_then(|opt| match &opt.kind {
786            acp::SessionConfigKind::Select(select) => Some(select.current_value.clone()),
787            _ => None,
788        })
789}
790
791fn options_to_picker_entries(
792    options: &[ConfigOptionValue],
793    favorites: &HashSet<acp::SessionConfigValueId>,
794) -> Vec<ConfigOptionPickerEntry> {
795    let mut entries = Vec::new();
796
797    let mut favorite_options = Vec::new();
798
799    for option in options {
800        if favorites.contains(&option.value) {
801            favorite_options.push(option.clone());
802        }
803    }
804
805    if !favorite_options.is_empty() {
806        entries.push(ConfigOptionPickerEntry::Separator("Favorites".into()));
807        for option in favorite_options {
808            entries.push(ConfigOptionPickerEntry::Option(option));
809        }
810
811        // If the remaining list would start ungrouped (group == None), insert a separator so
812        // Favorites doesn't visually run into the main list.
813        if let Some(option) = options.first()
814            && option.group.is_none()
815        {
816            entries.push(ConfigOptionPickerEntry::Separator("All Options".into()));
817        }
818    }
819
820    let mut current_group: Option<String> = None;
821    for option in options {
822        if option.group != current_group {
823            if let Some(group_name) = &option.group {
824                entries.push(ConfigOptionPickerEntry::Separator(
825                    group_name.clone().into(),
826                ));
827            }
828            current_group = option.group.clone();
829        }
830        entries.push(ConfigOptionPickerEntry::Option(option.clone()));
831    }
832
833    entries
834}
835
836async fn fuzzy_search_options(
837    options: Vec<ConfigOptionValue>,
838    query: &str,
839    executor: BackgroundExecutor,
840) -> Vec<ConfigOptionValue> {
841    let candidates = options
842        .iter()
843        .enumerate()
844        .map(|(ix, opt)| StringMatchCandidate::new(ix, &opt.name))
845        .collect::<Vec<_>>();
846
847    let mut matches = fuzzy::match_strings(
848        &candidates,
849        query,
850        false,
851        true,
852        100,
853        &Default::default(),
854        executor,
855    )
856    .await;
857
858    matches.sort_unstable_by_key(|mat| {
859        let candidate = &candidates[mat.candidate_id];
860        (Reverse(OrderedFloat(mat.score)), candidate.id)
861    });
862
863    matches
864        .into_iter()
865        .map(|mat| options[mat.candidate_id].clone())
866        .collect()
867}
868
869fn find_option_name(
870    options: &acp::SessionConfigSelectOptions,
871    value_id: &acp::SessionConfigValueId,
872) -> Option<String> {
873    match options {
874        acp::SessionConfigSelectOptions::Ungrouped(opts) => opts
875            .iter()
876            .find(|o| &o.value == value_id)
877            .map(|o| o.name.clone()),
878        acp::SessionConfigSelectOptions::Grouped(groups) => groups.iter().find_map(|group| {
879            group
880                .options
881                .iter()
882                .find(|o| &o.value == value_id)
883                .map(|o| o.name.clone())
884        }),
885        _ => None,
886    }
887}
888
889fn count_config_options(option: &acp::SessionConfigOption) -> usize {
890    match &option.kind {
891        acp::SessionConfigKind::Select(select) => match &select.options {
892            acp::SessionConfigSelectOptions::Ungrouped(options) => options.len(),
893            acp::SessionConfigSelectOptions::Grouped(groups) => {
894                groups.iter().map(|g| g.options.len()).sum()
895            }
896            _ => 0,
897        },
898        _ => 0,
899    }
900}