1375 lines · 48.2 KB · Rust
RawDownload
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.