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
491491 "prompt_store",
492492 "proto",
493493 "rand 0.8.5",
494− "regex",
495494 "release_channel",
496495 "rope",
497496 "serde",
crates/assistant2/Cargo.toml+0−1
@@ -62,7 +62,6 @@ prompt_library.workspace = true · 61 unmodified lines
6262 prompt_store.workspace = true
6363 proto.workspace = true
6464 release_channel.workspace = true
65−regex.workspace = true
6665 rope.workspace = true
6766 serde.workspace = true
6867 serde_json.workspace = true
crates/assistant2/src/assistant_configuration.rs+1−0
@@ -1,6 +1,7 @@
11 mod add_context_server_modal;
22 mod manage_profiles_modal;
33 mod profile_picker;
4+mod tool_picker;
45
56 use std::sync::Arc;
67
crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs+136−14
@@ -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};
39 use workspace::{ModalView, Workspace};
410
511 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};
714
815 enum Mode {
916 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,
1025 }
1126
1227 pub struct ManageProfilesModal {
13− #[allow(dead_code)]
14− workspace: WeakEntity<Workspace>,
28+ fs: Arc<dyn Fs>,
29+ tools: Arc<ToolWorkingSet>,
30+ focus_handle: FocusHandle,
1531 mode: Mode,
1632 }
1733
@@ -22,27 +38,79 @@ impl ManageProfilesModal { · 4 unmodified lines
2238 _cx: &mut Context<Workspace>,
2339 ) {
2440 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+ }
2947 });
3048 }
3149
3250 pub fn new(
33− workspace: WeakEntity<Workspace>,
51+ fs: Arc<dyn Fs>,
52+ tools: Arc<ToolWorkingSet>,
3453 window: &mut Window,
3554 cx: &mut Context<Self>,
3655 ) -> Self {
56+ let focus_handle = cx.focus_handle();
57+ let handle = cx.entity();
58+
3759 Self {
38− workspace,
60+ fs,
61+ tools,
62+ focus_handle,
3963 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+ );
4172 ProfilePicker::new(delegate, window, cx)
4273 })),
4374 }
4475 }
4576
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+
46114 fn confirm(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
47115
48116 fn cancel(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
@@ -53,15 +121,65 @@ impl ModalView for ManageProfilesModal {} · 4 unmodified lines
53121 impl Focusable for ManageProfilesModal {
54122 fn focus_handle(&self, cx: &App) -> FocusHandle {
55123 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(),
57127 }
58128 }
59129 }
60130
61131 impl EventEmitter<DismissEvent> for ManageProfilesModal {}
62132
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+
63181 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 {
65183 div()
66184 .elevation_3(cx)
67185 .w(rems(34.))
@@ -74,6 +192,10 @@ impl Render for ManageProfilesModal { · 6 unmodified lines
74192 .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
75193 .child(match &self.mode {
76194 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(),
77199 })
78200 }
79201 }
crates/assistant2/src/assistant_configuration/profile_picker.rs+16−3
@@ -42,7 +42,6 @@ impl Render for ProfilePicker { · 41 unmodified lines
4242
4343 #[derive(Debug)]
4444 pub struct ProfileEntry {
45− #[allow(dead_code)]
4645 pub id: Arc<str>,
4746 pub name: SharedString,
4847 }
@@ -52,10 +51,14 @@ pub struct ProfilePickerDelegate { · 3 unmodified lines
5251 profiles: Vec<ProfileEntry>,
5352 matches: Vec<StringMatch>,
5453 selected_index: usize,
54+ on_confirm: Arc<dyn Fn(&Arc<str>, &mut Window, &mut App) + 'static>,
5555 }
5656
5757 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 {
5962 let settings = AssistantSettings::get_global(cx);
6063
6164 let profiles = settings
@@ -72,6 +75,7 @@ impl ProfilePickerDelegate { · 10 unmodified lines
7275 profiles,
7376 matches: Vec::new(),
7477 selected_index: 0,
78+ on_confirm: Arc::new(on_confirm),
7579 }
7680 }
7781 }
@@ -149,7 +153,16 @@ impl PickerDelegate for ProfilePickerDelegate { · 71 unmodified lines
149153 })
150154 }
151155
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);
153166 }
154167
155168 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 @@
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+}
crates/assistant2/src/profile_selector.rs+4−85
@@ -1,19 +1,14 @@
1−use std::sync::{Arc, LazyLock};
1+use std::sync::Arc;
22
3−use anyhow::Result;
43 use assistant_settings::{AgentProfile, AssistantSettings};
5−use editor::scroll::Autoscroll;
6−use editor::Editor;
74 use fs::Fs;
8−use gpui::{prelude::*, AsyncWindowContext, Entity, Subscription, WeakEntity};
5+use gpui::{prelude::*, Action, Entity, Subscription, WeakEntity};
96 use indexmap::IndexMap;
10−use regex::Regex;
117 use settings::{update_settings_file, Settings as _, SettingsStore};
128 use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip};
139 use util::ResultExt as _;
14−use workspace::{create_and_open_local_file, Workspace};
1510
16−use crate::ThreadStore;
11+use crate::{ManageProfiles, ThreadStore};
1712
1813 pub struct ProfileSelector {
1914 profiles: IndexMap<Arc<str>, AgentProfile>,
@@ -92,89 +87,13 @@ impl ProfileSelector { · 72 unmodified lines
9287 .icon(IconName::Pencil)
9388 .icon_color(Color::Muted)
9489 .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);
10391 }),
10492 );
10593
10694 menu
10795 })
10896 }
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− }
17897 }
17998
18099 impl Render for ProfileSelector {
crates/assistant_settings/src/agent_profile.rs+1−1
@@ -12,7 +12,7 @@ pub struct AgentProfile { · 11 unmodified lines
1212 pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
1313 }
1414
15−#[derive(Debug, Clone)]
15+#[derive(Debug, Clone, Default)]
1616 pub struct ContextServerPreset {
1717 pub tools: IndexMap<Arc<str>, bool>,
1818 }
crates/assistant_settings/src/assistant_settings.rs+1−1
@@ -442,7 +442,7 @@ pub struct AgentProfileContent { · 441 unmodified lines
442442 pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
443443 }
444444
445−#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
445+#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
446446 pub struct ContextServerPresetContent {
447447 pub tools: IndexMap<Arc<str>, bool>,
448448 }
crates/ui/src/components/navigable.rs+1−0
@@ -2,6 +2,7 @@ use crate::prelude::*; · 1 unmodified line
22 use gpui::{AnyElement, FocusHandle, ScrollAnchor, ScrollHandle};
33
44 /// An element that can be navigated through via keyboard. Intended for use with scrollable views that want to use
5+#[derive(IntoElement)]
56 pub struct Navigable {
67 child: AnyElement,
78 selectable_children: Vec<NavigableEntry>,