use std::{cmp::Reverse, rc::Rc, sync::Arc}; use acp_thread::AgentSessionConfigOptions;use agent_client_protocol::schema::v1 as acp;use agent_servers::AgentServer; use collections::HashSet;use fs::Fs;use fuzzy::StringMatchCandidate;use gpui::{ App, BackgroundExecutor, Context, DismissEvent, Entity, Subscription, Task, Window, prelude::*,};use ordered_float::OrderedFloat;use picker::popover_menu::PickerPopoverMenu;use picker::{Picker, PickerDelegate};use settings::{AgentConfigOptionValue, SettingsStore};use ui::{ ElevationIndex, IconButton, KeyBinding, ListItem, ListItemSpacing, PopoverMenuHandle, Switch, SwitchLabelPosition, ToggleState, Tooltip, prelude::*,};use unicode_segmentation::UnicodeSegmentation;use util::ResultExt as _;use zed_actions::agent::ToggleModelSelector; use crate::ui::documentation_aside_side;use crate::{ CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort, ToggleProfileSelector, ToggleThinkingEffortMenu,}; const PICKER_THRESHOLD: usize = 5; pub struct ConfigOptionsView { config_options: Rc<dyn AgentSessionConfigOptions>, selectors: Vec<Entity<ConfigOptionSelector>>, agent_server: Rc<dyn AgentServer>, fs: Arc<dyn Fs>, config_option_ids: Vec<acp::SessionConfigId>, _refresh_task: Task<()>,} impl ConfigOptionsView { pub fn new( config_options: Rc<dyn AgentSessionConfigOptions>, agent_server: Rc<dyn AgentServer>, fs: Arc<dyn Fs>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let selectors = Self::build_selectors(&config_options, &agent_server, &fs, window, cx); let config_option_ids = Self::config_option_ids(&config_options); let rx = config_options.watch(cx); let refresh_task = cx.spawn_in(window, async move |this, cx| { if let Some(mut rx) = rx { while let Ok(()) = rx.recv().await { this.update_in(cx, |this, window, cx| { this.rebuild_selectors(window, cx); cx.notify(); }) .log_err(); } } }); Self { config_options, selectors, agent_server, fs, config_option_ids, _refresh_task: refresh_task, } } pub fn toggle_category_picker( &mut self, category: acp::SessionConfigOptionCategory, window: &mut Window, cx: &mut Context<Self>, ) -> bool { let Some(config_id) = self.first_config_option_id_matching(category, |option| { matches!(&option.kind, acp::SessionConfigKind::Select(_)) }) else { return false; }; let Some(selector) = self.selector_for_config_id(&config_id, cx) else { return false; }; selector.update(cx, |selector, cx| selector.toggle_picker(window, cx)) } pub fn cycle_category_option( &mut self, category: acp::SessionConfigOptionCategory, favorites_only: bool, cx: &mut Context<Self>, ) -> bool { let Some(config_id) = self.first_config_option_id_matching(category, |option| { Self::can_cycle_config_option(option, favorites_only) }) else { return false; }; let Some(next_value) = self.next_value_for_config(&config_id, favorites_only, cx) else { return false; }; let default_value = setting_value_for_config_option_value(&next_value); self.agent_server.set_default_config_option( config_id.0.as_ref(), default_value, self.fs.clone(), cx, ); let task = self .config_options .set_config_option(config_id, next_value, cx); cx.spawn(async move |_, _| { if let Err(err) = task.await { log::error!("Failed to set config option: {:?}", err); } }) .detach(); true } fn first_config_option_id_matching( &self, category: acp::SessionConfigOptionCategory, predicate: impl Fn(&acp::SessionConfigOption) -> bool, ) -> Option<acp::SessionConfigId> { self.config_options .config_options() .into_iter() .find(|option| option.category.as_ref() == Some(&category) && predicate(option)) .map(|option| option.id) } fn can_cycle_config_option(option: &acp::SessionConfigOption, favorites_only: bool) -> bool { match &option.kind { acp::SessionConfigKind::Select(_) => true, acp::SessionConfigKind::Boolean(_) => !favorites_only, _ => false, } } fn selector_for_config_id( &self, config_id: &acp::SessionConfigId, cx: &App, ) -> Option<Entity<ConfigOptionSelector>> { self.selectors .iter() .find(|selector| selector.read(cx).config_id() == config_id) .cloned() } fn next_value_for_config( &self, config_id: &acp::SessionConfigId, favorites_only: bool, cx: &mut Context<Self>, ) -> Option<acp::SessionConfigOptionValue> { let option = self .config_options .config_options() .into_iter() .find(|option| &option.id == config_id)?; match &option.kind { acp::SessionConfigKind::Select(_) => { let mut options = extract_options(&self.config_options, config_id); if options.is_empty() { return None; } if favorites_only { let favorites = self .agent_server .favorite_config_option_value_ids(config_id, cx); options.retain(|option| favorites.contains(&option.value)); if options.is_empty() { return None; } } let current_value = get_current_select_value(&self.config_options, config_id); let current_index = current_value .as_ref() .and_then(|current| options.iter().position(|option| &option.value == current)) .unwrap_or(usize::MAX); let next_index = if current_index == usize::MAX { 0 } else { (current_index + 1) % options.len() }; Some(acp::SessionConfigOptionValue::value_id( options[next_index].value.clone(), )) } acp::SessionConfigKind::Boolean(boolean) => { if favorites_only { None } else { Some(acp::SessionConfigOptionValue::boolean( !boolean.current_value, )) } } _ => None, } } fn config_option_ids( config_options: &Rc<dyn AgentSessionConfigOptions>, ) -> Vec<acp::SessionConfigId> { config_options .config_options() .into_iter() .map(|option| option.id) .collect() } fn rebuild_selectors(&mut self, window: &mut Window, cx: &mut Context<Self>) { // Config option updates can mutate option values for existing IDs (for example, // reasoning levels after a model switch). Rebuild to refresh cached picker entries. self.config_option_ids = Self::config_option_ids(&self.config_options); self.selectors = Self::build_selectors( &self.config_options, &self.agent_server, &self.fs, window, cx, ); cx.notify(); } fn build_selectors( config_options: &Rc<dyn AgentSessionConfigOptions>, agent_server: &Rc<dyn AgentServer>, fs: &Arc<dyn Fs>, window: &mut Window, cx: &mut Context<Self>, ) -> Vec<Entity<ConfigOptionSelector>> { config_options .config_options() .into_iter() .map(|option| { let config_options = config_options.clone(); let agent_server = agent_server.clone(); let fs = fs.clone(); cx.new(|cx| { ConfigOptionSelector::new( config_options, option.id.clone(), agent_server, fs, window, cx, ) }) }) .collect() }} impl Render for ConfigOptionsView { fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { if self.selectors.is_empty() { return div().into_any_element(); } h_flex() .min_w_0() .flex_wrap() .gap_1() .children(self.selectors.iter().cloned()) .into_any_element() }} struct ConfigOptionSelector { config_options: Rc<dyn AgentSessionConfigOptions>, config_id: acp::SessionConfigId, agent_server: Rc<dyn AgentServer>, fs: Arc<dyn Fs>, picker_handle: Option<PopoverMenuHandle<Picker<ConfigOptionPickerDelegate>>>, picker: Option<Entity<Picker<ConfigOptionPickerDelegate>>>, setting_value: bool,} impl ConfigOptionSelector { pub fn new( config_options: Rc<dyn AgentSessionConfigOptions>, config_id: acp::SessionConfigId, agent_server: Rc<dyn AgentServer>, fs: Arc<dyn Fs>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let current_option = config_options .config_options() .into_iter() .find(|opt| opt.id == config_id); let option_count = current_option .as_ref() .map(count_config_options) .unwrap_or(0); let is_select = current_option .as_ref() .is_some_and(|option| matches!(&option.kind, acp::SessionConfigKind::Select(_))); let is_searchable = option_count >= PICKER_THRESHOLD; let (picker_handle, picker) = if is_select { let config_options = config_options.clone(); let config_id = config_id.clone(); let agent_server = agent_server.clone(); let fs = fs.clone(); let picker = cx.new(move |picker_cx| { let delegate = ConfigOptionPickerDelegate::new( config_options, config_id, agent_server, fs, window, picker_cx, ); if is_searchable { Picker::list(delegate, window, picker_cx) } else { Picker::nonsearchable_list(delegate, window, picker_cx) } .show_scrollbar(true) .initial_width(rems(20.)) }); (Some(PopoverMenuHandle::default()), Some(picker)) } else { (None, None) }; Self { config_options, config_id, agent_server, fs, picker_handle, picker, setting_value: false, } } fn current_option(&self) -> Option<acp::SessionConfigOption> { self.config_options .config_options() .into_iter() .find(|opt| opt.id == self.config_id) } fn config_id(&self) -> &acp::SessionConfigId { &self.config_id } fn toggle_picker(&self, window: &mut Window, cx: &mut Context<Self>) -> bool { if let Some(picker_handle) = &self.picker_handle { picker_handle.toggle(window, cx); true } else { false } } fn current_value_name(&self) -> String { let Some(option) = self.current_option() else { return "Unknown".to_string(); }; match &option.kind { acp::SessionConfigKind::Select(select) => { find_option_name(&select.options, &select.current_value) .unwrap_or_else(|| "Unknown".to_string()) } _ => "Unknown".to_string(), } } fn handles_category_keybindings(&self, category: &acp::SessionConfigOptionCategory) -> bool { self.config_options .config_options() .into_iter() .find(|option| { option.category.as_ref() == Some(category) && matches!(&option.kind, acp::SessionConfigKind::Select(_)) }) .is_some_and(|option| option.id == self.config_id) } fn render_trigger_button(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Button { let Some(option) = self.current_option() else { return Button::new("config-option-trigger", "Unknown") .label_size(LabelSize::Small) .color(Color::Muted) .disabled(true); }; let picker_deployed = self .picker_handle .as_ref() .is_some_and(|picker_handle| picker_handle.is_deployed()); let icon = if picker_deployed { IconName::ChevronUp } else { IconName::ChevronDown }; let value_name = self.current_value_name(); let mut graphemes = value_name.graphemes(true); let truncated = graphemes.by_ref().take(32).collect::<String>(); let display_name = if graphemes.next().is_some() { format!("{truncated}…") } else { truncated }; Button::new( ElementId::Name(format!("config-option-{}", option.id.0).into()), display_name, ) .label_size(LabelSize::Small) .color(Color::Muted) .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted)) .disabled(self.setting_value) }} impl Render for ConfigOptionSelector { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let Some(option) = self.current_option() else { return div().into_any_element(); }; match &option.kind { acp::SessionConfigKind::Select(_) => { let (Some(picker), Some(picker_handle)) = (self.picker.clone(), self.picker_handle.clone()) else { return div().into_any_element(); }; let trigger_button = self.render_trigger_button(window, cx); let show_category_keybindings = option .category .as_ref() .is_some_and(|category| self.handles_category_keybindings(category)); let option_category = option.category.clone(); let option_name = option.name.clone(); let option_description: Option<SharedString> = option.description.clone().map(Into::into); let tooltip = Tooltip::element(move |_window, cx| { let mut content = v_flex().gap_1().child(Label::new(option_name.clone())); if let Some(desc) = option_description.as_ref() { content = content.child( Label::new(desc.clone()) .size(LabelSize::Small) .color(Color::Muted), ); } let action_tooltip_container = |label: &str, keybinding: KeyBinding| { h_flex() .pt_1() .gap_2() .justify_between() .border_t_1() .border_color(cx.theme().colors().border_variant) .child(Label::new(label)) .child(keybinding) }; if show_category_keybindings && let Some(category) = &option_category { match category { acp::SessionConfigOptionCategory::Mode => { content = content .child(action_tooltip_container( "Change Mode", KeyBinding::for_action(&ToggleProfileSelector, cx), )) .child(action_tooltip_container( "Cycle Through Modes",Lines 1–500 of 1375. Load more
use std::{cmp::Reverse, rc::Rc, sync::Arc}; use acp_thread::AgentSessionConfigOptions; use agent_client_protocol::schema::v1 as acp; use agent_servers::AgentServer; use collections::HashSet; use fs::Fs; use fuzzy::StringMatchCandidate; use gpui::{ App, BackgroundExecutor, Context, DismissEvent, Entity, Subscription, Task, Window, prelude::*, }; use ordered_float::OrderedFloat; use picker::popover_menu::PickerPopoverMenu; use picker::{Picker, PickerDelegate}; use settings::{AgentConfigOptionValue, SettingsStore}; use ui::{ ElevationIndex, IconButton, KeyBinding, ListItem, ListItemSpacing, PopoverMenuHandle, Switch, SwitchLabelPosition, ToggleState, Tooltip, prelude::*, }; use unicode_segmentation::UnicodeSegmentation; use util::ResultExt as _; use zed_actions::agent::ToggleModelSelector; use crate::ui::documentation_aside_side; use crate::{ CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort, ToggleProfileSelector, ToggleThinkingEffortMenu, }; const PICKER_THRESHOLD: usize = 5; pub struct ConfigOptionsView { config_options: Rc<dyn AgentSessionConfigOptions>, selectors: Vec<Entity<ConfigOptionSelector>>, agent_server: Rc<dyn AgentServer>, fs: Arc<dyn Fs>, config_option_ids: Vec<acp::SessionConfigId>, _refresh_task: Task<()>, } impl ConfigOptionsView { pub fn new( config_options: Rc<dyn AgentSessionConfigOptions>, agent_server: Rc<dyn AgentServer>, fs: Arc<dyn Fs>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let selectors = Self::build_selectors(&config_options, &agent_server, &fs, window, cx); let config_option_ids = Self::config_option_ids(&config_options); let rx = config_options.watch(cx); let refresh_task = cx.spawn_in(window, async move |this, cx| { if let Some(mut rx) = rx { while let Ok(()) = rx.recv().await { this.update_in(cx, |this, window, cx| { this.rebuild_selectors(window, cx); cx.notify(); }) .log_err(); } } }); Self { config_options, selectors, agent_server, fs, config_option_ids, _refresh_task: refresh_task, } } pub fn toggle_category_picker( &mut self, category: acp::SessionConfigOptionCategory, window: &mut Window, cx: &mut Context<Self>, ) -> bool { let Some(config_id) = self.first_config_option_id_matching(category, |option| { matches!(&option.kind, acp::SessionConfigKind::Select(_)) }) else { return false; }; let Some(selector) = self.selector_for_config_id(&config_id, cx) else { return false; }; selector.update(cx, |selector, cx| selector.toggle_picker(window, cx)) } pub fn cycle_category_option( &mut self, category: acp::SessionConfigOptionCategory, favorites_only: bool, cx: &mut Context<Self>, ) -> bool { let Some(config_id) = self.first_config_option_id_matching(category, |option| { Self::can_cycle_config_option(option, favorites_only) }) else { return false; }; let Some(next_value) = self.next_value_for_config(&config_id, favorites_only, cx) else { return false; }; let default_value = setting_value_for_config_option_value(&next_value); self.agent_server.set_default_config_option( config_id.0.as_ref(), default_value, self.fs.clone(), cx, ); let task = self .config_options .set_config_option(config_id, next_value, cx); cx.spawn(async move |_, _| { if let Err(err) = task.await { log::error!("Failed to set config option: {:?}", err); } }) .detach(); true } fn first_config_option_id_matching( &self, category: acp::SessionConfigOptionCategory, predicate: impl Fn(&acp::SessionConfigOption) -> bool, ) -> Option<acp::SessionConfigId> { self.config_options .config_options() .into_iter() .find(|option| option.category.as_ref() == Some(&category) && predicate(option)) .map(|option| option.id) } fn can_cycle_config_option(option: &acp::SessionConfigOption, favorites_only: bool) -> bool { match &option.kind { acp::SessionConfigKind::Select(_) => true, acp::SessionConfigKind::Boolean(_) => !favorites_only, _ => false, } } fn selector_for_config_id( &self, config_id: &acp::SessionConfigId, cx: &App, ) -> Option<Entity<ConfigOptionSelector>> { self.selectors .iter() .find(|selector| selector.read(cx).config_id() == config_id) .cloned() } fn next_value_for_config( &self, config_id: &acp::SessionConfigId, favorites_only: bool, cx: &mut Context<Self>, ) -> Option<acp::SessionConfigOptionValue> { let option = self .config_options .config_options() .into_iter() .find(|option| &option.id == config_id)?; match &option.kind { acp::SessionConfigKind::Select(_) => { let mut options = extract_options(&self.config_options, config_id); if options.is_empty() { return None; } if favorites_only { let favorites = self .agent_server .favorite_config_option_value_ids(config_id, cx); options.retain(|option| favorites.contains(&option.value)); if options.is_empty() { return None; } } let current_value = get_current_select_value(&self.config_options, config_id); let current_index = current_value .as_ref() .and_then(|current| options.iter().position(|option| &option.value == current)) .unwrap_or(usize::MAX); let next_index = if current_index == usize::MAX { 0 } else { (current_index + 1) % options.len() }; Some(acp::SessionConfigOptionValue::value_id( options[next_index].value.clone(), )) } acp::SessionConfigKind::Boolean(boolean) => { if favorites_only { None } else { Some(acp::SessionConfigOptionValue::boolean( !boolean.current_value, )) } } _ => None, } } fn config_option_ids( config_options: &Rc<dyn AgentSessionConfigOptions>, ) -> Vec<acp::SessionConfigId> { config_options .config_options() .into_iter() .map(|option| option.id) .collect() } fn rebuild_selectors(&mut self, window: &mut Window, cx: &mut Context<Self>) { // Config option updates can mutate option values for existing IDs (for example, // reasoning levels after a model switch). Rebuild to refresh cached picker entries. self.config_option_ids = Self::config_option_ids(&self.config_options); self.selectors = Self::build_selectors( &self.config_options, &self.agent_server, &self.fs, window, cx, ); cx.notify(); } fn build_selectors( config_options: &Rc<dyn AgentSessionConfigOptions>, agent_server: &Rc<dyn AgentServer>, fs: &Arc<dyn Fs>, window: &mut Window, cx: &mut Context<Self>, ) -> Vec<Entity<ConfigOptionSelector>> { config_options .config_options() .into_iter() .map(|option| { let config_options = config_options.clone(); let agent_server = agent_server.clone(); let fs = fs.clone(); cx.new(|cx| { ConfigOptionSelector::new( config_options, option.id.clone(), agent_server, fs, window, cx, ) }) }) .collect() } } impl Render for ConfigOptionsView { fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { if self.selectors.is_empty() { return div().into_any_element(); } h_flex() .min_w_0() .flex_wrap() .gap_1() .children(self.selectors.iter().cloned()) .into_any_element() } } struct ConfigOptionSelector { config_options: Rc<dyn AgentSessionConfigOptions>, config_id: acp::SessionConfigId, agent_server: Rc<dyn AgentServer>, fs: Arc<dyn Fs>, picker_handle: Option<PopoverMenuHandle<Picker<ConfigOptionPickerDelegate>>>, picker: Option<Entity<Picker<ConfigOptionPickerDelegate>>>, setting_value: bool, } impl ConfigOptionSelector { pub fn new( config_options: Rc<dyn AgentSessionConfigOptions>, config_id: acp::SessionConfigId, agent_server: Rc<dyn AgentServer>, fs: Arc<dyn Fs>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let current_option = config_options .config_options() .into_iter() .find(|opt| opt.id == config_id); let option_count = current_option .as_ref() .map(count_config_options) .unwrap_or(0); let is_select = current_option .as_ref() .is_some_and(|option| matches!(&option.kind, acp::SessionConfigKind::Select(_))); let is_searchable = option_count >= PICKER_THRESHOLD; let (picker_handle, picker) = if is_select { let config_options = config_options.clone(); let config_id = config_id.clone(); let agent_server = agent_server.clone(); let fs = fs.clone(); let picker = cx.new(move |picker_cx| { let delegate = ConfigOptionPickerDelegate::new( config_options, config_id, agent_server, fs, window, picker_cx, ); if is_searchable { Picker::list(delegate, window, picker_cx) } else { Picker::nonsearchable_list(delegate, window, picker_cx) } .show_scrollbar(true) .initial_width(rems(20.)) }); (Some(PopoverMenuHandle::default()), Some(picker)) } else { (None, None) }; Self { config_options, config_id, agent_server, fs, picker_handle, picker, setting_value: false, } } fn current_option(&self) -> Option<acp::SessionConfigOption> { self.config_options .config_options() .into_iter() .find(|opt| opt.id == self.config_id) } fn config_id(&self) -> &acp::SessionConfigId { &self.config_id } fn toggle_picker(&self, window: &mut Window, cx: &mut Context<Self>) -> bool { if let Some(picker_handle) = &self.picker_handle { picker_handle.toggle(window, cx); true } else { false } } fn current_value_name(&self) -> String { let Some(option) = self.current_option() else { return "Unknown".to_string(); }; match &option.kind { acp::SessionConfigKind::Select(select) => { find_option_name(&select.options, &select.current_value) .unwrap_or_else(|| "Unknown".to_string()) } _ => "Unknown".to_string(), } } fn handles_category_keybindings(&self, category: &acp::SessionConfigOptionCategory) -> bool { self.config_options .config_options() .into_iter() .find(|option| { option.category.as_ref() == Some(category) && matches!(&option.kind, acp::SessionConfigKind::Select(_)) }) .is_some_and(|option| option.id == self.config_id) } fn render_trigger_button(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Button { let Some(option) = self.current_option() else { return Button::new("config-option-trigger", "Unknown") .label_size(LabelSize::Small) .color(Color::Muted) .disabled(true); }; let picker_deployed = self .picker_handle .as_ref() .is_some_and(|picker_handle| picker_handle.is_deployed()); let icon = if picker_deployed { IconName::ChevronUp } else { IconName::ChevronDown }; let value_name = self.current_value_name(); let mut graphemes = value_name.graphemes(true); let truncated = graphemes.by_ref().take(32).collect::<String>(); let display_name = if graphemes.next().is_some() { format!("{truncated}…") } else { truncated }; Button::new( ElementId::Name(format!("config-option-{}", option.id.0).into()), display_name, ) .label_size(LabelSize::Small) .color(Color::Muted) .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted)) .disabled(self.setting_value) } } impl Render for ConfigOptionSelector { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let Some(option) = self.current_option() else { return div().into_any_element(); }; match &option.kind { acp::SessionConfigKind::Select(_) => { let (Some(picker), Some(picker_handle)) = (self.picker.clone(), self.picker_handle.clone()) else { return div().into_any_element(); }; let trigger_button = self.render_trigger_button(window, cx); let show_category_keybindings = option .category .as_ref() .is_some_and(|category| self.handles_category_keybindings(category)); let option_category = option.category.clone(); let option_name = option.name.clone(); let option_description: Option<SharedString> = option.description.clone().map(Into::into); let tooltip = Tooltip::element(move |_window, cx| { let mut content = v_flex().gap_1().child(Label::new(option_name.clone())); if let Some(desc) = option_description.as_ref() { content = content.child( Label::new(desc.clone()) .size(LabelSize::Small) .color(Color::Muted), ); } let action_tooltip_container = |label: &str, keybinding: KeyBinding| { h_flex() .pt_1() .gap_2() .justify_between() .border_t_1() .border_color(cx.theme().colors().border_variant) .child(Label::new(label)) .child(keybinding) }; if show_category_keybindings && let Some(category) = &option_category { match category { acp::SessionConfigOptionCategory::Mode => { content = content .child(action_tooltip_container( "Change Mode", KeyBinding::for_action(&ToggleProfileSelector, cx), )) .child(action_tooltip_container( "Cycle Through Modes",