Cargo.lock π
@@ -491,7 +491,6 @@ dependencies = [
"prompt_store",
"proto",
"rand 0.8.5",
- "regex",
"release_channel",
"rope",
"serde",
Marshall Bowers created
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
Cargo.lock | 1
crates/assistant2/Cargo.toml | 1
crates/assistant2/src/assistant_configuration.rs | 1
crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs | 150
crates/assistant2/src/assistant_configuration/profile_picker.rs | 19
crates/assistant2/src/assistant_configuration/tool_picker.rs | 267
crates/assistant2/src/profile_selector.rs | 89
crates/assistant_settings/src/agent_profile.rs | 2
crates/assistant_settings/src/assistant_settings.rs | 2
crates/ui/src/components/navigable.rs | 1
10 files changed, 427 insertions(+), 106 deletions(-)
@@ -491,7 +491,6 @@ dependencies = [
"prompt_store",
"proto",
"rand 0.8.5",
- "regex",
"release_channel",
"rope",
"serde",
@@ -62,7 +62,6 @@ prompt_library.workspace = true
prompt_store.workspace = true
proto.workspace = true
release_channel.workspace = true
-regex.workspace = true
rope.workspace = true
serde.workspace = true
serde_json.workspace = true
@@ -1,6 +1,7 @@
mod add_context_server_modal;
mod manage_profiles_modal;
mod profile_picker;
+mod tool_picker;
use std::sync::Arc;
@@ -1,17 +1,33 @@
-use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity};
-use ui::prelude::*;
+use std::sync::Arc;
+
+use assistant_settings::AssistantSettings;
+use assistant_tool::ToolWorkingSet;
+use fs::Fs;
+use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable};
+use settings::Settings as _;
+use ui::{prelude::*, ListItem, ListItemSpacing, Navigable, NavigableEntry};
use workspace::{ModalView, Workspace};
use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};
-use crate::ManageProfiles;
+use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
+use crate::{AssistantPanel, ManageProfiles};
enum Mode {
ChooseProfile(Entity<ProfilePicker>),
+ ViewProfile(ViewProfileMode),
+ ConfigureTools(Entity<ToolPicker>),
+}
+
+#[derive(Clone)]
+pub struct ViewProfileMode {
+ profile_id: Arc<str>,
+ configure_tools: NavigableEntry,
}
pub struct ManageProfilesModal {
- #[allow(dead_code)]
- workspace: WeakEntity<Workspace>,
+ fs: Arc<dyn Fs>,
+ tools: Arc<ToolWorkingSet>,
+ focus_handle: FocusHandle,
mode: Mode,
}
@@ -22,27 +38,79 @@ impl ManageProfilesModal {
_cx: &mut Context<Workspace>,
) {
workspace.register_action(|workspace, _: &ManageProfiles, window, cx| {
- let workspace_handle = cx.entity().downgrade();
- workspace.toggle_modal(window, cx, |window, cx| {
- Self::new(workspace_handle, window, cx)
- })
+ if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
+ let fs = workspace.app_state().fs.clone();
+ let thread_store = panel.read(cx).thread_store().read(cx);
+ let tools = thread_store.tools();
+ workspace.toggle_modal(window, cx, |window, cx| Self::new(fs, tools, window, cx))
+ }
});
}
pub fn new(
- workspace: WeakEntity<Workspace>,
+ fs: Arc<dyn Fs>,
+ tools: Arc<ToolWorkingSet>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
+ let focus_handle = cx.focus_handle();
+ let handle = cx.entity();
+
Self {
- workspace,
+ fs,
+ tools,
+ focus_handle,
mode: Mode::ChooseProfile(cx.new(|cx| {
- let delegate = ProfilePickerDelegate::new(cx);
+ let delegate = ProfilePickerDelegate::new(
+ move |profile_id, window, cx| {
+ handle.update(cx, |this, cx| {
+ this.view_profile(profile_id.clone(), window, cx);
+ })
+ },
+ cx,
+ );
ProfilePicker::new(delegate, window, cx)
})),
}
}
+ pub fn view_profile(
+ &mut self,
+ profile_id: Arc<str>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.mode = Mode::ViewProfile(ViewProfileMode {
+ profile_id,
+ configure_tools: NavigableEntry::focusable(cx),
+ });
+ self.focus_handle(cx).focus(window);
+ }
+
+ fn configure_tools(
+ &mut self,
+ profile_id: Arc<str>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let settings = AssistantSettings::get_global(cx);
+ let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
+ return;
+ };
+
+ self.mode = Mode::ConfigureTools(cx.new(|cx| {
+ let delegate = ToolPickerDelegate::new(
+ self.fs.clone(),
+ self.tools.clone(),
+ profile_id,
+ profile,
+ cx,
+ );
+ ToolPicker::new(delegate, window, cx)
+ }));
+ self.focus_handle(cx).focus(window);
+ }
+
fn confirm(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
fn cancel(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
@@ -53,15 +121,65 @@ impl ModalView for ManageProfilesModal {}
impl Focusable for ManageProfilesModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
- Mode::ChooseProfile(profile_picker) => profile_picker.read(cx).focus_handle(cx),
+ Mode::ChooseProfile(profile_picker) => profile_picker.focus_handle(cx),
+ Mode::ConfigureTools(tool_picker) => tool_picker.focus_handle(cx),
+ Mode::ViewProfile(_) => self.focus_handle.clone(),
}
}
}
impl EventEmitter<DismissEvent> for ManageProfilesModal {}
+impl ManageProfilesModal {
+ fn render_view_profile(
+ &mut self,
+ mode: ViewProfileMode,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> impl IntoElement {
+ Navigable::new(
+ div()
+ .track_focus(&self.focus_handle(cx))
+ .size_full()
+ .child(
+ v_flex().child(
+ div()
+ .id("configure-tools")
+ .track_focus(&mode.configure_tools.focus_handle)
+ .on_action({
+ let profile_id = mode.profile_id.clone();
+ cx.listener(move |this, _: &menu::Confirm, window, cx| {
+ this.configure_tools(profile_id.clone(), window, cx);
+ })
+ })
+ .child(
+ ListItem::new("configure-tools")
+ .toggle_state(
+ mode.configure_tools
+ .focus_handle
+ .contains_focused(window, cx),
+ )
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .start_slot(Icon::new(IconName::Cog))
+ .child(Label::new("Configure Tools"))
+ .on_click({
+ let profile_id = mode.profile_id.clone();
+ cx.listener(move |this, _, window, cx| {
+ this.configure_tools(profile_id.clone(), window, cx);
+ })
+ }),
+ ),
+ ),
+ )
+ .into_any_element(),
+ )
+ .entry(mode.configure_tools)
+ }
+}
+
impl Render for ManageProfilesModal {
- fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.elevation_3(cx)
.w(rems(34.))
@@ -74,6 +192,10 @@ impl Render for ManageProfilesModal {
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(match &self.mode {
Mode::ChooseProfile(profile_picker) => profile_picker.clone().into_any_element(),
+ Mode::ViewProfile(mode) => self
+ .render_view_profile(mode.clone(), window, cx)
+ .into_any_element(),
+ Mode::ConfigureTools(tool_picker) => tool_picker.clone().into_any_element(),
})
}
}
@@ -42,7 +42,6 @@ impl Render for ProfilePicker {
#[derive(Debug)]
pub struct ProfileEntry {
- #[allow(dead_code)]
pub id: Arc<str>,
pub name: SharedString,
}
@@ -52,10 +51,14 @@ pub struct ProfilePickerDelegate {
profiles: Vec<ProfileEntry>,
matches: Vec<StringMatch>,
selected_index: usize,
+ on_confirm: Arc<dyn Fn(&Arc<str>, &mut Window, &mut App) + 'static>,
}
impl ProfilePickerDelegate {
- pub fn new(cx: &mut Context<ProfilePicker>) -> Self {
+ pub fn new(
+ on_confirm: impl Fn(&Arc<str>, &mut Window, &mut App) + 'static,
+ cx: &mut Context<ProfilePicker>,
+ ) -> Self {
let settings = AssistantSettings::get_global(cx);
let profiles = settings
@@ -72,6 +75,7 @@ impl ProfilePickerDelegate {
profiles,
matches: Vec::new(),
selected_index: 0,
+ on_confirm: Arc::new(on_confirm),
}
}
}
@@ -149,7 +153,16 @@ impl PickerDelegate for ProfilePickerDelegate {
})
}
- fn confirm(&mut self, _secondary: bool, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {
+ fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ if self.matches.is_empty() {
+ self.dismissed(window, cx);
+ return;
+ }
+
+ let candidate_id = self.matches[self.selected_index].candidate_id;
+ let profile = &self.profiles[candidate_id];
+
+ (self.on_confirm)(&profile.id, window, cx);
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
@@ -0,0 +1,267 @@
+use std::sync::Arc;
+
+use assistant_settings::{
+ AgentProfile, AssistantSettings, AssistantSettingsContent, VersionedAssistantSettingsContent,
+};
+use assistant_tool::{ToolSource, ToolWorkingSet};
+use fs::Fs;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
+use picker::{Picker, PickerDelegate};
+use settings::update_settings_file;
+use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
+use util::ResultExt as _;
+
+pub struct ToolPicker {
+ picker: Entity<Picker<ToolPickerDelegate>>,
+}
+
+impl ToolPicker {
+ pub fn new(delegate: ToolPickerDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self {
+ let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
+ Self { picker }
+ }
+}
+
+impl EventEmitter<DismissEvent> for ToolPicker {}
+
+impl Focusable for ToolPicker {
+ fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl Render for ToolPicker {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ v_flex().w(rems(34.)).child(self.picker.clone())
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct ToolEntry {
+ pub name: Arc<str>,
+ pub source: ToolSource,
+}
+
+pub struct ToolPickerDelegate {
+ tool_picker: WeakEntity<ToolPicker>,
+ fs: Arc<dyn Fs>,
+ tools: Vec<ToolEntry>,
+ profile_id: Arc<str>,
+ profile: AgentProfile,
+ matches: Vec<StringMatch>,
+ selected_index: usize,
+}
+
+impl ToolPickerDelegate {
+ pub fn new(
+ fs: Arc<dyn Fs>,
+ tool_set: Arc<ToolWorkingSet>,
+ profile_id: Arc<str>,
+ profile: AgentProfile,
+ cx: &mut Context<ToolPicker>,
+ ) -> Self {
+ let mut tool_entries = Vec::new();
+
+ for (source, tools) in tool_set.tools_by_source(cx) {
+ tool_entries.extend(tools.into_iter().map(|tool| ToolEntry {
+ name: tool.name().into(),
+ source: source.clone(),
+ }));
+ }
+
+ Self {
+ tool_picker: cx.entity().downgrade(),
+ fs,
+ tools: tool_entries,
+ profile_id,
+ profile,
+ matches: Vec::new(),
+ selected_index: 0,
+ }
+ }
+}
+
+impl PickerDelegate for ToolPickerDelegate {
+ type ListItem = ListItem;
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(
+ &mut self,
+ ix: usize,
+ _window: &mut Window,
+ _cx: &mut Context<Picker<Self>>,
+ ) {
+ self.selected_index = ix;
+ }
+
+ fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+ "Search toolsβ¦".into()
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ window: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Task<()> {
+ let background = cx.background_executor().clone();
+ let candidates = self
+ .tools
+ .iter()
+ .enumerate()
+ .map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
+ .collect::<Vec<_>>();
+
+ cx.spawn_in(window, async move |this, cx| {
+ let matches = if query.is_empty() {
+ candidates
+ .into_iter()
+ .enumerate()
+ .map(|(index, candidate)| StringMatch {
+ candidate_id: index,
+ string: candidate.string,
+ positions: Vec::new(),
+ score: 0.,
+ })
+ .collect()
+ } else {
+ match_strings(
+ &candidates,
+ &query,
+ false,
+ 100,
+ &Default::default(),
+ background,
+ )
+ .await
+ };
+
+ this.update(cx, |this, _cx| {
+ this.delegate.matches = matches;
+ this.delegate.selected_index = this
+ .delegate
+ .selected_index
+ .min(this.delegate.matches.len().saturating_sub(1));
+ })
+ .log_err();
+ })
+ }
+
+ fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ if self.matches.is_empty() {
+ self.dismissed(window, cx);
+ return;
+ }
+
+ let candidate_id = self.matches[self.selected_index].candidate_id;
+ let tool = &self.tools[candidate_id];
+
+ let is_enabled = match &tool.source {
+ ToolSource::Native => {
+ let is_enabled = self.profile.tools.entry(tool.name.clone()).or_default();
+ *is_enabled = !*is_enabled;
+ *is_enabled
+ }
+ ToolSource::ContextServer { id } => {
+ let preset = self
+ .profile
+ .context_servers
+ .entry(id.clone().into())
+ .or_default();
+ let is_enabled = preset.tools.entry(tool.name.clone()).or_default();
+ *is_enabled = !*is_enabled;
+ *is_enabled
+ }
+ };
+
+ update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
+ let profile_id = self.profile_id.clone();
+ let tool = tool.clone();
+ move |settings, _cx| match settings {
+ AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
+ settings,
+ )) => {
+ if let Some(profiles) = &mut settings.profiles {
+ if let Some(profile) = profiles.get_mut(&profile_id) {
+ match tool.source {
+ ToolSource::Native => {
+ *profile.tools.entry(tool.name).or_default() = is_enabled;
+ }
+ ToolSource::ContextServer { id } => {
+ let preset = profile
+ .context_servers
+ .entry(id.clone().into())
+ .or_default();
+ *preset.tools.entry(tool.name.clone()).or_default() =
+ is_enabled;
+ }
+ }
+ }
+ }
+ }
+ _ => {}
+ }
+ });
+ }
+
+ fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ self.tool_picker
+ .update(cx, |_this, cx| cx.emit(DismissEvent))
+ .log_err();
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ _window: &mut Window,
+ _cx: &mut Context<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let tool_match = &self.matches[ix];
+ let tool = &self.tools[tool_match.candidate_id];
+
+ let is_enabled = match &tool.source {
+ ToolSource::Native => self.profile.tools.get(&tool.name).copied().unwrap_or(false),
+ ToolSource::ContextServer { id } => self
+ .profile
+ .context_servers
+ .get(id.as_ref())
+ .and_then(|preset| preset.tools.get(&tool.name))
+ .copied()
+ .unwrap_or(false),
+ };
+
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .toggle_state(selected)
+ .child(
+ h_flex()
+ .gap_2()
+ .child(HighlightedLabel::new(
+ tool_match.string.clone(),
+ tool_match.positions.clone(),
+ ))
+ .map(|parent| match &tool.source {
+ ToolSource::Native => parent,
+ ToolSource::ContextServer { id } => parent
+ .child(Label::new(id).size(LabelSize::XSmall).color(Color::Muted)),
+ }),
+ )
+ .end_slot::<Icon>(is_enabled.then(|| {
+ Icon::new(IconName::Check)
+ .size(IconSize::Small)
+ .color(Color::Success)
+ })),
+ )
+ }
+}
@@ -1,19 +1,14 @@
-use std::sync::{Arc, LazyLock};
+use std::sync::Arc;
-use anyhow::Result;
use assistant_settings::{AgentProfile, AssistantSettings};
-use editor::scroll::Autoscroll;
-use editor::Editor;
use fs::Fs;
-use gpui::{prelude::*, AsyncWindowContext, Entity, Subscription, WeakEntity};
+use gpui::{prelude::*, Action, Entity, Subscription, WeakEntity};
use indexmap::IndexMap;
-use regex::Regex;
use settings::{update_settings_file, Settings as _, SettingsStore};
use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip};
use util::ResultExt as _;
-use workspace::{create_and_open_local_file, Workspace};
-use crate::ThreadStore;
+use crate::{ManageProfiles, ThreadStore};
pub struct ProfileSelector {
profiles: IndexMap<Arc<str>, AgentProfile>,
@@ -92,89 +87,13 @@ impl ProfileSelector {
.icon(IconName::Pencil)
.icon_color(Color::Muted)
.handler(move |window, cx| {
- if let Some(workspace) = window.root().flatten() {
- let workspace = workspace.downgrade();
- window
- .spawn(cx, async |cx| {
- Self::open_profiles_setting_in_editor(workspace, cx).await
- })
- .detach_and_log_err(cx);
- }
+ window.dispatch_action(ManageProfiles.boxed_clone(), cx);
}),
);
menu
})
}
-
- async fn open_profiles_setting_in_editor(
- workspace: WeakEntity<Workspace>,
- cx: &mut AsyncWindowContext,
- ) -> Result<()> {
- let settings_editor = workspace
- .update_in(cx, |_, window, cx| {
- create_and_open_local_file(paths::settings_file(), window, cx, || {
- settings::initial_user_settings_content().as_ref().into()
- })
- })?
- .await?
- .downcast::<Editor>()
- .unwrap();
-
- settings_editor
- .downgrade()
- .update_in(cx, |editor, window, cx| {
- let text = editor.buffer().read(cx).snapshot(cx).text();
-
- let settings = cx.global::<SettingsStore>();
-
- let edits =
- settings.edits_for_update::<AssistantSettings>(
- &text,
- |settings| match settings {
- assistant_settings::AssistantSettingsContent::Versioned(settings) => {
- match settings {
- assistant_settings::VersionedAssistantSettingsContent::V2(
- settings,
- ) => {
- settings.profiles.get_or_insert_with(IndexMap::default);
- }
- assistant_settings::VersionedAssistantSettingsContent::V1(
- _,
- ) => {}
- }
- }
- assistant_settings::AssistantSettingsContent::Legacy(_) => {}
- },
- );
-
- if !edits.is_empty() {
- editor.edit(edits.iter().cloned(), cx);
- }
-
- let text = editor.buffer().read(cx).snapshot(cx).text();
-
- static PROFILES_REGEX: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(r#"(?P<key>"profiles":)\s*\{"#).unwrap());
- let range = PROFILES_REGEX.captures(&text).and_then(|captures| {
- captures
- .name("key")
- .map(|inner_match| inner_match.start()..inner_match.end())
- });
- if let Some(range) = range {
- editor.change_selections(
- Some(Autoscroll::newest()),
- window,
- cx,
- |selections| {
- selections.select_ranges(vec![range]);
- },
- );
- }
- })?;
-
- anyhow::Ok(())
- }
}
impl Render for ProfileSelector {
@@ -12,7 +12,7 @@ pub struct AgentProfile {
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Default)]
pub struct ContextServerPreset {
pub tools: IndexMap<Arc<str>, bool>,
}
@@ -442,7 +442,7 @@ pub struct AgentProfileContent {
pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
}
-#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ContextServerPresetContent {
pub tools: IndexMap<Arc<str>, bool>,
}
@@ -2,6 +2,7 @@ use crate::prelude::*;
use gpui::{AnyElement, FocusHandle, ScrollAnchor, ScrollHandle};
/// An element that can be navigated through via keyboard. Intended for use with scrollable views that want to use
+#[derive(IntoElement)]
pub struct Navigable {
child: AnyElement,
selectable_children: Vec<NavigableEntry>,