History

assistant2: Add ability to configure tools for profiles in the UI (#27562)

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

Marshall Bowers committed 1 year ago

Commit 231e9c21 parent 47b94e510 files · +427 −106Browse files

Unified · Split

10 files
  1. Cargo.lock +0 −1
  2. crates/assistant2/Cargo.toml +0 −1
  3. crates/assistant2/src/assistant_configuration.rs +1 −0
  4. crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs +136 −14
  5. crates/assistant2/src/assistant_configuration/profile_picker.rs +16 −3
  6. crates/assistant2/src/assistant_configuration/tool_picker.rs +267 −0
  7. crates/assistant2/src/profile_selector.rs +4 −85
  8. crates/assistant_settings/src/agent_profile.rs +1 −1
  9. crates/assistant_settings/src/assistant_settings.rs +1 −1
  10. crates/ui/src/components/navigable.rs +1 −0
Cargo.lock+0−1
@@ -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",
crates/assistant2/Cargo.toml+0−1
@@ -62,7 +62,6 @@ prompt_library.workspace = true · 61 unmodified lines
62prompt_store.workspace = true62prompt_store.workspace = true
63proto.workspace = true63proto.workspace = true
64release_channel.workspace = true64release_channel.workspace = true
65regex.workspace = true
66rope.workspace = true65rope.workspace = true
67serde.workspace = true66serde.workspace = true
68serde_json.workspace = true67serde_json.workspace = true
crates/assistant2/src/assistant_configuration.rs+1−0
@@ -1,6 +1,7 @@
1mod add_context_server_modal;1mod add_context_server_modal;
2mod manage_profiles_modal;2mod manage_profiles_modal;
3mod profile_picker;3mod profile_picker;
4mod tool_picker;
4 5
5use std::sync::Arc;6use std::sync::Arc;
6 7
crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs+136−14
@@ -1,17 +1,33 @@
1use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity};1use std::sync::Arc;
2use ui::prelude::*;2
3use assistant_settings::AssistantSettings;
4use assistant_tool::ToolWorkingSet;
5use fs::Fs;
6use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable};
7use settings::Settings as _;
8use ui::{prelude::*, ListItem, ListItemSpacing, Navigable, NavigableEntry};
3use workspace::{ModalView, Workspace};9use workspace::{ModalView, Workspace};
4 10
5use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};11use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};
6use crate::ManageProfiles;12use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
13use crate::{AssistantPanel, ManageProfiles};
7 14
8enum Mode {15enum Mode {
9 ChooseProfile(Entity<ProfilePicker>),16 ChooseProfile(Entity<ProfilePicker>),
17 ViewProfile(ViewProfileMode),
18 ConfigureTools(Entity<ToolPicker>),
19}
20
21#[derive(Clone)]
22pub struct ViewProfileMode {
23 profile_id: Arc<str>,
24 configure_tools: NavigableEntry,
10}25}
11 26
12pub struct ManageProfilesModal {27pub 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
53impl Focusable for ManageProfilesModal {121impl 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
61impl EventEmitter<DismissEvent> for ManageProfilesModal {}131impl EventEmitter<DismissEvent> for ManageProfilesModal {}
62 132
133impl 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
63impl Render for ManageProfilesModal {181impl 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}
crates/assistant2/src/assistant_configuration/profile_picker.rs+16−3
@@ -42,7 +42,6 @@ impl Render for ProfilePicker { · 41 unmodified lines
42 42
43#[derive(Debug)]43#[derive(Debug)]
44pub struct ProfileEntry {44pub 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
57impl ProfilePickerDelegate {57impl 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 = settings64 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>>) {
crates/assistant2/src/assistant_configuration/tool_picker.rs+267−0
@@ -0,0 +1,267 @@
1use std::sync::Arc;
2
3use assistant_settings::{
4 AgentProfile, AssistantSettings, AssistantSettingsContent, VersionedAssistantSettingsContent,
5};
6use assistant_tool::{ToolSource, ToolWorkingSet};
7use fs::Fs;
8use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
9use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
10use picker::{Picker, PickerDelegate};
11use settings::update_settings_file;
12use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
13use util::ResultExt as _;
14
15pub struct ToolPicker {
16 picker: Entity<Picker<ToolPickerDelegate>>,
17}
18
19impl 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
26impl EventEmitter<DismissEvent> for ToolPicker {}
27
28impl Focusable for ToolPicker {
29 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
30 self.picker.focus_handle(cx)
31 }
32}
33
34impl 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)]
41pub struct ToolEntry {
42 pub name: Arc<str>,
43 pub source: ToolSource,
44}
45
46pub 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
56impl 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
85impl 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}
crates/assistant2/src/profile_selector.rs+4−85
@@ -1,19 +1,14 @@
1use std::sync::{Arc, LazyLock};1use std::sync::Arc;
2 2
3use anyhow::Result;
4use assistant_settings::{AgentProfile, AssistantSettings};3use assistant_settings::{AgentProfile, AssistantSettings};
5use editor::scroll::Autoscroll;
6use editor::Editor;
7use fs::Fs;4use fs::Fs;
8use gpui::{prelude::*, AsyncWindowContext, Entity, Subscription, WeakEntity};5use gpui::{prelude::*, Action, Entity, Subscription, WeakEntity};
9use indexmap::IndexMap;6use indexmap::IndexMap;
10use regex::Regex;
11use settings::{update_settings_file, Settings as _, SettingsStore};7use settings::{update_settings_file, Settings as _, SettingsStore};
12use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip};8use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip};
13use util::ResultExt as _;9use util::ResultExt as _;
14use workspace::{create_and_open_local_file, Workspace};
15 10
16use crate::ThreadStore;11use crate::{ManageProfiles, ThreadStore};
17 12
18pub struct ProfileSelector {13pub 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 menu94 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
180impl Render for ProfileSelector {99impl Render for ProfileSelector {
crates/assistant_settings/src/agent_profile.rs+1−1
@@ -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)]
16pub struct ContextServerPreset {16pub struct ContextServerPreset {
17 pub tools: IndexMap<Arc<str>, bool>,17 pub tools: IndexMap<Arc<str>, bool>,
18}18}
crates/assistant_settings/src/assistant_settings.rs+1−1
@@ -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)]
446pub struct ContextServerPresetContent {446pub struct ContextServerPresetContent {
447 pub tools: IndexMap<Arc<str>, bool>,447 pub tools: IndexMap<Arc<str>, bool>,
448}448}
crates/ui/src/components/navigable.rs+1−0
@@ -2,6 +2,7 @@ use crate::prelude::*; · 1 unmodified line
2use gpui::{AnyElement, FocusHandle, ScrollAnchor, ScrollHandle};2use 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 use4/// An element that can be navigated through via keyboard. Intended for use with scrollable views that want to use
5#[derive(IntoElement)]
5pub struct Navigable {6pub struct Navigable {
6 child: AnyElement,7 child: AnyElement,
7 selectable_children: Vec<NavigableEntry>,8 selectable_children: Vec<NavigableEntry>,