diff --git a/Cargo.lock b/Cargo.lock index 7d1ddb4511..76be4214b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,7 +491,6 @@ dependencies = [ "prompt_store", "proto", "rand 0.8.5", - "regex", "release_channel", "rope", "serde", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 031e4270bd..d198bd19ae 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -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 diff --git a/crates/assistant2/src/assistant_configuration.rs b/crates/assistant2/src/assistant_configuration.rs index 4d275a181d..81036c1915 100644 --- a/crates/assistant2/src/assistant_configuration.rs +++ b/crates/assistant2/src/assistant_configuration.rs @@ -1,6 +1,7 @@ mod add_context_server_modal; mod manage_profiles_modal; mod profile_picker; +mod tool_picker; use std::sync::Arc; diff --git a/crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs b/crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs index 27681d5cfc..4058418717 100644 --- a/crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs +++ b/crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs @@ -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), + ViewProfile(ViewProfileMode), + ConfigureTools(Entity), +} + +#[derive(Clone)] +pub struct ViewProfileMode { + profile_id: Arc, + configure_tools: NavigableEntry, } pub struct ManageProfilesModal { - #[allow(dead_code)] - workspace: WeakEntity, + fs: Arc, + tools: Arc, + focus_handle: FocusHandle, mode: Mode, } @@ -22,27 +38,79 @@ impl ManageProfilesModal { _cx: &mut Context, ) { 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::(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, + fs: Arc, + tools: Arc, window: &mut Window, cx: &mut Context, ) -> 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, + window: &mut Window, + cx: &mut Context, + ) { + 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, + window: &mut Window, + cx: &mut Context, + ) { + 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) {} fn cancel(&mut self, _window: &mut Window, _cx: &mut Context) {} @@ -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 for ManageProfilesModal {} +impl ManageProfilesModal { + fn render_view_profile( + &mut self, + mode: ViewProfileMode, + window: &mut Window, + cx: &mut Context, + ) -> 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) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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(), }) } } diff --git a/crates/assistant2/src/assistant_configuration/profile_picker.rs b/crates/assistant2/src/assistant_configuration/profile_picker.rs index 3762a865b8..3b74570c3d 100644 --- a/crates/assistant2/src/assistant_configuration/profile_picker.rs +++ b/crates/assistant2/src/assistant_configuration/profile_picker.rs @@ -42,7 +42,6 @@ impl Render for ProfilePicker { #[derive(Debug)] pub struct ProfileEntry { - #[allow(dead_code)] pub id: Arc, pub name: SharedString, } @@ -52,10 +51,14 @@ pub struct ProfilePickerDelegate { profiles: Vec, matches: Vec, selected_index: usize, + on_confirm: Arc, &mut Window, &mut App) + 'static>, } impl ProfilePickerDelegate { - pub fn new(cx: &mut Context) -> Self { + pub fn new( + on_confirm: impl Fn(&Arc, &mut Window, &mut App) + 'static, + cx: &mut Context, + ) -> 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>) { + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { + 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>) { diff --git a/crates/assistant2/src/assistant_configuration/tool_picker.rs b/crates/assistant2/src/assistant_configuration/tool_picker.rs new file mode 100644 index 0000000000..347d99d722 --- /dev/null +++ b/crates/assistant2/src/assistant_configuration/tool_picker.rs @@ -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>, +} + +impl ToolPicker { + pub fn new(delegate: ToolPickerDelegate, window: &mut Window, cx: &mut Context) -> Self { + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); + Self { picker } + } +} + +impl EventEmitter 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) -> impl IntoElement { + v_flex().w(rems(34.)).child(self.picker.clone()) + } +} + +#[derive(Debug, Clone)] +pub struct ToolEntry { + pub name: Arc, + pub source: ToolSource, +} + +pub struct ToolPickerDelegate { + tool_picker: WeakEntity, + fs: Arc, + tools: Vec, + profile_id: Arc, + profile: AgentProfile, + matches: Vec, + selected_index: usize, +} + +impl ToolPickerDelegate { + pub fn new( + fs: Arc, + tool_set: Arc, + profile_id: Arc, + profile: AgentProfile, + cx: &mut Context, + ) -> 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>, + ) { + self.selected_index = ix; + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Search tools…".into() + } + + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { + let background = cx.background_executor().clone(); + let candidates = self + .tools + .iter() + .enumerate() + .map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref())) + .collect::>(); + + 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>) { + 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::(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>) { + 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>, + ) -> Option { + 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::(is_enabled.then(|| { + Icon::new(IconName::Check) + .size(IconSize::Small) + .color(Color::Success) + })), + ) + } +} diff --git a/crates/assistant2/src/profile_selector.rs b/crates/assistant2/src/profile_selector.rs index c0ba19016c..838b5b0dbe 100644 --- a/crates/assistant2/src/profile_selector.rs +++ b/crates/assistant2/src/profile_selector.rs @@ -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, 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, - 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::() - .unwrap(); - - settings_editor - .downgrade() - .update_in(cx, |editor, window, cx| { - let text = editor.buffer().read(cx).snapshot(cx).text(); - - let settings = cx.global::(); - - let edits = - settings.edits_for_update::( - &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 = - LazyLock::new(|| Regex::new(r#"(?P"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 { diff --git a/crates/assistant_settings/src/agent_profile.rs b/crates/assistant_settings/src/agent_profile.rs index 2b7c91cd9f..0e9459fcf0 100644 --- a/crates/assistant_settings/src/agent_profile.rs +++ b/crates/assistant_settings/src/agent_profile.rs @@ -12,7 +12,7 @@ pub struct AgentProfile { pub context_servers: IndexMap, ContextServerPreset>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct ContextServerPreset { pub tools: IndexMap, bool>, } diff --git a/crates/assistant_settings/src/assistant_settings.rs b/crates/assistant_settings/src/assistant_settings.rs index fd8cc67c04..fd94ba1d46 100644 --- a/crates/assistant_settings/src/assistant_settings.rs +++ b/crates/assistant_settings/src/assistant_settings.rs @@ -442,7 +442,7 @@ pub struct AgentProfileContent { pub context_servers: IndexMap, ContextServerPresetContent>, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct ContextServerPresetContent { pub tools: IndexMap, bool>, } diff --git a/crates/ui/src/components/navigable.rs b/crates/ui/src/components/navigable.rs index 52d7128b7a..dd1da2cc4a 100644 --- a/crates/ui/src/components/navigable.rs +++ b/crates/ui/src/components/navigable.rs @@ -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,