From 0ac717c3a873164944905d84b99757a451cd0235 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 26 Mar 2025 18:01:34 -0400 Subject: [PATCH] assistant2: Start on modal for managing profiles (#27546) This PR starts work on a modal for managing profiles. Release Notes: - N/A --- crates/assistant2/src/assistant.rs | 4 +- .../assistant2/src/assistant_configuration.rs | 3 + .../manage_profiles_modal.rs | 79 ++++++++ .../assistant_configuration/profile_picker.rs | 181 ++++++++++++++++++ 4 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs create mode 100644 crates/assistant2/src/assistant_configuration/profile_picker.rs diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 462ca12c33..c90ec19d54 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -32,7 +32,7 @@ use prompt_store::PromptBuilder; use settings::Settings as _; pub use crate::active_thread::ActiveThread; -use crate::assistant_configuration::AddContextServerModal; +use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal}; pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate}; pub use crate::inline_assistant::InlineAssistant; pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent}; @@ -47,6 +47,7 @@ actions!( RemoveAllContext, OpenHistory, OpenConfiguration, + ManageProfiles, AddContextServer, RemoveSelectedThread, Chat, @@ -89,6 +90,7 @@ pub fn init( cx, ); cx.observe_new(AddContextServerModal::register).detach(); + cx.observe_new(ManageProfilesModal::register).detach(); feature_gate_assistant2_actions(cx); } diff --git a/crates/assistant2/src/assistant_configuration.rs b/crates/assistant2/src/assistant_configuration.rs index b215709be2..4d275a181d 100644 --- a/crates/assistant2/src/assistant_configuration.rs +++ b/crates/assistant2/src/assistant_configuration.rs @@ -1,4 +1,6 @@ mod add_context_server_modal; +mod manage_profiles_modal; +mod profile_picker; use std::sync::Arc; @@ -12,6 +14,7 @@ use util::ResultExt as _; use zed_actions::ExtensionCategoryFilter; pub(crate) use add_context_server_modal::AddContextServerModal; +pub(crate) use manage_profiles_modal::ManageProfilesModal; use crate::AddContextServer; diff --git a/crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs b/crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs new file mode 100644 index 0000000000..27681d5cfc --- /dev/null +++ b/crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs @@ -0,0 +1,79 @@ +use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity}; +use ui::prelude::*; +use workspace::{ModalView, Workspace}; + +use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate}; +use crate::ManageProfiles; + +enum Mode { + ChooseProfile(Entity), +} + +pub struct ManageProfilesModal { + #[allow(dead_code)] + workspace: WeakEntity, + mode: Mode, +} + +impl ManageProfilesModal { + pub fn register( + workspace: &mut Workspace, + _window: Option<&mut Window>, + _cx: &mut Context, + ) { + workspace.register_action(|workspace, _: &ManageProfiles, window, cx| { + let workspace_handle = cx.entity().downgrade(); + workspace.toggle_modal(window, cx, |window, cx| { + Self::new(workspace_handle, window, cx) + }) + }); + } + + pub fn new( + workspace: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + Self { + workspace, + mode: Mode::ChooseProfile(cx.new(|cx| { + let delegate = ProfilePickerDelegate::new(cx); + ProfilePicker::new(delegate, window, cx) + })), + } + } + + fn confirm(&mut self, _window: &mut Window, _cx: &mut Context) {} + + fn cancel(&mut self, _window: &mut Window, _cx: &mut Context) {} +} + +impl ModalView for ManageProfilesModal {} + +impl Focusable for ManageProfilesModal { + fn focus_handle(&self, cx: &App) -> FocusHandle { + match &self.mode { + Mode::ChooseProfile(profile_picker) => profile_picker.read(cx).focus_handle(cx), + } + } +} + +impl EventEmitter for ManageProfilesModal {} + +impl Render for ManageProfilesModal { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .elevation_3(cx) + .w(rems(34.)) + .key_context("ManageProfilesModal") + .on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx))) + .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx))) + .capture_any_mouse_down(cx.listener(|this, _, window, cx| { + this.focus_handle(cx).focus(window); + })) + .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent))) + .child(match &self.mode { + Mode::ChooseProfile(profile_picker) => profile_picker.clone().into_any_element(), + }) + } +} diff --git a/crates/assistant2/src/assistant_configuration/profile_picker.rs b/crates/assistant2/src/assistant_configuration/profile_picker.rs new file mode 100644 index 0000000000..3762a865b8 --- /dev/null +++ b/crates/assistant2/src/assistant_configuration/profile_picker.rs @@ -0,0 +1,181 @@ +use std::sync::Arc; + +use assistant_settings::AssistantSettings; +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + App, Context, DismissEvent, Entity, EventEmitter, Focusable, SharedString, Task, WeakEntity, + Window, +}; +use picker::{Picker, PickerDelegate}; +use settings::Settings; +use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; +use util::ResultExt as _; + +pub struct ProfilePicker { + picker: Entity>, +} + +impl ProfilePicker { + pub fn new( + delegate: ProfilePickerDelegate, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); + Self { picker } + } +} + +impl EventEmitter for ProfilePicker {} + +impl Focusable for ProfilePicker { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl Render for ProfilePicker { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + v_flex().w(rems(34.)).child(self.picker.clone()) + } +} + +#[derive(Debug)] +pub struct ProfileEntry { + #[allow(dead_code)] + pub id: Arc, + pub name: SharedString, +} + +pub struct ProfilePickerDelegate { + profile_picker: WeakEntity, + profiles: Vec, + matches: Vec, + selected_index: usize, +} + +impl ProfilePickerDelegate { + pub fn new(cx: &mut Context) -> Self { + let settings = AssistantSettings::get_global(cx); + + let profiles = settings + .profiles + .iter() + .map(|(id, profile)| ProfileEntry { + id: id.clone(), + name: profile.name.clone(), + }) + .collect::>(); + + Self { + profile_picker: cx.entity().downgrade(), + profiles, + matches: Vec::new(), + selected_index: 0, + } + } +} + +impl PickerDelegate for ProfilePickerDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) { + self.selected_index = ix; + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Search profiles…".into() + } + + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { + let background = cx.background_executor().clone(); + let candidates = self + .profiles + .iter() + .enumerate() + .map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref())) + .collect::>(); + + cx.spawn_in(window, async move |this, cx| { + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0., + }) + .collect() + } else { + match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + ) + .await + }; + + this.update(cx, |this, _cx| { + this.delegate.matches = matches; + this.delegate.selected_index = this + .delegate + .selected_index + .min(this.delegate.matches.len().saturating_sub(1)); + }) + .log_err(); + }) + } + + fn confirm(&mut self, _secondary: bool, _window: &mut Window, _cx: &mut Context>) { + } + + fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { + self.profile_picker + .update(cx, |_this, cx| cx.emit(DismissEvent)) + .log_err(); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _window: &mut Window, + _cx: &mut Context>, + ) -> Option { + let profile_match = &self.matches[ix]; + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .child(HighlightedLabel::new( + profile_match.string.clone(), + profile_match.positions.clone(), + )), + ) + } +}