@@ -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);
}
@@ -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<ProfilePicker>),
+}
+
+pub struct ManageProfilesModal {
+ #[allow(dead_code)]
+ workspace: WeakEntity<Workspace>,
+ mode: Mode,
+}
+
+impl ManageProfilesModal {
+ pub fn register(
+ workspace: &mut Workspace,
+ _window: Option<&mut Window>,
+ _cx: &mut Context<Workspace>,
+ ) {
+ 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<Workspace>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> 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<Self>) {}
+
+ fn cancel(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
+}
+
+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<DismissEvent> for ManageProfilesModal {}
+
+impl Render for ManageProfilesModal {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> 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(),
+ })
+ }
+}
@@ -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<Picker<ProfilePickerDelegate>>,
+}
+
+impl ProfilePicker {
+ pub fn new(
+ delegate: ProfilePickerDelegate,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
+ Self { picker }
+ }
+}
+
+impl EventEmitter<DismissEvent> 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<Self>) -> impl IntoElement {
+ v_flex().w(rems(34.)).child(self.picker.clone())
+ }
+}
+
+#[derive(Debug)]
+pub struct ProfileEntry {
+ #[allow(dead_code)]
+ pub id: Arc<str>,
+ pub name: SharedString,
+}
+
+pub struct ProfilePickerDelegate {
+ profile_picker: WeakEntity<ProfilePicker>,
+ profiles: Vec<ProfileEntry>,
+ matches: Vec<StringMatch>,
+ selected_index: usize,
+}
+
+impl ProfilePickerDelegate {
+ pub fn new(cx: &mut Context<ProfilePicker>) -> Self {
+ let settings = AssistantSettings::get_global(cx);
+
+ let profiles = settings
+ .profiles
+ .iter()
+ .map(|(id, profile)| ProfileEntry {
+ id: id.clone(),
+ name: profile.name.clone(),
+ })
+ .collect::<Vec<_>>();
+
+ 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<Picker<Self>>,
+ ) {
+ self.selected_index = ix;
+ }
+
+ fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+ "Search profilesβ¦".into()
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ window: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Task<()> {
+ let background = cx.background_executor().clone();
+ let candidates = self
+ .profiles
+ .iter()
+ .enumerate()
+ .map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
+ .collect::<Vec<_>>();
+
+ 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<Picker<Self>>) {
+ }
+
+ fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ 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<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ 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(),
+ )),
+ )
+ }
+}