841 lines · 28.9 KB · Rust
RawDownload
use std::{cmp::Reverse, rc::Rc, sync::Arc}; use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};use agent_client_protocol::ModelId;use agent_servers::AgentServer; use anyhow::Result;use collections::{HashSet, IndexMap};use fs::Fs;use futures::FutureExt;use fuzzy::{StringMatchCandidate, match_strings};use gpui::{    Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,    WeakEntity,};use itertools::Itertools;use ordered_float::OrderedFloat;use picker::{Picker, PickerDelegate};use settings::SettingsStore;use ui::{DocumentationAside, IntoElement, prelude::*};use util::ResultExt;use zed_actions::agent::OpenSettings; use crate::ui::{    HoldForDefault, ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem,    documentation_aside_side,}; pub type ModelSelector = Picker<ModelPickerDelegate>; pub fn acp_model_selector(    selector: Rc<dyn AgentModelSelector>,    agent_server: Rc<dyn AgentServer>,    fs: Arc<dyn Fs>,    focus_handle: FocusHandle,    window: &mut Window,    cx: &mut Context<ModelSelector>,) -> ModelSelector {    let delegate = ModelPickerDelegate::new(selector, agent_server, fs, focus_handle, window, cx);    Picker::list(delegate, window, cx)        .show_scrollbar(true)        .width(rems(20.))        .max_height(Some(rems(20.).into()))} enum ModelPickerEntry {    Separator(SharedString),    Model(AgentModelInfo, bool),} pub struct ModelPickerDelegate {    selector: Rc<dyn AgentModelSelector>,    agent_server: Rc<dyn AgentServer>,    fs: Arc<dyn Fs>,    filtered_entries: Vec<ModelPickerEntry>,    models: Option<AgentModelList>,    selected_index: usize,    selected_description: Option<(usize, SharedString, bool)>,    selected_model: Option<AgentModelInfo>,    favorites: HashSet<ModelId>,    _refresh_models_task: Task<()>,    _settings_subscription: Subscription,    focus_handle: FocusHandle,} impl ModelPickerDelegate {    fn new(        selector: Rc<dyn AgentModelSelector>,        agent_server: Rc<dyn AgentServer>,        fs: Arc<dyn Fs>,        focus_handle: FocusHandle,        window: &mut Window,        cx: &mut Context<ModelSelector>,    ) -> Self {        let rx = selector.watch(cx);        let refresh_models_task = {            cx.spawn_in(window, {                async move |this, cx| {                    async fn refresh(                        this: &WeakEntity<Picker<ModelPickerDelegate>>,                        cx: &mut AsyncWindowContext,                    ) -> Result<()> {                        let (models_task, selected_model_task) = this.update(cx, |this, cx| {                            (                                this.delegate.selector.list_models(cx),                                this.delegate.selector.selected_model(cx),                            )                        })?;                         let (models, selected_model) =                            futures::join!(models_task, selected_model_task);                         this.update_in(cx, |this, window, cx| {                            this.delegate.models = models.ok();                            this.delegate.selected_model = selected_model.ok();                            this.refresh(window, cx)                        })                    }                     refresh(&this, cx).await.log_err();                    if let Some(mut rx) = rx {                        while let Ok(()) = rx.recv().await {                            refresh(&this, cx).await.log_err();                        }                    }                }            })        };         let agent_server_for_subscription = agent_server.clone();        let settings_subscription =            cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {                // Only refresh if the favorites actually changed to avoid redundant work                // when other settings are modified (e.g., user editing settings.json)                let new_favorites = agent_server_for_subscription.favorite_model_ids(cx);                if new_favorites != picker.delegate.favorites {                    picker.delegate.favorites = new_favorites;                    picker.refresh(window, cx);                }            });        let favorites = agent_server.favorite_model_ids(cx);         Self {            selector,            agent_server,            fs,            filtered_entries: Vec::new(),            models: None,            selected_model: None,            selected_index: 0,            selected_description: None,            favorites,            _refresh_models_task: refresh_models_task,            _settings_subscription: settings_subscription,            focus_handle,        }    }     pub fn active_model(&self) -> Option<&AgentModelInfo> {        self.selected_model.as_ref()    }     pub fn favorites_count(&self) -> usize {        self.favorites.len()    }     pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {        if self.favorites.is_empty() {            return;        }         let Some(models) = &self.models else {            return;        };         let all_models: Vec<&AgentModelInfo> = match models {            AgentModelList::Flat(list) => list.iter().collect(),            AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),        };         let favorite_models: Vec<_> = all_models            .into_iter()            .filter(|model| self.favorites.contains(&model.id))            .unique_by(|model| &model.id)            .collect();         if favorite_models.is_empty() {            return;        }         let current_id = self.selected_model.as_ref().map(|m| &m.id);         let current_index_in_favorites = current_id            .and_then(|id| favorite_models.iter().position(|m| &m.id == id))            .unwrap_or(usize::MAX);         let next_index = if current_index_in_favorites == usize::MAX {            0        } else {            (current_index_in_favorites + 1) % favorite_models.len()        };         let next_model = favorite_models[next_index].clone();         self.selector            .select_model(next_model.id.clone(), cx)            .detach_and_log_err(cx);         self.selected_model = Some(next_model);         // Keep the picker selection aligned with the newly-selected model        if let Some(new_index) = self.filtered_entries.iter().position(|entry| {            matches!(entry, ModelPickerEntry::Model(model_info, _) if self.selected_model.as_ref().is_some_and(|selected| model_info.id == selected.id))        }) {            self.set_selected_index(new_index, window, cx);        } else {            cx.notify();        }    }} impl PickerDelegate for ModelPickerDelegate {    type ListItem = AnyElement;     fn match_count(&self) -> usize {        self.filtered_entries.len()    }     fn selected_index(&self) -> usize {        self.selected_index    }     fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {        self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));        cx.notify();    }     fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {        match self.filtered_entries.get(ix) {            Some(ModelPickerEntry::Model(_, _)) => true,            Some(ModelPickerEntry::Separator(_)) | None => false,        }    }     fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {        "Select a model…".into()    }     fn update_matches(        &mut self,        query: String,        window: &mut Window,        cx: &mut Context<Picker<Self>>,    ) -> Task<()> {        let favorites = self.favorites.clone();         cx.spawn_in(window, async move |this, cx| {            let filtered_models = match this                .read_with(cx, |this, cx| {                    this.delegate.models.clone().map(move |models| {                        fuzzy_search(models, query, cx.background_executor().clone())                    })                })                .ok()                .flatten()            {                Some(task) => task.await,                None => AgentModelList::Flat(vec![]),            };             this.update_in(cx, |this, window, cx| {                this.delegate.filtered_entries =                    info_list_to_picker_entries(filtered_models, &favorites);                // Finds the currently selected model in the list                let new_index = this                    .delegate                    .selected_model                    .as_ref()                    .and_then(|selected| {                        this.delegate.filtered_entries.iter().position(|entry| {                            if let ModelPickerEntry::Model(model_info, _) = entry {                                model_info.id == selected.id                            } else {                                false                            }                        })                    })                    .unwrap_or(0);                this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);                cx.notify();            })            .ok();        })    }     fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {        if let Some(ModelPickerEntry::Model(model_info, _)) =            self.filtered_entries.get(self.selected_index)        {            if window.modifiers().secondary() {                let default_model = self.agent_server.default_model(cx);                let is_default = default_model.as_ref() == Some(&model_info.id);                 self.agent_server.set_default_model(                    if is_default {                        None                    } else {                        Some(model_info.id.clone())                    },                    self.fs.clone(),                    cx,                );            }             self.selector                .select_model(model_info.id.clone(), cx)                .detach_and_log_err(cx);            self.selected_model = Some(model_info.clone());            let current_index = self.selected_index;            self.set_selected_index(current_index, window, cx);             cx.emit(DismissEvent);        }    }     fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {        cx.defer_in(window, |picker, window, cx| {            picker.set_query("", window, cx);        });    }     fn render_match(        &self,        ix: usize,        selected: bool,        _: &mut Window,        cx: &mut Context<Picker<Self>>,    ) -> Option<Self::ListItem> {        match self.filtered_entries.get(ix)? {            ModelPickerEntry::Separator(title) => {                Some(ModelSelectorHeader::new(title, ix > 1).into_any_element())            }            ModelPickerEntry::Model(model_info, is_favorite) => {                let is_selected = Some(model_info) == self.selected_model.as_ref();                let default_model = self.agent_server.default_model(cx);                let is_default = default_model.as_ref() == Some(&model_info.id);                 let is_favorite = *is_favorite;                let handle_action_click = {                    let model_id = model_info.id.clone();                    let fs = self.fs.clone();                    let agent_server = self.agent_server.clone();                     cx.listener(move |_, _, _, cx| {                        agent_server.toggle_favorite_model(                            model_id.clone(),                            !is_favorite,                            fs.clone(),                            cx,                        );                    })                };                 let model_cost = model_info.cost.clone();                 Some(                    div()                        .id(("model-picker-menu-child", ix))                        .when_some(model_info.description.clone(), |this, description| {                            this.on_hover(cx.listener(move |menu, hovered, _, cx| {                                if *hovered {                                    menu.delegate.selected_description =                                        Some((ix, description.clone(), is_default));                                } else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) {                                    menu.delegate.selected_description = None;                                }                                cx.notify();                            }))                        })                        .child(                            ModelSelectorListItem::new(ix, model_info.name.clone())                                .map(|this| match &model_info.icon {                                    Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()),                                    Some(AgentModelIcon::Named(icon)) => this.icon(*icon),                                    None => this,                                })                                .is_selected(is_selected)                                .is_focused(selected)                                .is_latest(model_info.is_latest)                                .is_favorite(is_favorite)                                .on_toggle_favorite(handle_action_click)                                .cost_info(model_cost)                        )                        .into_any_element(),                )            }        }    }     fn documentation_aside(        &self,        _window: &mut Window,        cx: &mut Context<Picker<Self>>,    ) -> Option<ui::DocumentationAside> {        self.selected_description            .as_ref()            .map(|(_, description, is_default)| {                let description = description.clone();                let is_default = *is_default;                 let side = documentation_aside_side(cx);                 DocumentationAside::new(                    side,                    Rc::new(move |_| {                        v_flex()                            .gap_1()                            .child(Label::new(description.clone()))                            .child(HoldForDefault::new(is_default))                            .into_any_element()                    }),                )            })    }     fn documentation_aside_index(&self) -> Option<usize> {        self.selected_description.as_ref().map(|(ix, _, _)| *ix)    }     fn render_footer(        &self,        _window: &mut Window,        _cx: &mut Context<Picker<Self>>,    ) -> Option<AnyElement> {        let focus_handle = self.focus_handle.clone();         if !self.selector.should_render_footer() {            return None;        }         Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())    }} fn info_list_to_picker_entries(    model_list: AgentModelList,    favorites: &HashSet<ModelId>,) -> Vec<ModelPickerEntry> {    let mut entries = Vec::new();     let all_models: Vec<_> = match &model_list {        AgentModelList::Flat(list) => list.iter().collect(),        AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),    };     let favorite_models: Vec<_> = all_models        .iter()        .filter(|m| favorites.contains(&m.id))        .unique_by(|m| &m.id)        .collect();     let has_favorites = !favorite_models.is_empty();    if has_favorites {        entries.push(ModelPickerEntry::Separator("Favorite".into()));        for model in favorite_models {            entries.push(ModelPickerEntry::Model((*model).clone(), true));        }    }     match model_list {        AgentModelList::Flat(list) => {            if has_favorites {                entries.push(ModelPickerEntry::Separator("All".into()));            }            for model in list {                let is_favorite = favorites.contains(&model.id);                entries.push(ModelPickerEntry::Model(model, is_favorite));            }        }        AgentModelList::Grouped(index_map) => {            for (group_name, models) in index_map {                entries.push(ModelPickerEntry::Separator(group_name.0));                for model in models {                    let is_favorite = favorites.contains(&model.id);                    entries.push(ModelPickerEntry::Model(model, is_favorite));                }            }        }    }     entries} async fn fuzzy_search(    model_list: AgentModelList,    query: String,    executor: BackgroundExecutor,) -> AgentModelList {    async fn fuzzy_search_list(        model_list: Vec<AgentModelInfo>,        query: &str,        executor: BackgroundExecutor,    ) -> Vec<AgentModelInfo> {        let candidates = model_list            .iter()            .enumerate()            .map(|(ix, model)| StringMatchCandidate::new(ix, model.name.as_ref()))            .collect::<Vec<_>>();        let mut matches = match_strings(            &candidates,            query,            false,            true,            100,            &Default::default(),            executor,        )        .await;         matches.sort_unstable_by_key(|mat| {

Lines 1–500 of 841.