Skip to repository content1376 lines · 49.3 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:08:18.151Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
config_options.rs
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