This PR adds the ability to configure tools for a profile in the UI: https://github.com/user-attachments/assets/16642f14-8faa-4a91-bb9e-1d480692f1f2 Note: Doesn't yet work for customizing tools for the default profiles. Release Notes: - N/A
Unified · Split
| @@ -491,7 +491,6 @@ dependencies = [ · 490 unmodified lines | ||
| 491 | 491 | "prompt_store", |
| 492 | 492 | "proto", |
| 493 | 493 | "rand 0.8.5", |
| 494 | − "regex", | |
| 495 | 494 | "release_channel", |
| 496 | 495 | "rope", |
| 497 | 496 | "serde", |
| @@ -62,7 +62,6 @@ prompt_library.workspace = true · 61 unmodified lines | ||
| 62 | 62 | prompt_store.workspace = true |
| 63 | 63 | proto.workspace = true |
| 64 | 64 | release_channel.workspace = true |
| 65 | −regex.workspace = true | |
| 66 | 65 | rope.workspace = true |
| 67 | 66 | serde.workspace = true |
| 68 | 67 | serde_json.workspace = true |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | mod add_context_server_modal; |
| 2 | 2 | mod manage_profiles_modal; |
| 3 | 3 | mod profile_picker; |
| 4 | +mod tool_picker; | |
| 4 | 5 | |
| 5 | 6 | use std::sync::Arc; |
| 6 | 7 | |
| @@ -1,17 +1,33 @@ | ||
| 1 | −use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity}; | |
| 2 | −use ui::prelude::*; | |
| 1 | +use std::sync::Arc; | |
| 2 | + | |
| 3 | +use assistant_settings::AssistantSettings; | |
| 4 | +use assistant_tool::ToolWorkingSet; | |
| 5 | +use fs::Fs; | |
| 6 | +use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable}; | |
| 7 | +use settings::Settings as _; | |
| 8 | +use ui::{prelude::*, ListItem, ListItemSpacing, Navigable, NavigableEntry}; | |
| 3 | 9 | use workspace::{ModalView, Workspace}; |
| 4 | 10 | |
| 5 | 11 | use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate}; |
| 6 | −use crate::ManageProfiles; | |
| 12 | +use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate}; | |
| 13 | +use crate::{AssistantPanel, ManageProfiles}; | |
| 7 | 14 | |
| 8 | 15 | enum Mode { |
| 9 | 16 | ChooseProfile(Entity<ProfilePicker>), |
| 17 | + ViewProfile(ViewProfileMode), | |
| 18 | + ConfigureTools(Entity<ToolPicker>), | |
| 19 | +} | |
| 20 | + | |
| 21 | +#[derive(Clone)] | |
| 22 | +pub struct ViewProfileMode { | |
| 23 | + profile_id: Arc<str>, | |
| 24 | + configure_tools: NavigableEntry, | |
| 10 | 25 | } |
| 11 | 26 | |
| 12 | 27 | pub struct ManageProfilesModal { |
| 13 | − #[allow(dead_code)] | |
| 14 | − workspace: WeakEntity<Workspace>, | |
| 28 | + fs: Arc<dyn Fs>, | |
| 29 | + tools: Arc<ToolWorkingSet>, | |
| 30 | + focus_handle: FocusHandle, | |
| 15 | 31 | mode: Mode, |
| 16 | 32 | } |
| 17 | 33 | |
| @@ -22,27 +38,79 @@ impl ManageProfilesModal { · 4 unmodified lines | ||
| 22 | 38 | _cx: &mut Context<Workspace>, |
| 23 | 39 | ) { |
| 24 | 40 | workspace.register_action(|workspace, _: &ManageProfiles, window, cx| { |
| 25 | − let workspace_handle = cx.entity().downgrade(); | |
| 26 | − workspace.toggle_modal(window, cx, |window, cx| { | |
| 27 | − Self::new(workspace_handle, window, cx) | |
| 28 | − }) | |
| 41 | + if let Some(panel) = workspace.panel::<AssistantPanel>(cx) { | |
| 42 | + let fs = workspace.app_state().fs.clone(); | |
| 43 | + let thread_store = panel.read(cx).thread_store().read(cx); | |
| 44 | + let tools = thread_store.tools(); | |
| 45 | + workspace.toggle_modal(window, cx, |window, cx| Self::new(fs, tools, window, cx)) | |
| 46 | + } | |
| 29 | 47 | }); |
| 30 | 48 | } |
| 31 | 49 | |
| 32 | 50 | pub fn new( |
| 33 | − workspace: WeakEntity<Workspace>, | |
| 51 | + fs: Arc<dyn Fs>, | |
| 52 | + tools: Arc<ToolWorkingSet>, | |
| 34 | 53 | window: &mut Window, |
| 35 | 54 | cx: &mut Context<Self>, |
| 36 | 55 | ) -> Self { |
| 56 | + let focus_handle = cx.focus_handle(); | |
| 57 | + let handle = cx.entity(); | |
| 58 | + | |
| 37 | 59 | Self { |
| 38 | − workspace, | |
| 60 | + fs, | |
| 61 | + tools, | |
| 62 | + focus_handle, | |
| 39 | 63 | mode: Mode::ChooseProfile(cx.new(|cx| { |
| 40 | − let delegate = ProfilePickerDelegate::new(cx); | |
| 64 | + let delegate = ProfilePickerDelegate::new( | |
| 65 | + move |profile_id, window, cx| { | |
| 66 | + handle.update(cx, |this, cx| { | |
| 67 | + this.view_profile(profile_id.clone(), window, cx); | |
| 68 | + }) | |
| 69 | + }, | |
| 70 | + cx, | |
| 71 | + ); | |
| 41 | 72 | ProfilePicker::new(delegate, window, cx) |
| 42 | 73 | })), |
| 43 | 74 | } |
| 44 | 75 | } |
| 45 | 76 | |
| 77 | + pub fn view_profile( | |
| 78 | + &mut self, | |
| 79 | + profile_id: Arc<str>, | |
| 80 | + window: &mut Window, | |
| 81 | + cx: &mut Context<Self>, | |
| 82 | + ) { | |
| 83 | + self.mode = Mode::ViewProfile(ViewProfileMode { | |
| 84 | + profile_id, | |
| 85 | + configure_tools: NavigableEntry::focusable(cx), | |
| 86 | + }); | |
| 87 | + self.focus_handle(cx).focus(window); | |
| 88 | + } | |
| 89 | + | |
| 90 | + fn configure_tools( | |
| 91 | + &mut self, | |
| 92 | + profile_id: Arc<str>, | |
| 93 | + window: &mut Window, | |
| 94 | + cx: &mut Context<Self>, | |
| 95 | + ) { | |
| 96 | + let settings = AssistantSettings::get_global(cx); | |
| 97 | + let Some(profile) = settings.profiles.get(&profile_id).cloned() else { | |
| 98 | + return; | |
| 99 | + }; | |
| 100 | + | |
| 101 | + self.mode = Mode::ConfigureTools(cx.new(|cx| { | |
| 102 | + let delegate = ToolPickerDelegate::new( | |
| 103 | + self.fs.clone(), | |
| 104 | + self.tools.clone(), | |
| 105 | + profile_id, | |
| 106 | + profile, | |
| 107 | + cx, | |
| 108 | + ); | |
| 109 | + ToolPicker::new(delegate, window, cx) | |
| 110 | + })); | |
| 111 | + self.focus_handle(cx).focus(window); | |
| 112 | + } | |
| 113 | + | |
| 46 | 114 | fn confirm(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {} |
| 47 | 115 | |
| 48 | 116 | fn cancel(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {} |
| @@ -53,15 +121,65 @@ impl ModalView for ManageProfilesModal {} · 4 unmodified lines | ||
| 53 | 121 | impl Focusable for ManageProfilesModal { |
| 54 | 122 | fn focus_handle(&self, cx: &App) -> FocusHandle { |
| 55 | 123 | match &self.mode { |
| 56 | − Mode::ChooseProfile(profile_picker) => profile_picker.read(cx).focus_handle(cx), | |
| 124 | + Mode::ChooseProfile(profile_picker) => profile_picker.focus_handle(cx), | |
| 125 | + Mode::ConfigureTools(tool_picker) => tool_picker.focus_handle(cx), | |
| 126 | + Mode::ViewProfile(_) => self.focus_handle.clone(), | |
| 57 | 127 | } |
| 58 | 128 | } |
| 59 | 129 | } |
| 60 | 130 | |
| 61 | 131 | impl EventEmitter<DismissEvent> for ManageProfilesModal {} |
| 62 | 132 | |
| 133 | +impl ManageProfilesModal { | |
| 134 | + fn render_view_profile( | |
| 135 | + &mut self, | |
| 136 | + mode: ViewProfileMode, | |
| 137 | + window: &mut Window, | |
| 138 | + cx: &mut Context<Self>, | |
| 139 | + ) -> impl IntoElement { | |
| 140 | + Navigable::new( | |
| 141 | + div() | |
| 142 | + .track_focus(&self.focus_handle(cx)) | |
| 143 | + .size_full() | |
| 144 | + .child( | |
| 145 | + v_flex().child( | |
| 146 | + div() | |
| 147 | + .id("configure-tools") | |
| 148 | + .track_focus(&mode.configure_tools.focus_handle) | |
| 149 | + .on_action({ | |
| 150 | + let profile_id = mode.profile_id.clone(); | |
| 151 | + cx.listener(move |this, _: &menu::Confirm, window, cx| { | |
| 152 | + this.configure_tools(profile_id.clone(), window, cx); | |
| 153 | + }) | |
| 154 | + }) | |
| 155 | + .child( | |
| 156 | + ListItem::new("configure-tools") | |
| 157 | + .toggle_state( | |
| 158 | + mode.configure_tools | |
| 159 | + .focus_handle | |
| 160 | + .contains_focused(window, cx), | |
| 161 | + ) | |
| 162 | + .inset(true) | |
| 163 | + .spacing(ListItemSpacing::Sparse) | |
| 164 | + .start_slot(Icon::new(IconName::Cog)) | |
| 165 | + .child(Label::new("Configure Tools")) | |
| 166 | + .on_click({ | |
| 167 | + let profile_id = mode.profile_id.clone(); | |
| 168 | + cx.listener(move |this, _, window, cx| { | |
| 169 | + this.configure_tools(profile_id.clone(), window, cx); | |
| 170 | + }) | |
| 171 | + }), | |
| 172 | + ), | |
| 173 | + ), | |
| 174 | + ) | |
| 175 | + .into_any_element(), | |
| 176 | + ) | |
| 177 | + .entry(mode.configure_tools) | |
| 178 | + } | |
| 179 | +} | |
| 180 | + | |
| 63 | 181 | impl Render for ManageProfilesModal { |
| 64 | − fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | |
| 182 | + fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { | |
| 65 | 183 | div() |
| 66 | 184 | .elevation_3(cx) |
| 67 | 185 | .w(rems(34.)) |
| @@ -74,6 +192,10 @@ impl Render for ManageProfilesModal { · 6 unmodified lines | ||
| 74 | 192 | .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent))) |
| 75 | 193 | .child(match &self.mode { |
| 76 | 194 | Mode::ChooseProfile(profile_picker) => profile_picker.clone().into_any_element(), |
| 195 | + Mode::ViewProfile(mode) => self | |
| 196 | + .render_view_profile(mode.clone(), window, cx) | |
| 197 | + .into_any_element(), | |
| 198 | + Mode::ConfigureTools(tool_picker) => tool_picker.clone().into_any_element(), | |
| 77 | 199 | }) |
| 78 | 200 | } |
| 79 | 201 | } |
| @@ -42,7 +42,6 @@ impl Render for ProfilePicker { · 41 unmodified lines | ||
| 42 | 42 | |
| 43 | 43 | #[derive(Debug)] |
| 44 | 44 | pub struct ProfileEntry { |
| 45 | − #[allow(dead_code)] | |
| 46 | 45 | pub id: Arc<str>, |
| 47 | 46 | pub name: SharedString, |
| 48 | 47 | } |
| @@ -52,10 +51,14 @@ pub struct ProfilePickerDelegate { · 3 unmodified lines | ||
| 52 | 51 | profiles: Vec<ProfileEntry>, |
| 53 | 52 | matches: Vec<StringMatch>, |
| 54 | 53 | selected_index: usize, |
| 54 | + on_confirm: Arc<dyn Fn(&Arc<str>, &mut Window, &mut App) + 'static>, | |
| 55 | 55 | } |
| 56 | 56 | |
| 57 | 57 | impl ProfilePickerDelegate { |
| 58 | − pub fn new(cx: &mut Context<ProfilePicker>) -> Self { | |
| 58 | + pub fn new( | |
| 59 | + on_confirm: impl Fn(&Arc<str>, &mut Window, &mut App) + 'static, | |
| 60 | + cx: &mut Context<ProfilePicker>, | |
| 61 | + ) -> Self { | |
| 59 | 62 | let settings = AssistantSettings::get_global(cx); |
| 60 | 63 | |
| 61 | 64 | let profiles = settings |
| @@ -72,6 +75,7 @@ impl ProfilePickerDelegate { · 10 unmodified lines | ||
| 72 | 75 | profiles, |
| 73 | 76 | matches: Vec::new(), |
| 74 | 77 | selected_index: 0, |
| 78 | + on_confirm: Arc::new(on_confirm), | |
| 75 | 79 | } |
| 76 | 80 | } |
| 77 | 81 | } |
| @@ -149,7 +153,16 @@ impl PickerDelegate for ProfilePickerDelegate { · 71 unmodified lines | ||
| 149 | 153 | }) |
| 150 | 154 | } |
| 151 | 155 | |
| 152 | − fn confirm(&mut self, _secondary: bool, _window: &mut Window, _cx: &mut Context<Picker<Self>>) { | |
| 156 | + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) { | |
| 157 | + if self.matches.is_empty() { | |
| 158 | + self.dismissed(window, cx); | |
| 159 | + return; | |
| 160 | + } | |
| 161 | + | |
| 162 | + let candidate_id = self.matches[self.selected_index].candidate_id; | |
| 163 | + let profile = &self.profiles[candidate_id]; | |
| 164 | + | |
| 165 | + (self.on_confirm)(&profile.id, window, cx); | |
| 153 | 166 | } |
| 154 | 167 | |
| 155 | 168 | fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) { |
| @@ -0,0 +1,267 @@ | ||
| 1 | +use std::sync::Arc; | |
| 2 | + | |
| 3 | +use assistant_settings::{ | |
| 4 | + AgentProfile, AssistantSettings, AssistantSettingsContent, VersionedAssistantSettingsContent, | |
| 5 | +}; | |
| 6 | +use assistant_tool::{ToolSource, ToolWorkingSet}; | |
| 7 | +use fs::Fs; | |
| 8 | +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; | |
| 9 | +use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window}; | |
| 10 | +use picker::{Picker, PickerDelegate}; | |
| 11 | +use settings::update_settings_file; | |
| 12 | +use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; | |
| 13 | +use util::ResultExt as _; | |
| 14 | + | |
| 15 | +pub struct ToolPicker { | |
| 16 | + picker: Entity<Picker<ToolPickerDelegate>>, | |
| 17 | +} | |
| 18 | + | |
| 19 | +impl ToolPicker { | |
| 20 | + pub fn new(delegate: ToolPickerDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self { | |
| 21 | + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); | |
| 22 | + Self { picker } | |
| 23 | + } | |
| 24 | +} | |
| 25 | + | |
| 26 | +impl EventEmitter<DismissEvent> for ToolPicker {} | |
| 27 | + | |
| 28 | +impl Focusable for ToolPicker { | |
| 29 | + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { | |
| 30 | + self.picker.focus_handle(cx) | |
| 31 | + } | |
| 32 | +} | |
| 33 | + | |
| 34 | +impl Render for ToolPicker { | |
| 35 | + fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { | |
| 36 | + v_flex().w(rems(34.)).child(self.picker.clone()) | |
| 37 | + } | |
| 38 | +} | |
| 39 | + | |
| 40 | +#[derive(Debug, Clone)] | |
| 41 | +pub struct ToolEntry { | |
| 42 | + pub name: Arc<str>, | |
| 43 | + pub source: ToolSource, | |
| 44 | +} | |
| 45 | + | |
| 46 | +pub struct ToolPickerDelegate { | |
| 47 | + tool_picker: WeakEntity<ToolPicker>, | |
| 48 | + fs: Arc<dyn Fs>, | |
| 49 | + tools: Vec<ToolEntry>, | |
| 50 | + profile_id: Arc<str>, | |
| 51 | + profile: AgentProfile, | |
| 52 | + matches: Vec<StringMatch>, | |
| 53 | + selected_index: usize, | |
| 54 | +} | |
| 55 | + | |
| 56 | +impl ToolPickerDelegate { | |
| 57 | + pub fn new( | |
| 58 | + fs: Arc<dyn Fs>, | |
| 59 | + tool_set: Arc<ToolWorkingSet>, | |
| 60 | + profile_id: Arc<str>, | |
| 61 | + profile: AgentProfile, | |
| 62 | + cx: &mut Context<ToolPicker>, | |
| 63 | + ) -> Self { | |
| 64 | + let mut tool_entries = Vec::new(); | |
| 65 | + | |
| 66 | + for (source, tools) in tool_set.tools_by_source(cx) { | |
| 67 | + tool_entries.extend(tools.into_iter().map(|tool| ToolEntry { | |
| 68 | + name: tool.name().into(), | |
| 69 | + source: source.clone(), | |
| 70 | + })); | |
| 71 | + } | |
| 72 | + | |
| 73 | + Self { | |
| 74 | + tool_picker: cx.entity().downgrade(), | |
| 75 | + fs, | |
| 76 | + tools: tool_entries, | |
| 77 | + profile_id, | |
| 78 | + profile, | |
| 79 | + matches: Vec::new(), | |
| 80 | + selected_index: 0, | |
| 81 | + } | |
| 82 | + } | |
| 83 | +} | |
| 84 | + | |
| 85 | +impl PickerDelegate for ToolPickerDelegate { | |
| 86 | + type ListItem = ListItem; | |
| 87 | + | |
| 88 | + fn match_count(&self) -> usize { | |
| 89 | + self.matches.len() | |
| 90 | + } | |
| 91 | + | |
| 92 | + fn selected_index(&self) -> usize { | |
| 93 | + self.selected_index | |
| 94 | + } | |
| 95 | + | |
| 96 | + fn set_selected_index( | |
| 97 | + &mut self, | |
| 98 | + ix: usize, | |
| 99 | + _window: &mut Window, | |
| 100 | + _cx: &mut Context<Picker<Self>>, | |
| 101 | + ) { | |
| 102 | + self.selected_index = ix; | |
| 103 | + } | |
| 104 | + | |
| 105 | + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> { | |
| 106 | + "Search tools…".into() | |
| 107 | + } | |
| 108 | + | |
| 109 | + fn update_matches( | |
| 110 | + &mut self, | |
| 111 | + query: String, | |
| 112 | + window: &mut Window, | |
| 113 | + cx: &mut Context<Picker<Self>>, | |
| 114 | + ) -> Task<()> { | |
| 115 | + let background = cx.background_executor().clone(); | |
| 116 | + let candidates = self | |
| 117 | + .tools | |
| 118 | + .iter() | |
| 119 | + .enumerate() | |
| 120 | + .map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref())) | |
| 121 | + .collect::<Vec<_>>(); | |
| 122 | + | |
| 123 | + cx.spawn_in(window, async move |this, cx| { | |
| 124 | + let matches = if query.is_empty() { | |
| 125 | + candidates | |
| 126 | + .into_iter() | |
| 127 | + .enumerate() | |
| 128 | + .map(|(index, candidate)| StringMatch { | |
| 129 | + candidate_id: index, | |
| 130 | + string: candidate.string, | |
| 131 | + positions: Vec::new(), | |
| 132 | + score: 0., | |
| 133 | + }) | |
| 134 | + .collect() | |
| 135 | + } else { | |
| 136 | + match_strings( | |
| 137 | + &candidates, | |
| 138 | + &query, | |
| 139 | + false, | |
| 140 | + 100, | |
| 141 | + &Default::default(), | |
| 142 | + background, | |
| 143 | + ) | |
| 144 | + .await | |
| 145 | + }; | |
| 146 | + | |
| 147 | + this.update(cx, |this, _cx| { | |
| 148 | + this.delegate.matches = matches; | |
| 149 | + this.delegate.selected_index = this | |
| 150 | + .delegate | |
| 151 | + .selected_index | |
| 152 | + .min(this.delegate.matches.len().saturating_sub(1)); | |
| 153 | + }) | |
| 154 | + .log_err(); | |
| 155 | + }) | |
| 156 | + } | |
| 157 | + | |
| 158 | + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) { | |
| 159 | + if self.matches.is_empty() { | |
| 160 | + self.dismissed(window, cx); | |
| 161 | + return; | |
| 162 | + } | |
| 163 | + | |
| 164 | + let candidate_id = self.matches[self.selected_index].candidate_id; | |
| 165 | + let tool = &self.tools[candidate_id]; | |
| 166 | + | |
| 167 | + let is_enabled = match &tool.source { | |
| 168 | + ToolSource::Native => { | |
| 169 | + let is_enabled = self.profile.tools.entry(tool.name.clone()).or_default(); | |
| 170 | + *is_enabled = !*is_enabled; | |
| 171 | + *is_enabled | |
| 172 | + } | |
| 173 | + ToolSource::ContextServer { id } => { | |
| 174 | + let preset = self | |
| 175 | + .profile | |
| 176 | + .context_servers | |
| 177 | + .entry(id.clone().into()) | |
| 178 | + .or_default(); | |
| 179 | + let is_enabled = preset.tools.entry(tool.name.clone()).or_default(); | |
| 180 | + *is_enabled = !*is_enabled; | |
| 181 | + *is_enabled | |
| 182 | + } | |
| 183 | + }; | |
| 184 | + | |
| 185 | + update_settings_file::<AssistantSettings>(self.fs.clone(), cx, { | |
| 186 | + let profile_id = self.profile_id.clone(); | |
| 187 | + let tool = tool.clone(); | |
| 188 | + move |settings, _cx| match settings { | |
| 189 | + AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2( | |
| 190 | + settings, | |
| 191 | + )) => { | |
| 192 | + if let Some(profiles) = &mut settings.profiles { | |
| 193 | + if let Some(profile) = profiles.get_mut(&profile_id) { | |
| 194 | + match tool.source { | |
| 195 | + ToolSource::Native => { | |
| 196 | + *profile.tools.entry(tool.name).or_default() = is_enabled; | |
| 197 | + } | |
| 198 | + ToolSource::ContextServer { id } => { | |
| 199 | + let preset = profile | |
| 200 | + .context_servers | |
| 201 | + .entry(id.clone().into()) | |
| 202 | + .or_default(); | |
| 203 | + *preset.tools.entry(tool.name.clone()).or_default() = | |
| 204 | + is_enabled; | |
| 205 | + } | |
| 206 | + } | |
| 207 | + } | |
| 208 | + } | |
| 209 | + } | |
| 210 | + _ => {} | |
| 211 | + } | |
| 212 | + }); | |
| 213 | + } | |
| 214 | + | |
| 215 | + fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) { | |
| 216 | + self.tool_picker | |
| 217 | + .update(cx, |_this, cx| cx.emit(DismissEvent)) | |
| 218 | + .log_err(); | |
| 219 | + } | |
| 220 | + | |
| 221 | + fn render_match( | |
| 222 | + &self, | |
| 223 | + ix: usize, | |
| 224 | + selected: bool, | |
| 225 | + _window: &mut Window, | |
| 226 | + _cx: &mut Context<Picker<Self>>, | |
| 227 | + ) -> Option<Self::ListItem> { | |
| 228 | + let tool_match = &self.matches[ix]; | |
| 229 | + let tool = &self.tools[tool_match.candidate_id]; | |
| 230 | + | |
| 231 | + let is_enabled = match &tool.source { | |
| 232 | + ToolSource::Native => self.profile.tools.get(&tool.name).copied().unwrap_or(false), | |
| 233 | + ToolSource::ContextServer { id } => self | |
| 234 | + .profile | |
| 235 | + .context_servers | |
| 236 | + .get(id.as_ref()) | |
| 237 | + .and_then(|preset| preset.tools.get(&tool.name)) | |
| 238 | + .copied() | |
| 239 | + .unwrap_or(false), | |
| 240 | + }; | |
| 241 | + | |
| 242 | + Some( | |
| 243 | + ListItem::new(ix) | |
| 244 | + .inset(true) | |
| 245 | + .spacing(ListItemSpacing::Sparse) | |
| 246 | + .toggle_state(selected) | |
| 247 | + .child( | |
| 248 | + h_flex() | |
| 249 | + .gap_2() | |
| 250 | + .child(HighlightedLabel::new( | |
| 251 | + tool_match.string.clone(), | |
| 252 | + tool_match.positions.clone(), | |
| 253 | + )) | |
| 254 | + .map(|parent| match &tool.source { | |
| 255 | + ToolSource::Native => parent, | |
| 256 | + ToolSource::ContextServer { id } => parent | |
| 257 | + .child(Label::new(id).size(LabelSize::XSmall).color(Color::Muted)), | |
| 258 | + }), | |
| 259 | + ) | |
| 260 | + .end_slot::<Icon>(is_enabled.then(|| { | |
| 261 | + Icon::new(IconName::Check) | |
| 262 | + .size(IconSize::Small) | |
| 263 | + .color(Color::Success) | |
| 264 | + })), | |
| 265 | + ) | |
| 266 | + } | |
| 267 | +} | |
| @@ -1,19 +1,14 @@ | ||
| 1 | −use std::sync::{Arc, LazyLock}; | |
| 1 | +use std::sync::Arc; | |
| 2 | 2 | |
| 3 | −use anyhow::Result; | |
| 4 | 3 | use assistant_settings::{AgentProfile, AssistantSettings}; |
| 5 | −use editor::scroll::Autoscroll; | |
| 6 | −use editor::Editor; | |
| 7 | 4 | use fs::Fs; |
| 8 | −use gpui::{prelude::*, AsyncWindowContext, Entity, Subscription, WeakEntity}; | |
| 5 | +use gpui::{prelude::*, Action, Entity, Subscription, WeakEntity}; | |
| 9 | 6 | use indexmap::IndexMap; |
| 10 | −use regex::Regex; | |
| 11 | 7 | use settings::{update_settings_file, Settings as _, SettingsStore}; |
| 12 | 8 | use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip}; |
| 13 | 9 | use util::ResultExt as _; |
| 14 | −use workspace::{create_and_open_local_file, Workspace}; | |
| 15 | 10 | |
| 16 | −use crate::ThreadStore; | |
| 11 | +use crate::{ManageProfiles, ThreadStore}; | |
| 17 | 12 | |
| 18 | 13 | pub struct ProfileSelector { |
| 19 | 14 | profiles: IndexMap<Arc<str>, AgentProfile>, |
| @@ -92,89 +87,13 @@ impl ProfileSelector { · 72 unmodified lines | ||
| 92 | 87 | .icon(IconName::Pencil) |
| 93 | 88 | .icon_color(Color::Muted) |
| 94 | 89 | .handler(move |window, cx| { |
| 95 | − if let Some(workspace) = window.root().flatten() { | |
| 96 | − let workspace = workspace.downgrade(); | |
| 97 | − window | |
| 98 | − .spawn(cx, async |cx| { | |
| 99 | − Self::open_profiles_setting_in_editor(workspace, cx).await | |
| 100 | − }) | |
| 101 | − .detach_and_log_err(cx); | |
| 102 | − } | |
| 90 | + window.dispatch_action(ManageProfiles.boxed_clone(), cx); | |
| 103 | 91 | }), |
| 104 | 92 | ); |
| 105 | 93 | |
| 106 | 94 | menu |
| 107 | 95 | }) |
| 108 | 96 | } |
| 109 | − | |
| 110 | − async fn open_profiles_setting_in_editor( | |
| 111 | − workspace: WeakEntity<Workspace>, | |
| 112 | − cx: &mut AsyncWindowContext, | |
| 113 | − ) -> Result<()> { | |
| 114 | − let settings_editor = workspace | |
| 115 | − .update_in(cx, |_, window, cx| { | |
| 116 | − create_and_open_local_file(paths::settings_file(), window, cx, || { | |
| 117 | − settings::initial_user_settings_content().as_ref().into() | |
| 118 | − }) | |
| 119 | − })? | |
| 120 | − .await? | |
| 121 | − .downcast::<Editor>() | |
| 122 | − .unwrap(); | |
| 123 | − | |
| 124 | − settings_editor | |
| 125 | − .downgrade() | |
| 126 | − .update_in(cx, |editor, window, cx| { | |
| 127 | − let text = editor.buffer().read(cx).snapshot(cx).text(); | |
| 128 | − | |
| 129 | − let settings = cx.global::<SettingsStore>(); | |
| 130 | − | |
| 131 | − let edits = | |
| 132 | − settings.edits_for_update::<AssistantSettings>( | |
| 133 | − &text, | |
| 134 | − |settings| match settings { | |
| 135 | − assistant_settings::AssistantSettingsContent::Versioned(settings) => { | |
| 136 | − match settings { | |
| 137 | − assistant_settings::VersionedAssistantSettingsContent::V2( | |
| 138 | − settings, | |
| 139 | − ) => { | |
| 140 | − settings.profiles.get_or_insert_with(IndexMap::default); | |
| 141 | − } | |
| 142 | − assistant_settings::VersionedAssistantSettingsContent::V1( | |
| 143 | − _, | |
| 144 | − ) => {} | |
| 145 | − } | |
| 146 | − } | |
| 147 | − assistant_settings::AssistantSettingsContent::Legacy(_) => {} | |
| 148 | − }, | |
| 149 | − ); | |
| 150 | − | |
| 151 | − if !edits.is_empty() { | |
| 152 | − editor.edit(edits.iter().cloned(), cx); | |
| 153 | − } | |
| 154 | − | |
| 155 | − let text = editor.buffer().read(cx).snapshot(cx).text(); | |
| 156 | − | |
| 157 | − static PROFILES_REGEX: LazyLock<Regex> = | |
| 158 | − LazyLock::new(|| Regex::new(r#"(?P<key>"profiles":)\s*\{"#).unwrap()); | |
| 159 | − let range = PROFILES_REGEX.captures(&text).and_then(|captures| { | |
| 160 | − captures | |
| 161 | − .name("key") | |
| 162 | − .map(|inner_match| inner_match.start()..inner_match.end()) | |
| 163 | − }); | |
| 164 | − if let Some(range) = range { | |
| 165 | − editor.change_selections( | |
| 166 | − Some(Autoscroll::newest()), | |
| 167 | − window, | |
| 168 | − cx, | |
| 169 | − |selections| { | |
| 170 | − selections.select_ranges(vec![range]); | |
| 171 | − }, | |
| 172 | − ); | |
| 173 | − } | |
| 174 | − })?; | |
| 175 | − | |
| 176 | − anyhow::Ok(()) | |
| 177 | − } | |
| 178 | 97 | } |
| 179 | 98 | |
| 180 | 99 | impl Render for ProfileSelector { |
| @@ -12,7 +12,7 @@ pub struct AgentProfile { · 11 unmodified lines | ||
| 12 | 12 | pub context_servers: IndexMap<Arc<str>, ContextServerPreset>, |
| 13 | 13 | } |
| 14 | 14 | |
| 15 | −#[derive(Debug, Clone)] | |
| 15 | +#[derive(Debug, Clone, Default)] | |
| 16 | 16 | pub struct ContextServerPreset { |
| 17 | 17 | pub tools: IndexMap<Arc<str>, bool>, |
| 18 | 18 | } |
| @@ -442,7 +442,7 @@ pub struct AgentProfileContent { · 441 unmodified lines | ||
| 442 | 442 | pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>, |
| 443 | 443 | } |
| 444 | 444 | |
| 445 | −#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] | |
| 445 | +#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)] | |
| 446 | 446 | pub struct ContextServerPresetContent { |
| 447 | 447 | pub tools: IndexMap<Arc<str>, bool>, |
| 448 | 448 | } |
| @@ -2,6 +2,7 @@ use crate::prelude::*; · 1 unmodified line | ||
| 2 | 2 | use gpui::{AnyElement, FocusHandle, ScrollAnchor, ScrollHandle}; |
| 3 | 3 | |
| 4 | 4 | /// An element that can be navigated through via keyboard. Intended for use with scrollable views that want to use |
| 5 | +#[derive(IntoElement)] | |
| 5 | 6 | pub struct Navigable { |
| 6 | 7 | child: AnyElement, |
| 7 | 8 | selectable_children: Vec<NavigableEntry>, |