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 | "prompt_store", | 491 | "prompt_store", |
| 492 | "proto", | 492 | "proto", |
| 493 | "rand 0.8.5", | 493 | "rand 0.8.5", |
| 494 | "regex", | ||
| 495 | "release_channel", | 494 | "release_channel", |
| 496 | "rope", | 495 | "rope", |
| 497 | "serde", | 496 | "serde", |
| @@ -62,7 +62,6 @@ prompt_library.workspace = true · 61 unmodified lines | |||
| 62 | prompt_store.workspace = true | 62 | prompt_store.workspace = true |
| 63 | proto.workspace = true | 63 | proto.workspace = true |
| 64 | release_channel.workspace = true | 64 | release_channel.workspace = true |
| 65 | regex.workspace = true | ||
| 66 | rope.workspace = true | 65 | rope.workspace = true |
| 67 | serde.workspace = true | 66 | serde.workspace = true |
| 68 | serde_json.workspace = true | 67 | serde_json.workspace = true |
| @@ -1,6 +1,7 @@ | |||
| 1 | mod add_context_server_modal; | 1 | mod add_context_server_modal; |
| 2 | mod manage_profiles_modal; | 2 | mod manage_profiles_modal; |
| 3 | mod profile_picker; | 3 | mod profile_picker; |
| 4 | mod tool_picker; | ||
| 4 | 5 | ||
| 5 | use std::sync::Arc; | 6 | use std::sync::Arc; |
| 6 | 7 | ||
| @@ -1,17 +1,33 @@ | |||
| 1 | use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity}; | 1 | use std::sync::Arc; |
| 2 | use ui::prelude::*; | 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 | use workspace::{ModalView, Workspace}; | 9 | use workspace::{ModalView, Workspace}; |
| 4 | 10 | ||
| 5 | use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate}; | 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 | enum Mode { | 15 | enum Mode { |
| 9 | ChooseProfile(Entity<ProfilePicker>), | 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 | pub struct ManageProfilesModal { | 27 | pub struct ManageProfilesModal { |
| 13 | #[allow(dead_code)] | 28 | fs: Arc<dyn Fs>, |
| 14 | workspace: WeakEntity<Workspace>, | 29 | tools: Arc<ToolWorkingSet>, |
| 30 | focus_handle: FocusHandle, | ||
| 15 | mode: Mode, | 31 | mode: Mode, |
| 16 | } | 32 | } |
| 17 | 33 | ||
| @@ -22,27 +38,79 @@ impl ManageProfilesModal { · 4 unmodified lines | |||
| 22 | _cx: &mut Context<Workspace>, | 38 | _cx: &mut Context<Workspace>, |
| 23 | ) { | 39 | ) { |
| 24 | workspace.register_action(|workspace, _: &ManageProfiles, window, cx| { | 40 | workspace.register_action(|workspace, _: &ManageProfiles, window, cx| { |
| 25 | let workspace_handle = cx.entity().downgrade(); | 41 | if let Some(panel) = workspace.panel::<AssistantPanel>(cx) { |
| 26 | workspace.toggle_modal(window, cx, |window, cx| { | 42 | let fs = workspace.app_state().fs.clone(); |
| 27 | Self::new(workspace_handle, window, cx) | 43 | let thread_store = panel.read(cx).thread_store().read(cx); |
| 28 | }) | 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 | pub fn new( | 50 | pub fn new( |
| 33 | workspace: WeakEntity<Workspace>, | 51 | fs: Arc<dyn Fs>, |
| 52 | tools: Arc<ToolWorkingSet>, | ||
| 34 | window: &mut Window, | 53 | window: &mut Window, |
| 35 | cx: &mut Context<Self>, | 54 | cx: &mut Context<Self>, |
| 36 | ) -> Self { | 55 | ) -> Self { |
| 56 | let focus_handle = cx.focus_handle(); | ||
| 57 | let handle = cx.entity(); | ||
| 58 | |||
| 37 | Self { | 59 | Self { |
| 38 | workspace, | 60 | fs, |
| 61 | tools, | ||
| 62 | focus_handle, | ||
| 39 | mode: Mode::ChooseProfile(cx.new(|cx| { | 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 | ProfilePicker::new(delegate, window, cx) | 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 | fn confirm(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {} | 114 | fn confirm(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {} |
| 47 | 115 | ||
| 48 | fn cancel(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {} | 116 | fn cancel(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {} |
| @@ -53,15 +121,65 @@ impl ModalView for ManageProfilesModal {} · 4 unmodified lines | |||
| 53 | impl Focusable for ManageProfilesModal { | 121 | impl Focusable for ManageProfilesModal { |
| 54 | fn focus_handle(&self, cx: &App) -> FocusHandle { | 122 | fn focus_handle(&self, cx: &App) -> FocusHandle { |
| 55 | match &self.mode { | 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 | impl EventEmitter<DismissEvent> for ManageProfilesModal {} | 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 | impl Render for ManageProfilesModal { | 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 | div() | 183 | div() |
| 66 | .elevation_3(cx) | 184 | .elevation_3(cx) |
| 67 | .w(rems(34.)) | 185 | .w(rems(34.)) |
| @@ -74,6 +192,10 @@ impl Render for ManageProfilesModal { · 6 unmodified lines | |||
| 74 | .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent))) | 192 | .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent))) |
| 75 | .child(match &self.mode { | 193 | .child(match &self.mode { |
| 76 | Mode::ChooseProfile(profile_picker) => profile_picker.clone().into_any_element(), | 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 | #[derive(Debug)] | 43 | #[derive(Debug)] |
| 44 | pub struct ProfileEntry { | 44 | pub struct ProfileEntry { |
| 45 | #[allow(dead_code)] | ||
| 46 | pub id: Arc<str>, | 45 | pub id: Arc<str>, |
| 47 | pub name: SharedString, | 46 | pub name: SharedString, |
| 48 | } | 47 | } |
| @@ -52,10 +51,14 @@ pub struct ProfilePickerDelegate { · 3 unmodified lines | |||
| 52 | profiles: Vec<ProfileEntry>, | 51 | profiles: Vec<ProfileEntry>, |
| 53 | matches: Vec<StringMatch>, | 52 | matches: Vec<StringMatch>, |
| 54 | selected_index: usize, | 53 | selected_index: usize, |
| 54 | on_confirm: Arc<dyn Fn(&Arc<str>, &mut Window, &mut App) + 'static>, | ||
| 55 | } | 55 | } |
| 56 | 56 | ||
| 57 | impl ProfilePickerDelegate { | 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 | let settings = AssistantSettings::get_global(cx); | 62 | let settings = AssistantSettings::get_global(cx); |
| 60 | 63 | ||
| 61 | let profiles = settings | 64 | let profiles = settings |
| @@ -72,6 +75,7 @@ impl ProfilePickerDelegate { · 10 unmodified lines | |||
| 72 | profiles, | 75 | profiles, |
| 73 | matches: Vec::new(), | 76 | matches: Vec::new(), |
| 74 | selected_index: 0, | 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 | fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) { | 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 | use assistant_settings::{AgentProfile, AssistantSettings}; | 3 | use assistant_settings::{AgentProfile, AssistantSettings}; |
| 5 | use editor::scroll::Autoscroll; | ||
| 6 | use editor::Editor; | ||
| 7 | use fs::Fs; | 4 | use fs::Fs; |
| 8 | use gpui::{prelude::*, AsyncWindowContext, Entity, Subscription, WeakEntity}; | 5 | use gpui::{prelude::*, Action, Entity, Subscription, WeakEntity}; |
| 9 | use indexmap::IndexMap; | 6 | use indexmap::IndexMap; |
| 10 | use regex::Regex; | ||
| 11 | use settings::{update_settings_file, Settings as _, SettingsStore}; | 7 | use settings::{update_settings_file, Settings as _, SettingsStore}; |
| 12 | use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip}; | 8 | use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip}; |
| 13 | use util::ResultExt as _; | 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 | pub struct ProfileSelector { | 13 | pub struct ProfileSelector { |
| 19 | profiles: IndexMap<Arc<str>, AgentProfile>, | 14 | profiles: IndexMap<Arc<str>, AgentProfile>, |
| @@ -92,89 +87,13 @@ impl ProfileSelector { · 72 unmodified lines | |||
| 92 | .icon(IconName::Pencil) | 87 | .icon(IconName::Pencil) |
| 93 | .icon_color(Color::Muted) | 88 | .icon_color(Color::Muted) |
| 94 | .handler(move |window, cx| { | 89 | .handler(move |window, cx| { |
| 95 | if let Some(workspace) = window.root().flatten() { | 90 | window.dispatch_action(ManageProfiles.boxed_clone(), cx); |
| 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 | } | ||
| 103 | }), | 91 | }), |
| 104 | ); | 92 | ); |
| 105 | 93 | ||
| 106 | menu | 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 | impl Render for ProfileSelector { | 99 | impl Render for ProfileSelector { |
| @@ -12,7 +12,7 @@ pub struct AgentProfile { · 11 unmodified lines | |||
| 12 | pub context_servers: IndexMap<Arc<str>, ContextServerPreset>, | 12 | pub context_servers: IndexMap<Arc<str>, ContextServerPreset>, |
| 13 | } | 13 | } |
| 14 | 14 | ||
| 15 | #[derive(Debug, Clone)] | 15 | #[derive(Debug, Clone, Default)] |
| 16 | pub struct ContextServerPreset { | 16 | pub struct ContextServerPreset { |
| 17 | pub tools: IndexMap<Arc<str>, bool>, | 17 | pub tools: IndexMap<Arc<str>, bool>, |
| 18 | } | 18 | } |
| @@ -442,7 +442,7 @@ pub struct AgentProfileContent { · 441 unmodified lines | |||
| 442 | pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>, | 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 | pub struct ContextServerPresetContent { | 446 | pub struct ContextServerPresetContent { |
| 447 | pub tools: IndexMap<Arc<str>, bool>, | 447 | pub tools: IndexMap<Arc<str>, bool>, |
| 448 | } | 448 | } |
| @@ -2,6 +2,7 @@ use crate::prelude::*; · 1 unmodified line | |||
| 2 | use gpui::{AnyElement, FocusHandle, ScrollAnchor, ScrollHandle}; | 2 | use gpui::{AnyElement, FocusHandle, ScrollAnchor, ScrollHandle}; |
| 3 | 3 | ||
| 4 | /// An element that can be navigated through via keyboard. Intended for use with scrollable views that want to use | 4 | /// An element that can be navigated through via keyboard. Intended for use with scrollable views that want to use |
| 5 | #[derive(IntoElement)] | ||
| 5 | pub struct Navigable { | 6 | pub struct Navigable { |
| 6 | child: AnyElement, | 7 | child: AnyElement, |
| 7 | selectable_children: Vec<NavigableEntry>, | 8 | selectable_children: Vec<NavigableEntry>, |