Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:58:04.586Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

config_options.rs

1376 lines · 49.3 KB · rust
1use std::{cmp::Reverse, rc::Rc, sync::Arc};
2
3use acp_thread::AgentSessionConfigOptions;
4use agent_client_protocol::schema::v1 as acp;
5use agent_servers::AgentServer;
6
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::{AgentConfigOptionValue, SettingsStore};
17use ui::{
18    ElevationIndex, IconButton, KeyBinding, ListItem, ListItemSpacing, PopoverMenuHandle, Switch,
19    SwitchLabelPosition, ToggleState, Tooltip, prelude::*,
20};
21use unicode_segmentation::UnicodeSegmentation;
22use util::ResultExt as _;
23use zed_actions::agent::ToggleModelSelector;
24
25use crate::ui::documentation_aside_side;
26use crate::{
27    CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort, ToggleProfileSelector,
28    ToggleThinkingEffortMenu,
29};
30
31const PICKER_THRESHOLD: usize = 5;
32
33pub struct ConfigOptionsView {
34    config_options: Rc<dyn AgentSessionConfigOptions>,
35    selectors: Vec<Entity<ConfigOptionSelector>>,
36    agent_server: Rc<dyn AgentServer>,
37    fs: Arc<dyn Fs>,
38    config_option_ids: Vec<acp::SessionConfigId>,
39    _refresh_task: Task<()>,
40}
41
42impl ConfigOptionsView {
43    pub fn new(
44        config_options: Rc<dyn AgentSessionConfigOptions>,
45        agent_server: Rc<dyn AgentServer>,
46        fs: Arc<dyn Fs>,
47        window: &mut Window,
48        cx: &mut Context<Self>,
49    ) -> Self {
50        let selectors = Self::build_selectors(&config_options, &agent_server, &fs, window, cx);
51        let config_option_ids = Self::config_option_ids(&config_options);
52
53        let rx = config_options.watch(cx);
54        let refresh_task = cx.spawn_in(window, async move |this, cx| {
55            if let Some(mut rx) = rx {
56                while let Ok(()) = rx.recv().await {
57                    this.update_in(cx, |this, window, cx| {
58                        this.rebuild_selectors(window, cx);
59                        cx.notify();
60                    })
61                    .log_err();
62                }
63            }
64        });
65
66        Self {
67            config_options,
68            selectors,
69            agent_server,
70            fs,
71            config_option_ids,
72            _refresh_task: refresh_task,
73        }
74    }
75
76    pub fn toggle_category_picker(
77        &mut self,
78        category: acp::SessionConfigOptionCategory,
79        window: &mut Window,
80        cx: &mut Context<Self>,
81    ) -> bool {
82        let Some(config_id) = self.first_config_option_id_matching(category, |option| {
83            matches!(&option.kind, acp::SessionConfigKind::Select(_))
84        }) else {
85            return false;
86        };
87
88        let Some(selector) = self.selector_for_config_id(&config_id, cx) else {
89            return false;
90        };
91
92        selector.update(cx, |selector, cx| selector.toggle_picker(window, cx))
93    }
94
95    pub fn cycle_category_option(
96        &mut self,
97        category: acp::SessionConfigOptionCategory,
98        favorites_only: bool,
99        cx: &mut Context<Self>,
100    ) -> bool {
101        let Some(config_id) = self.first_config_option_id_matching(category, |option| {
102            Self::can_cycle_config_option(option, favorites_only)
103        }) else {
104            return false;
105        };
106
107        let Some(next_value) = self.next_value_for_config(&config_id, favorites_only, cx) else {
108            return false;
109        };
110        let default_value = setting_value_for_config_option_value(&next_value);
111
112        self.agent_server.set_default_config_option(
113            config_id.0.as_ref(),
114            default_value,
115            self.fs.clone(),
116            cx,
117        );
118
119        let task = self
120            .config_options
121            .set_config_option(config_id, next_value, cx);
122
123        cx.spawn(async move |_, _| {
124            if let Err(err) = task.await {
125                log::error!("Failed to set config option: {:?}", err);
126            }
127        })
128        .detach();
129
130        true
131    }
132
133    fn first_config_option_id_matching(
134        &self,
135        category: acp::SessionConfigOptionCategory,
136        predicate: impl Fn(&acp::SessionConfigOption) -> bool,
137    ) -> Option<acp::SessionConfigId> {
138        self.config_options
139            .config_options()
140            .into_iter()
141            .find(|option| option.category.as_ref() == Some(&category) && predicate(option))
142            .map(|option| option.id)
143    }
144
145    fn can_cycle_config_option(option: &acp::SessionConfigOption, favorites_only: bool) -> bool {
146        match &option.kind {
147            acp::SessionConfigKind::Select(_) => true,
148            acp::SessionConfigKind::Boolean(_) => !favorites_only,
149            _ => false,
150        }
151    }
152
153    fn selector_for_config_id(
154        &self,
155        config_id: &acp::SessionConfigId,
156        cx: &App,
157    ) -> Option<Entity<ConfigOptionSelector>> {
158        self.selectors
159            .iter()
160            .find(|selector| selector.read(cx).config_id() == config_id)
161            .cloned()
162    }
163
164    fn next_value_for_config(
165        &self,
166        config_id: &acp::SessionConfigId,
167        favorites_only: bool,
168        cx: &mut Context<Self>,
169    ) -> Option<acp::SessionConfigOptionValue> {
170        let option = self
171            .config_options
172            .config_options()
173            .into_iter()
174            .find(|option| &option.id == config_id)?;
175
176        match &option.kind {
177            acp::SessionConfigKind::Select(_) => {
178                let mut options = extract_options(&self.config_options, config_id);
179                if options.is_empty() {
180                    return None;
181                }
182
183                if favorites_only {
184                    let favorites = self
185                        .agent_server
186                        .favorite_config_option_value_ids(config_id, cx);
187                    options.retain(|option| favorites.contains(&option.value));
188                    if options.is_empty() {
189                        return None;
190                    }
191                }
192
193                let current_value = get_current_select_value(&self.config_options, config_id);
194                let current_index = current_value
195                    .as_ref()
196                    .and_then(|current| options.iter().position(|option| &option.value == current))
197                    .unwrap_or(usize::MAX);
198
199                let next_index = if current_index == usize::MAX {
200                    0
201                } else {
202                    (current_index + 1) % options.len()
203                };
204
205                Some(acp::SessionConfigOptionValue::value_id(
206                    options[next_index].value.clone(),
207                ))
208            }
209            acp::SessionConfigKind::Boolean(boolean) => {
210                if favorites_only {
211                    None
212                } else {
213                    Some(acp::SessionConfigOptionValue::boolean(
214                        !boolean.current_value,
215                    ))
216                }
217            }
218            _ => None,
219        }
220    }
221
222    fn config_option_ids(
223        config_options: &Rc<dyn AgentSessionConfigOptions>,
224    ) -> Vec<acp::SessionConfigId> {
225        config_options
226            .config_options()
227            .into_iter()
228            .map(|option| option.id)
229            .collect()
230    }
231
232    fn rebuild_selectors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
233        // Config option updates can mutate option values for existing IDs (for example,
234        // reasoning levels after a model switch). Rebuild to refresh cached picker entries.
235        self.config_option_ids = Self::config_option_ids(&self.config_options);
236        self.selectors = Self::build_selectors(
237            &self.config_options,
238            &self.agent_server,
239            &self.fs,
240            window,
241            cx,
242        );
243        cx.notify();
244    }
245
246    fn build_selectors(
247        config_options: &Rc<dyn AgentSessionConfigOptions>,
248        agent_server: &Rc<dyn AgentServer>,
249        fs: &Arc<dyn Fs>,
250        window: &mut Window,
251        cx: &mut Context<Self>,
252    ) -> Vec<Entity<ConfigOptionSelector>> {
253        config_options
254            .config_options()
255            .into_iter()
256            .map(|option| {
257                let config_options = config_options.clone();
258                let agent_server = agent_server.clone();
259                let fs = fs.clone();
260                cx.new(|cx| {
261                    ConfigOptionSelector::new(
262                        config_options,
263                        option.id.clone(),
264                        agent_server,
265                        fs,
266                        window,
267                        cx,
268                    )
269                })
270            })
271            .collect()
272    }
273}
274
275impl Render for ConfigOptionsView {
276    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
277        if self.selectors.is_empty() {
278            return div().into_any_element();
279        }
280
281        h_flex()
282            .min_w_0()
283            .flex_wrap()
284            .gap_1()
285            .children(self.selectors.iter().cloned())
286            .into_any_element()
287    }
288}
289
290struct ConfigOptionSelector {
291    config_options: Rc<dyn AgentSessionConfigOptions>,
292    config_id: acp::SessionConfigId,
293    agent_server: Rc<dyn AgentServer>,
294    fs: Arc<dyn Fs>,
295    picker_handle: Option<PopoverMenuHandle<Picker<ConfigOptionPickerDelegate>>>,
296    picker: Option<Entity<Picker<ConfigOptionPickerDelegate>>>,
297    setting_value: bool,
298}
299
300impl ConfigOptionSelector {
301    pub fn new(
302        config_options: Rc<dyn AgentSessionConfigOptions>,
303        config_id: acp::SessionConfigId,
304        agent_server: Rc<dyn AgentServer>,
305        fs: Arc<dyn Fs>,
306        window: &mut Window,
307        cx: &mut Context<Self>,
308    ) -> Self {
309        let current_option = config_options
310            .config_options()
311            .into_iter()
312            .find(|opt| opt.id == config_id);
313        let option_count = current_option
314            .as_ref()
315            .map(count_config_options)
316            .unwrap_or(0);
317        let is_select = current_option
318            .as_ref()
319            .is_some_and(|option| matches!(&option.kind, acp::SessionConfigKind::Select(_)));
320
321        let is_searchable = option_count >= PICKER_THRESHOLD;
322
323        let (picker_handle, picker) = if is_select {
324            let config_options = config_options.clone();
325            let config_id = config_id.clone();
326            let agent_server = agent_server.clone();
327            let fs = fs.clone();
328            let picker = cx.new(move |picker_cx| {
329                let delegate = ConfigOptionPickerDelegate::new(
330                    config_options,
331                    config_id,
332                    agent_server,
333                    fs,
334                    window,
335                    picker_cx,
336                );
337
338                if is_searchable {
339                    Picker::list(delegate, window, picker_cx)
340                } else {
341                    Picker::nonsearchable_list(delegate, window, picker_cx)
342                }
343                .show_scrollbar(true)
344                .initial_width(rems(20.))
345            });
346            (Some(PopoverMenuHandle::default()), Some(picker))
347        } else {
348            (None, None)
349        };
350
351        Self {
352            config_options,
353            config_id,
354            agent_server,
355            fs,
356            picker_handle,
357            picker,
358            setting_value: false,
359        }
360    }
361
362    fn current_option(&self) -> Option<acp::SessionConfigOption> {
363        self.config_options
364            .config_options()
365            .into_iter()
366            .find(|opt| opt.id == self.config_id)
367    }
368
369    fn config_id(&self) -> &acp::SessionConfigId {
370        &self.config_id
371    }
372
373    fn toggle_picker(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
374        if let Some(picker_handle) = &self.picker_handle {
375            picker_handle.toggle(window, cx);
376            true
377        } else {
378            false
379        }
380    }
381
382    fn current_value_name(&self) -> String {
383        let Some(option) = self.current_option() else {
384            return "Unknown".to_string();
385        };
386
387        match &option.kind {
388            acp::SessionConfigKind::Select(select) => {
389                find_option_name(&select.options, &select.current_value)
390                    .unwrap_or_else(|| "Unknown".to_string())
391            }
392            _ => "Unknown".to_string(),
393        }
394    }
395
396    fn handles_category_keybindings(&self, category: &acp::SessionConfigOptionCategory) -> bool {
397        self.config_options
398            .config_options()
399            .into_iter()
400            .find(|option| {
401                option.category.as_ref() == Some(category)
402                    && matches!(&option.kind, acp::SessionConfigKind::Select(_))
403            })
404            .is_some_and(|option| option.id == self.config_id)
405    }
406
407    fn render_trigger_button(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Button {
408        let Some(option) = self.current_option() else {
409            return Button::new("config-option-trigger", "Unknown")
410                .label_size(LabelSize::Small)
411                .color(Color::Muted)
412                .disabled(true);
413        };
414
415        let picker_deployed = self
416            .picker_handle
417            .as_ref()
418            .is_some_and(|picker_handle| picker_handle.is_deployed());
419        let icon = if picker_deployed {
420            IconName::ChevronUp
421        } else {
422            IconName::ChevronDown
423        };
424
425        let value_name = self.current_value_name();
426        let mut graphemes = value_name.graphemes(true);
427        let truncated = graphemes.by_ref().take(32).collect::<String>();
428        let display_name = if graphemes.next().is_some() {
429            format!("{truncated}…")
430        } else {
431            truncated
432        };
433
434        Button::new(
435            ElementId::Name(format!("config-option-{}", option.id.0).into()),
436            display_name,
437        )
438        .label_size(LabelSize::Small)
439        .color(Color::Muted)
440        .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
441        .disabled(self.setting_value)
442    }
443}
444
445impl Render for ConfigOptionSelector {
446    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
447        let Some(option) = self.current_option() else {
448            return div().into_any_element();
449        };
450
451        match &option.kind {
452            acp::SessionConfigKind::Select(_) => {
453                let (Some(picker), Some(picker_handle)) =
454                    (self.picker.clone(), self.picker_handle.clone())
455                else {
456                    return div().into_any_element();
457                };
458
459                let trigger_button = self.render_trigger_button(window, cx);
460
461                let show_category_keybindings = option
462                    .category
463                    .as_ref()
464                    .is_some_and(|category| self.handles_category_keybindings(category));
465                let option_category = option.category.clone();
466                let option_name = option.name.clone();
467                let option_description: Option<SharedString> =
468                    option.description.clone().map(Into::into);
469
470                let tooltip = Tooltip::element(move |_window, cx| {
471                    let mut content = v_flex().gap_1().child(Label::new(option_name.clone()));
472                    if let Some(desc) = option_description.as_ref() {
473                        content = content.child(
474                            Label::new(desc.clone())
475                                .size(LabelSize::Small)
476                                .color(Color::Muted),
477                        );
478                    }
479
480                    let action_tooltip_container = |label: &str, keybinding: KeyBinding| {
481                        h_flex()
482                            .pt_1()
483                            .gap_2()
484                            .justify_between()
485                            .border_t_1()
486                            .border_color(cx.theme().colors().border_variant)
487                            .child(Label::new(label))
488                            .child(keybinding)
489                    };
490
491                    if show_category_keybindings && let Some(category) = &option_category {
492                        match category {
493                            acp::SessionConfigOptionCategory::Mode => {
494                                content = content
495                                    .child(action_tooltip_container(
496                                        "Change Mode",
497                                        KeyBinding::for_action(&ToggleProfileSelector, cx),
498                                    ))
499                                    .child(action_tooltip_container(
500                                        "Cycle Through Modes",
501                                        KeyBinding::for_action(&CycleModeSelector, cx),
502                                    ));
503                            }
504                            acp::SessionConfigOptionCategory::Model => {
505                                content = content
506                                    .child(action_tooltip_container(
507                                        "Change Model",
508                                        KeyBinding::for_action(&ToggleModelSelector, cx),
509                                    ))
510                                    .child(action_tooltip_container(
511                                        "Cycle Favorite Models",
512                                        KeyBinding::for_action(&CycleFavoriteModels, cx),
513                                    ));
514                            }
515                            acp::SessionConfigOptionCategory::ThoughtLevel => {
516                                content = content
517                                    .child(action_tooltip_container(
518                                        "Change Thinking Effort",
519                                        KeyBinding::for_action(&ToggleThinkingEffortMenu, cx),
520                                    ))
521                                    .child(action_tooltip_container(
522                                        "Cycle Thinking Effort",
523                                        KeyBinding::for_action(&CycleThinkingEffort, cx),
524                                    ));
525                            }
526                            _ => {}
527                        }
528                    }
529                    content.into_any()
530                });
531
532                PickerPopoverMenu::new(
533                    picker,
534                    trigger_button,
535                    tooltip,
536                    gpui::Anchor::BottomRight,
537                    cx,
538                )
539                .with_handle(picker_handle)
540                .render(window, cx)
541                .into_any_element()
542            }
543            acp::SessionConfigKind::Boolean(boolean) => {
544                let option_id = option.id.clone();
545                let option_name: SharedString = option.name.clone().into();
546                let option_description: Option<SharedString> =
547                    option.description.clone().map(Into::into);
548                let tooltip_name = option_name.clone();
549                let tooltip = Tooltip::element(move |_window, _cx| {
550                    let mut content = v_flex().gap_1().child(Label::new(tooltip_name.clone()));
551                    if let Some(desc) = option_description.as_ref() {
552                        content = content.child(
553                            Label::new(desc.clone())
554                                .size(LabelSize::Small)
555                                .color(Color::Muted),
556                        );
557                    }
558                    content.into_any()
559                });
560
561                let config_id = self.config_id.clone();
562                let config_options = self.config_options.clone();
563                let agent_server = self.agent_server.clone();
564                let fs = self.fs.clone();
565                let current_value = boolean.current_value;
566                let toggle_state = if current_value {
567                    ToggleState::Selected
568                } else {
569                    ToggleState::Unselected
570                };
571
572                h_flex()
573                    .id(ElementId::Name(
574                        format!("config-option-{}", option_id.0).into(),
575                    ))
576                    .pr_1()
577                    .tooltip(tooltip)
578                    .child(
579                        Switch::new(
580                            ElementId::Name(format!("config-option-{}-switch", option_id.0).into()),
581                            toggle_state,
582                        )
583                        .label(option_name)
584                        .label_position(SwitchLabelPosition::Start)
585                        .label_size(LabelSize::Small)
586                        .label_color(Color::Muted)
587                        .disabled(self.setting_value)
588                        .on_click(move |state, _window, cx| {
589                            let next_value = matches!(state, ToggleState::Selected);
590                            agent_server.set_default_config_option(
591                                config_id.0.as_ref(),
592                                Some(AgentConfigOptionValue::Boolean(next_value)),
593                                fs.clone(),
594                                cx,
595                            );
596
597                            let task = config_options.set_config_option(
598                                config_id.clone(),
599                                acp::SessionConfigOptionValue::boolean(next_value),
600                                cx,
601                            );
602
603                            cx.spawn(async move |_| {
604                                if let Err(err) = task.await {
605                                    log::error!("Failed to set config option: {:?}", err);
606                                }
607                            })
608                            .detach();
609                        }),
610                    )
611                    .into_any_element()
612            }
613            _ => div().into_any_element(),
614        }
615    }
616}
617
618#[derive(Clone)]
619enum ConfigOptionPickerEntry {
620    Separator(SharedString),
621    Option(ConfigOptionValue),
622}
623
624#[derive(Clone)]
625struct ConfigOptionValue {
626    value: acp::SessionConfigValueId,
627    name: String,
628    description: Option<String>,
629    group: Option<String>,
630}
631
632struct ConfigOptionPickerDelegate {
633    config_options: Rc<dyn AgentSessionConfigOptions>,
634    config_id: acp::SessionConfigId,
635    agent_server: Rc<dyn AgentServer>,
636    fs: Arc<dyn Fs>,
637    filtered_entries: Vec<ConfigOptionPickerEntry>,
638    all_options: Vec<ConfigOptionValue>,
639    selected_index: usize,
640    selected_description: Option<(usize, SharedString)>,
641    favorites: HashSet<acp::SessionConfigValueId>,
642    _settings_subscription: Subscription,
643}
644
645impl ConfigOptionPickerDelegate {
646    fn new(
647        config_options: Rc<dyn AgentSessionConfigOptions>,
648        config_id: acp::SessionConfigId,
649        agent_server: Rc<dyn AgentServer>,
650        fs: Arc<dyn Fs>,
651        window: &mut Window,
652        cx: &mut Context<Picker<Self>>,
653    ) -> Self {
654        let favorites = agent_server.favorite_config_option_value_ids(&config_id, cx);
655
656        let all_options = extract_options(&config_options, &config_id);
657        let filtered_entries = options_to_picker_entries(&all_options, &favorites);
658
659        let current_value = get_current_select_value(&config_options, &config_id);
660        let selected_index = current_value
661            .and_then(|current| {
662                filtered_entries.iter().position(|entry| {
663                    matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
664                })
665            })
666            .unwrap_or(0);
667
668        let agent_server_for_subscription = agent_server.clone();
669        let config_id_for_subscription = config_id.clone();
670        let settings_subscription =
671            cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
672                let new_favorites = agent_server_for_subscription
673                    .favorite_config_option_value_ids(&config_id_for_subscription, cx);
674                if new_favorites != picker.delegate.favorites {
675                    picker.delegate.favorites = new_favorites;
676                    picker.refresh(window, cx);
677                }
678            });
679
680        cx.notify();
681
682        Self {
683            config_options,
684            config_id,
685            agent_server,
686            fs,
687            filtered_entries,
688            all_options,
689            selected_index,
690            selected_description: None,
691            favorites,
692            _settings_subscription: settings_subscription,
693        }
694    }
695
696    fn current_value(&self) -> Option<acp::SessionConfigValueId> {
697        get_current_select_value(&self.config_options, &self.config_id)
698    }
699}
700
701impl PickerDelegate for ConfigOptionPickerDelegate {
702    type ListItem = AnyElement;
703
704    fn name() -> &'static str {
705        "config options"
706    }
707
708    fn match_count(&self) -> usize {
709        self.filtered_entries.len()
710    }
711
712    fn selected_index(&self) -> usize {
713        self.selected_index
714    }
715
716    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
717        self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
718        cx.notify();
719    }
720
721    fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
722        match self.filtered_entries.get(ix) {
723            Some(ConfigOptionPickerEntry::Option(_)) => true,
724            Some(ConfigOptionPickerEntry::Separator(_)) | None => false,
725        }
726    }
727
728    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
729        "Select an option…".into()
730    }
731
732    fn update_matches(
733        &mut self,
734        query: String,
735        window: &mut Window,
736        cx: &mut Context<Picker<Self>>,
737    ) -> Task<()> {
738        let all_options = self.all_options.clone();
739
740        cx.spawn_in(window, async move |this, cx| {
741            let filtered_options = match this
742                .read_with(cx, |_, cx| {
743                    if query.is_empty() {
744                        None
745                    } else {
746                        Some((all_options.clone(), query.clone(), cx.background_executor().clone()))
747                    }
748                })
749                .ok()
750                .flatten()
751            {
752                Some((options, q, executor)) => fuzzy_search_options(options, &q, executor).await,
753                None => all_options,
754            };
755
756            this.update_in(cx, |this, window, cx| {
757                this.delegate.filtered_entries =
758                    options_to_picker_entries(&filtered_options, &this.delegate.favorites);
759
760                let current_value = this.delegate.current_value();
761                let new_index = current_value
762                    .and_then(|current| {
763                        this.delegate.filtered_entries.iter().position(|entry| {
764                            matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
765                        })
766                    })
767                    .unwrap_or(0);
768
769                this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
770                cx.notify();
771            })
772            .ok();
773        })
774    }
775
776    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
777        if let Some(ConfigOptionPickerEntry::Option(option)) =
778            self.filtered_entries.get(self.selected_index)
779        {
780            self.agent_server.set_default_config_option(
781                self.config_id.0.as_ref(),
782                Some(AgentConfigOptionValue::ValueId(option.value.0.to_string())),
783                self.fs.clone(),
784                cx,
785            );
786            let task = self.config_options.set_config_option(
787                self.config_id.clone(),
788                acp::SessionConfigOptionValue::value_id(option.value.clone()),
789                cx,
790            );
791
792            cx.spawn(async move |_, _| {
793                if let Err(err) = task.await {
794                    log::error!("Failed to set config option: {:?}", err);
795                }
796            })
797            .detach();
798
799            cx.emit(DismissEvent);
800        }
801    }
802
803    fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
804        cx.defer_in(window, |picker, window, cx| {
805            picker.set_query("", window, cx);
806        });
807    }
808
809    fn render_match(
810        &self,
811        ix: usize,
812        selected: bool,
813        _: &mut Window,
814        cx: &mut Context<Picker<Self>>,
815    ) -> Option<Self::ListItem> {
816        match self.filtered_entries.get(ix)? {
817            ConfigOptionPickerEntry::Separator(title) => Some(
818                div()
819                    .when(ix > 0, |this| this.mt_1())
820                    .child(
821                        div()
822                            .px_2()
823                            .py_1()
824                            .text_xs()
825                            .text_color(cx.theme().colors().text_muted)
826                            .child(title.clone()),
827                    )
828                    .into_any_element(),
829            ),
830            ConfigOptionPickerEntry::Option(option) => {
831                let current_value = self.current_value();
832                let is_selected = current_value.as_ref() == Some(&option.value);
833
834                let is_favorite = self.favorites.contains(&option.value);
835
836                let option_name = option.name.clone();
837                let description = option.description.clone();
838
839                Some(
840                    div()
841                        .id(("config-option-picker-item", ix))
842                        .when_some(description, |this, desc| {
843                            let desc: SharedString = desc.into();
844                            this.on_hover(cx.listener(move |menu, hovered, _, cx| {
845                                if *hovered {
846                                    menu.delegate.selected_description = Some((ix, desc.clone()));
847                                } else if matches!(menu.delegate.selected_description, Some((id, _)) if id == ix)
848                                {
849                                    menu.delegate.selected_description = None;
850                                }
851                                cx.notify();
852                            }))
853                        })
854                        .child(
855                            ListItem::new(ix)
856                                .inset(true)
857                                .spacing(ListItemSpacing::Sparse)
858                                .toggle_state(selected)
859                                .child(h_flex().w_full().child(Label::new(option_name).truncate()))
860                                .end_slot(div().pr_2().when(is_selected, |this| {
861                                    this.child(Icon::new(IconName::Check).color(Color::Accent))
862                                }))
863                                .end_slot_on_hover(div().pr_1p5().child({
864                                    let (icon, color, tooltip) = if is_favorite {
865                                        (IconName::StarFilled, Color::Accent, "Unfavorite")
866                                    } else {
867                                        (IconName::Star, Color::Default, "Favorite")
868                                    };
869
870                                    let config_id = self.config_id.clone();
871                                    let value_id = option.value.clone();
872                                    let agent_server = self.agent_server.clone();
873                                    let fs = self.fs.clone();
874
875                                    IconButton::new(("toggle-favorite-config-option", ix), icon)
876                                        .layer(ElevationIndex::ElevatedSurface)
877                                        .icon_color(color)
878                                        .icon_size(IconSize::Small)
879                                        .tooltip(Tooltip::text(tooltip))
880                                        .on_click(move |_, _, cx| {
881                                            agent_server.toggle_favorite_config_option_value(
882                                                config_id.clone(),
883                                                value_id.clone(),
884                                                !is_favorite,
885                                                fs.clone(),
886                                                cx,
887                                            );
888                                        })
889                                })),
890                        )
891                        .into_any_element(),
892                )
893            }
894        }
895    }
896
897    fn documentation_aside(
898        &self,
899        _window: &mut Window,
900        cx: &mut Context<Picker<Self>>,
901    ) -> Option<ui::DocumentationAside> {
902        self.selected_description.as_ref().map(|(_, description)| {
903            let description = description.clone();
904
905            let side = documentation_aside_side(cx);
906
907            ui::DocumentationAside::new(
908                side,
909                Rc::new(move |_| Label::new(description.clone()).into_any_element()),
910            )
911        })
912    }
913
914    fn documentation_aside_index(&self) -> Option<usize> {
915        self.selected_description.as_ref().map(|(ix, _)| *ix)
916    }
917}
918
919fn extract_options(
920    config_options: &Rc<dyn AgentSessionConfigOptions>,
921    config_id: &acp::SessionConfigId,
922) -> Vec<ConfigOptionValue> {
923    let Some(option) = config_options
924        .config_options()
925        .into_iter()
926        .find(|opt| &opt.id == config_id)
927    else {
928        return Vec::new();
929    };
930
931    match &option.kind {
932        acp::SessionConfigKind::Select(select) => match &select.options {
933            acp::SessionConfigSelectOptions::Ungrouped(options) => options
934                .iter()
935                .map(|opt| ConfigOptionValue {
936                    value: opt.value.clone(),
937                    name: opt.name.clone(),
938                    description: opt.description.clone(),
939                    group: None,
940                })
941                .collect(),
942            acp::SessionConfigSelectOptions::Grouped(groups) => groups
943                .iter()
944                .flat_map(|group| {
945                    group.options.iter().map(|opt| ConfigOptionValue {
946                        value: opt.value.clone(),
947                        name: opt.name.clone(),
948                        description: opt.description.clone(),
949                        group: Some(group.name.clone()),
950                    })
951                })
952                .collect(),
953            _ => Vec::new(),
954        },
955        _ => Vec::new(),
956    }
957}
958
959fn get_current_select_value(
960    config_options: &Rc<dyn AgentSessionConfigOptions>,
961    config_id: &acp::SessionConfigId,
962) -> Option<acp::SessionConfigValueId> {
963    config_options
964        .config_options()
965        .into_iter()
966        .find(|opt| &opt.id == config_id)
967        .and_then(|opt| match &opt.kind {
968            acp::SessionConfigKind::Select(select) => Some(select.current_value.clone()),
969            _ => None,
970        })
971}
972
973fn setting_value_for_config_option_value(
974    value: &acp::SessionConfigOptionValue,
975) -> Option<AgentConfigOptionValue> {
976    match value {
977        acp::SessionConfigOptionValue::ValueId { value } => {
978            Some(AgentConfigOptionValue::ValueId(value.0.to_string()))
979        }
980        acp::SessionConfigOptionValue::Boolean { value } => {
981            Some(AgentConfigOptionValue::Boolean(*value))
982        }
983        _ => None,
984    }
985}
986
987fn options_to_picker_entries(
988    options: &[ConfigOptionValue],
989    favorites: &HashSet<acp::SessionConfigValueId>,
990) -> Vec<ConfigOptionPickerEntry> {
991    let mut entries = Vec::new();
992
993    let mut favorite_options = Vec::new();
994
995    for option in options {
996        if favorites.contains(&option.value) {
997            favorite_options.push(option.clone());
998        }
999    }
1000
1001    if !favorite_options.is_empty() {
1002        entries.push(ConfigOptionPickerEntry::Separator("Favorites".into()));
1003        for option in favorite_options {
1004            entries.push(ConfigOptionPickerEntry::Option(option));
1005        }
1006
1007        // If the remaining list would start ungrouped (group == None), insert a separator so
1008        // Favorites doesn't visually run into the main list.
1009        if let Some(option) = options.first()
1010            && option.group.is_none()
1011        {
1012            entries.push(ConfigOptionPickerEntry::Separator("All Options".into()));
1013        }
1014    }
1015
1016    let mut current_group: Option<String> = None;
1017    for option in options {
1018        if option.group != current_group {
1019            if let Some(group_name) = &option.group {
1020                entries.push(ConfigOptionPickerEntry::Separator(
1021                    group_name.clone().into(),
1022                ));
1023            }
1024            current_group = option.group.clone();
1025        }
1026        entries.push(ConfigOptionPickerEntry::Option(option.clone()));
1027    }
1028
1029    entries
1030}
1031
1032async fn fuzzy_search_options(
1033    options: Vec<ConfigOptionValue>,
1034    query: &str,
1035    executor: BackgroundExecutor,
1036) -> Vec<ConfigOptionValue> {
1037    let candidates = options
1038        .iter()
1039        .enumerate()
1040        .map(|(ix, opt)| StringMatchCandidate::new(ix, &opt.name))
1041        .collect::<Vec<_>>();
1042
1043    let mut matches = fuzzy::match_strings(
1044        &candidates,
1045        query,
1046        false,
1047        true,
1048        100,
1049        &Default::default(),
1050        executor,
1051    )
1052    .await;
1053
1054    matches.sort_unstable_by_key(|mat| {
1055        let candidate = &candidates[mat.candidate_id];
1056        (Reverse(OrderedFloat(mat.score)), candidate.id)
1057    });
1058
1059    matches
1060        .into_iter()
1061        .map(|mat| options[mat.candidate_id].clone())
1062        .collect()
1063}
1064
1065fn find_option_name(
1066    options: &acp::SessionConfigSelectOptions,
1067    value_id: &acp::SessionConfigValueId,
1068) -> Option<String> {
1069    match options {
1070        acp::SessionConfigSelectOptions::Ungrouped(opts) => opts
1071            .iter()
1072            .find(|o| &o.value == value_id)
1073            .map(|o| o.name.clone()),
1074        acp::SessionConfigSelectOptions::Grouped(groups) => groups.iter().find_map(|group| {
1075            group
1076                .options
1077                .iter()
1078                .find(|o| &o.value == value_id)
1079                .map(|o| o.name.clone())
1080        }),
1081        _ => None,
1082    }
1083}
1084
1085fn count_config_options(option: &acp::SessionConfigOption) -> usize {
1086    match &option.kind {
1087        acp::SessionConfigKind::Select(select) => match &select.options {
1088            acp::SessionConfigSelectOptions::Ungrouped(options) => options.len(),
1089            acp::SessionConfigSelectOptions::Grouped(groups) => {
1090                groups.iter().map(|g| g.options.len()).sum()
1091            }
1092            _ => 0,
1093        },
1094        _ => 0,
1095    }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100    use super::*;
1101    use acp_thread::AgentConnection;
1102    use fs::FakeFs;
1103    use gpui::TestAppContext;
1104    use parking_lot::Mutex;
1105    use project::{AgentId, Project};
1106    use std::{any::Any, cell::RefCell};
1107
1108    #[gpui::test]
1109    fn cycling_config_option_saves_selected_value_as_default(cx: &mut TestAppContext) {
1110        let agent_server = Rc::new(TestAgentServer::default());
1111        let config_options = Rc::new(TestSessionConfigOptions::new(vec![
1112            acp::SessionConfigOption::select(
1113                "mode",
1114                "Mode",
1115                "auto",
1116                vec![
1117                    acp::SessionConfigSelectOption::new("auto", "Auto"),
1118                    acp::SessionConfigSelectOption::new("manual", "Manual"),
1119                ],
1120            )
1121            .category(acp::SessionConfigOptionCategory::Mode),
1122        ]));
1123        let fs: Arc<dyn Fs> = FakeFs::new(cx.executor());
1124
1125        cx.update(|cx| {
1126            let config_options: Rc<dyn AgentSessionConfigOptions> = config_options.clone();
1127            let agent_server: Rc<dyn AgentServer> = agent_server.clone();
1128            let fs = fs.clone();
1129            let view = cx.new(|_| ConfigOptionsView {
1130                config_option_ids: ConfigOptionsView::config_option_ids(&config_options),
1131                config_options,
1132                selectors: Vec::new(),
1133                agent_server,
1134                fs,
1135                _refresh_task: Task::ready(()),
1136            });
1137
1138            assert!(view.update(cx, |view, cx| {
1139                view.cycle_category_option(acp::SessionConfigOptionCategory::Mode, false, cx)
1140            }));
1141        });
1142
1143        assert_eq!(
1144            agent_server.saved_defaults.lock().as_slice(),
1145            &[(
1146                "mode".to_string(),
1147                Some(AgentConfigOptionValue::ValueId("manual".to_string()))
1148            )]
1149        );
1150        assert_eq!(
1151            config_options.set_values.borrow().as_slice(),
1152            &[(
1153                "mode".to_string(),
1154                acp::SessionConfigOptionValue::value_id("manual")
1155            )]
1156        );
1157    }
1158
1159    #[gpui::test]
1160    fn cycling_boolean_config_option_saves_selected_value_as_default(cx: &mut TestAppContext) {
1161        let agent_server = Rc::new(TestAgentServer::default());
1162        let config_options = Rc::new(TestSessionConfigOptions::new(vec![
1163            acp::SessionConfigOption::boolean("web_search", "Web Search", false)
1164                .category(acp::SessionConfigOptionCategory::ModelConfig),
1165        ]));
1166        let fs: Arc<dyn Fs> = FakeFs::new(cx.executor());
1167
1168        cx.update(|cx| {
1169            let config_options: Rc<dyn AgentSessionConfigOptions> = config_options.clone();
1170            let agent_server: Rc<dyn AgentServer> = agent_server.clone();
1171            let fs = fs.clone();
1172            let view = cx.new(|_| ConfigOptionsView {
1173                config_option_ids: ConfigOptionsView::config_option_ids(&config_options),
1174                config_options,
1175                selectors: Vec::new(),
1176                agent_server,
1177                fs,
1178                _refresh_task: Task::ready(()),
1179            });
1180
1181            assert!(view.update(cx, |view, cx| {
1182                view.cycle_category_option(acp::SessionConfigOptionCategory::ModelConfig, false, cx)
1183            }));
1184        });
1185
1186        assert_eq!(
1187            agent_server.saved_defaults.lock().as_slice(),
1188            &[(
1189                "web_search".to_string(),
1190                Some(AgentConfigOptionValue::Boolean(true))
1191            )]
1192        );
1193        assert_eq!(
1194            config_options.set_values.borrow().as_slice(),
1195            &[(
1196                "web_search".to_string(),
1197                acp::SessionConfigOptionValue::boolean(true)
1198            )]
1199        );
1200    }
1201
1202    #[gpui::test]
1203    fn cycling_category_cycles_boolean_config_option_first(cx: &mut TestAppContext) {
1204        let agent_server = Rc::new(TestAgentServer::default());
1205        let config_options = Rc::new(TestSessionConfigOptions::new(vec![
1206            acp::SessionConfigOption::boolean("web_search", "Web Search", false)
1207                .category(acp::SessionConfigOptionCategory::Model),
1208            acp::SessionConfigOption::select(
1209                "model",
1210                "Model",
1211                "small",
1212                vec![
1213                    acp::SessionConfigSelectOption::new("small", "Small"),
1214                    acp::SessionConfigSelectOption::new("large", "Large"),
1215                ],
1216            )
1217            .category(acp::SessionConfigOptionCategory::Model),
1218        ]));
1219        let fs: Arc<dyn Fs> = FakeFs::new(cx.executor());
1220
1221        cx.update(|cx| {
1222            let config_options: Rc<dyn AgentSessionConfigOptions> = config_options.clone();
1223            let agent_server: Rc<dyn AgentServer> = agent_server.clone();
1224            let fs = fs.clone();
1225            let view = cx.new(|_| ConfigOptionsView {
1226                config_option_ids: ConfigOptionsView::config_option_ids(&config_options),
1227                config_options,
1228                selectors: Vec::new(),
1229                agent_server,
1230                fs,
1231                _refresh_task: Task::ready(()),
1232            });
1233
1234            assert!(view.update(cx, |view, cx| {
1235                view.cycle_category_option(acp::SessionConfigOptionCategory::Model, false, cx)
1236            }));
1237        });
1238
1239        assert_eq!(
1240            agent_server.saved_defaults.lock().as_slice(),
1241            &[(
1242                "web_search".to_string(),
1243                Some(AgentConfigOptionValue::Boolean(true))
1244            )]
1245        );
1246        assert_eq!(
1247            config_options.set_values.borrow().as_slice(),
1248            &[(
1249                "web_search".to_string(),
1250                acp::SessionConfigOptionValue::boolean(true)
1251            )]
1252        );
1253    }
1254
1255    #[gpui::test]
1256    fn toggling_category_picker_without_select_config_option_is_unhandled(cx: &mut TestAppContext) {
1257        let agent_server = Rc::new(TestAgentServer::default());
1258        let config_options = Rc::new(TestSessionConfigOptions::new(vec![
1259            acp::SessionConfigOption::boolean("web_search", "Web Search", false)
1260                .category(acp::SessionConfigOptionCategory::Model),
1261        ]));
1262        let fs: Arc<dyn Fs> = FakeFs::new(cx.executor());
1263        let cx = cx.add_empty_window();
1264        let view = cx.update({
1265            move |window, cx| {
1266                let config_options: Rc<dyn AgentSessionConfigOptions> = config_options;
1267                let agent_server: Rc<dyn AgentServer> = agent_server;
1268                cx.new(|cx| ConfigOptionsView::new(config_options, agent_server, fs, window, cx))
1269            }
1270        });
1271
1272        let handled = cx.update(|window, cx| {
1273            view.update(cx, |view, cx| {
1274                view.toggle_category_picker(acp::SessionConfigOptionCategory::Model, window, cx)
1275            })
1276        });
1277
1278        assert!(!handled);
1279    }
1280
1281    #[derive(Default)]
1282    struct TestAgentServer {
1283        saved_defaults: Arc<Mutex<Vec<(String, Option<AgentConfigOptionValue>)>>>,
1284    }
1285
1286    impl AgentServer for TestAgentServer {
1287        fn logo(&self) -> IconName {
1288            IconName::OmegaAssistant
1289        }
1290
1291        fn agent_id(&self) -> AgentId {
1292            AgentId::new("test-agent")
1293        }
1294
1295        fn connect(
1296            &self,
1297            _delegate: agent_servers::AgentServerDelegate,
1298            _project: Entity<Project>,
1299            _cx: &mut App,
1300        ) -> Task<anyhow::Result<Rc<dyn AgentConnection>>> {
1301            Task::ready(Err(anyhow::anyhow!("test agent server cannot connect")))
1302        }
1303
1304        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
1305            self
1306        }
1307
1308        fn set_default_config_option(
1309            &self,
1310            config_id: &str,
1311            value: Option<AgentConfigOptionValue>,
1312            _fs: Arc<dyn Fs>,
1313            _cx: &mut App,
1314        ) {
1315            self.saved_defaults
1316                .lock()
1317                .push((config_id.to_string(), value));
1318        }
1319    }
1320
1321    struct TestSessionConfigOptions {
1322        options: RefCell<Vec<acp::SessionConfigOption>>,
1323        set_values: RefCell<Vec<(String, acp::SessionConfigOptionValue)>>,
1324    }
1325
1326    impl TestSessionConfigOptions {
1327        fn new(options: Vec<acp::SessionConfigOption>) -> Self {
1328            Self {
1329                options: RefCell::new(options),
1330                set_values: RefCell::new(Vec::new()),
1331            }
1332        }
1333    }
1334
1335    impl AgentSessionConfigOptions for TestSessionConfigOptions {
1336        fn config_options(&self) -> Vec<acp::SessionConfigOption> {
1337            self.options.borrow().clone()
1338        }
1339
1340        fn set_config_option(
1341            &self,
1342            config_id: acp::SessionConfigId,
1343            value: acp::SessionConfigOptionValue,
1344            _cx: &mut App,
1345        ) -> Task<anyhow::Result<Vec<acp::SessionConfigOption>>> {
1346            self.set_values
1347                .borrow_mut()
1348                .push((config_id.0.to_string(), value.clone()));
1349
1350            let options = {
1351                let mut options = self.options.borrow_mut();
1352                if let Some(option) = options.iter_mut().find(|option| option.id == config_id) {
1353                    match (&mut option.kind, value) {
1354                        (
1355                            acp::SessionConfigKind::Select(select),
1356                            acp::SessionConfigOptionValue::ValueId { value },
1357                        ) => {
1358                            select.current_value = value;
1359                        }
1360                        (
1361                            acp::SessionConfigKind::Boolean(boolean),
1362                            acp::SessionConfigOptionValue::Boolean { value },
1363                        ) => {
1364                            boolean.current_value = value;
1365                        }
1366                        _ => {}
1367                    }
1368                }
1369                options.clone()
1370            };
1371
1372            Task::ready(Ok(options))
1373        }
1374    }
1375}
1376
Served at tenant.openagents/omega Member data and write actions are omitted.