diff --git a/Cargo.lock b/Cargo.lock index 76be4214b0..7d1ddb4511 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,6 +491,7 @@ dependencies = [ "prompt_store", "proto", "rand 0.8.5", + "regex", "release_channel", "rope", "serde", diff --git a/assets/settings/default.json b/assets/settings/default.json index 3d2711c295..9eef75d0e6 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -622,6 +622,7 @@ // The model to use. "model": "claude-3-5-sonnet-latest" }, + "default_profile": "code-writer", "profiles": { "read-only": { "name": "Read-only", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index d198bd19ae..031e4270bd 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -62,6 +62,7 @@ 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.rs b/crates/assistant2/src/assistant.rs index 6e97bd9d8e..462ca12c33 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -11,12 +11,12 @@ mod history_store; mod inline_assistant; mod inline_prompt_editor; mod message_editor; +mod profile_selector; mod terminal_codegen; mod terminal_inline_assistant; mod thread; mod thread_history; mod thread_store; -mod tool_selector; mod tool_use; mod ui; diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index a46024134a..40e513a506 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -26,9 +26,9 @@ use crate::assistant_model_selector::AssistantModelSelector; use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider}; use crate::context_store::{refresh_context_store_text, ContextStore}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; +use crate::profile_selector::ProfileSelector; use crate::thread::{RequestKind, Thread}; use crate::thread_store::ThreadStore; -use crate::tool_selector::ToolSelector; use crate::{Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker}; pub struct MessageEditor { @@ -43,7 +43,7 @@ pub struct MessageEditor { inline_context_picker: Entity, inline_context_picker_menu_handle: PopoverMenuHandle, model_selector: Entity, - tool_selector: Entity, + profile_selector: Entity, _subscriptions: Vec, } @@ -57,7 +57,6 @@ impl MessageEditor { window: &mut Window, cx: &mut Context, ) -> Self { - let tools = thread.read(cx).tools().clone(); let context_picker_menu_handle = PopoverMenuHandle::default(); let inline_context_picker_menu_handle = PopoverMenuHandle::default(); let model_selector_menu_handle = PopoverMenuHandle::default(); @@ -129,14 +128,14 @@ impl MessageEditor { inline_context_picker_menu_handle, model_selector: cx.new(|cx| { AssistantModelSelector::new( - fs, + fs.clone(), model_selector_menu_handle, editor.focus_handle(cx), window, cx, ) }), - tool_selector: cx.new(|cx| ToolSelector::new(tools, cx)), + profile_selector: cx.new(|cx| ProfileSelector::new(fs, thread_store, cx)), _subscriptions: subscriptions, } } @@ -624,7 +623,7 @@ impl Render for MessageEditor { .child( h_flex() .justify_between() - .child(h_flex().gap_2().child(self.tool_selector.clone())) + .child(h_flex().gap_2().child(self.profile_selector.clone())) .child( h_flex().gap_1().child(self.model_selector.clone()).child( ButtonLike::new("submit-message") diff --git a/crates/assistant2/src/profile_selector.rs b/crates/assistant2/src/profile_selector.rs new file mode 100644 index 0000000000..b9b2c45773 --- /dev/null +++ b/crates/assistant2/src/profile_selector.rs @@ -0,0 +1,202 @@ +use std::sync::{Arc, LazyLock}; + +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 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; + +pub struct ProfileSelector { + profiles: IndexMap, AgentProfile>, + fs: Arc, + thread_store: WeakEntity, + _subscriptions: Vec, +} + +impl ProfileSelector { + pub fn new( + fs: Arc, + thread_store: WeakEntity, + cx: &mut Context, + ) -> Self { + let settings_subscription = cx.observe_global::(move |this, cx| { + this.refresh_profiles(cx); + }); + + let mut this = Self { + profiles: IndexMap::default(), + fs, + thread_store, + _subscriptions: vec![settings_subscription], + }; + this.refresh_profiles(cx); + + this + } + + fn refresh_profiles(&mut self, cx: &mut Context) { + let settings = AssistantSettings::get_global(cx); + + self.profiles = settings.profiles.clone(); + } + + fn build_context_menu( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + ContextMenu::build(window, cx, |mut menu, _window, cx| { + let settings = AssistantSettings::get_global(cx); + let icon_position = IconPosition::Start; + + menu = menu.header("Profiles"); + for (profile_id, profile) in self.profiles.clone() { + menu = menu.toggleable_entry( + profile.name.clone(), + profile_id == settings.default_profile, + icon_position, + None, + { + let fs = self.fs.clone(); + let thread_store = self.thread_store.clone(); + move |_window, cx| { + update_settings_file::(fs.clone(), cx, { + let profile_id = profile_id.clone(); + move |settings, _cx| { + settings.set_profile(profile_id.clone()); + } + }); + + thread_store + .update(cx, |this, cx| { + this.load_default_profile(cx); + }) + .log_err(); + } + }, + ); + } + + menu = menu.separator(); + menu = menu.item( + ContextMenuEntry::new("Configure Profiles") + .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); + } + }), + ); + + 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 { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let settings = AssistantSettings::get_global(cx); + let profile = settings + .profiles + .get(&settings.default_profile) + .map(|profile| profile.name.clone()) + .unwrap_or_else(|| "Unknown".into()); + + let this = cx.entity().clone(); + PopoverMenu::new("tool-selector") + .menu(move |window, cx| { + Some(this.update(cx, |this, cx| this.build_context_menu(window, cx))) + }) + .trigger_with_tooltip( + Button::new("profile-selector-button", profile) + .style(ButtonStyle::Filled) + .label_size(LabelSize::Small), + Tooltip::text("Change Profile"), + ) + .anchor(gpui::Corner::BottomLeft) + } +} diff --git a/crates/assistant2/src/thread_store.rs b/crates/assistant2/src/thread_store.rs index 7c54eef658..b2ace1a2ac 100644 --- a/crates/assistant2/src/thread_store.rs +++ b/crates/assistant2/src/thread_store.rs @@ -3,7 +3,8 @@ use std::path::PathBuf; use std::sync::Arc; use anyhow::{anyhow, Result}; -use assistant_tool::{ToolId, ToolWorkingSet}; +use assistant_settings::AssistantSettings; +use assistant_tool::{ToolId, ToolSource, ToolWorkingSet}; use chrono::{DateTime, Utc}; use collections::HashMap; use context_server::manager::ContextServerManager; @@ -19,6 +20,7 @@ use language_model::{LanguageModelToolUseId, Role}; use project::Project; use prompt_store::PromptBuilder; use serde::{Deserialize, Serialize}; +use settings::Settings as _; use util::ResultExt as _; use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadEvent, ThreadId}; @@ -57,6 +59,7 @@ impl ThreadStore { context_server_tool_ids: HashMap::default(), threads: Vec::new(), }; + this.load_default_profile(cx); this.register_context_server_handlers(cx); this.reload(cx).detach_and_log_err(cx); @@ -184,6 +187,38 @@ impl ThreadStore { }) } + pub fn load_default_profile(&self, cx: &mut Context) { + let assistant_settings = AssistantSettings::get_global(cx); + + if let Some(profile) = assistant_settings + .profiles + .get(&assistant_settings.default_profile) + { + self.tools.disable_source(ToolSource::Native, cx); + self.tools.enable( + ToolSource::Native, + &profile + .tools + .iter() + .filter_map(|(tool, enabled)| enabled.then(|| tool.clone())) + .collect::>(), + ); + + for (context_server_id, preset) in &profile.context_servers { + self.tools.enable( + ToolSource::ContextServer { + id: context_server_id.clone().into(), + }, + &preset + .tools + .iter() + .filter_map(|(tool, enabled)| enabled.then(|| tool.clone())) + .collect::>(), + ) + } + } + } + fn register_context_server_handlers(&self, cx: &mut Context) { cx.subscribe( &self.context_server_manager.clone(), diff --git a/crates/assistant2/src/tool_selector.rs b/crates/assistant2/src/tool_selector.rs deleted file mode 100644 index ffe4f533cf..0000000000 --- a/crates/assistant2/src/tool_selector.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::sync::Arc; - -use assistant_settings::{AgentProfile, AssistantSettings}; -use assistant_tool::{ToolSource, ToolWorkingSet}; -use gpui::{Entity, Subscription}; -use indexmap::IndexMap; -use settings::{Settings as _, SettingsStore}; -use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip}; - -pub struct ToolSelector { - profiles: IndexMap, AgentProfile>, - tools: Arc, - _subscriptions: Vec, -} - -impl ToolSelector { - pub fn new(tools: Arc, cx: &mut Context) -> Self { - let settings_subscription = cx.observe_global::(move |this, cx| { - this.refresh_profiles(cx); - }); - - let mut this = Self { - profiles: IndexMap::default(), - tools, - _subscriptions: vec![settings_subscription], - }; - this.refresh_profiles(cx); - - this - } - - fn refresh_profiles(&mut self, cx: &mut Context) { - let settings = AssistantSettings::get_global(cx); - - self.profiles = settings.profiles.clone(); - } - - fn build_context_menu( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let profiles = self.profiles.clone(); - let tool_set = self.tools.clone(); - ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| { - let icon_position = IconPosition::End; - - menu = menu.header("Profiles"); - for (_id, profile) in profiles.clone() { - menu = menu.toggleable_entry(profile.name.clone(), false, icon_position, None, { - let tools = tool_set.clone(); - move |_window, cx| { - tools.disable_all_tools(cx); - - tools.enable( - ToolSource::Native, - &profile - .tools - .iter() - .filter_map(|(tool, enabled)| enabled.then(|| tool.clone())) - .collect::>(), - ); - - for (context_server_id, preset) in &profile.context_servers { - tools.enable( - ToolSource::ContextServer { - id: context_server_id.clone().into(), - }, - &preset - .tools - .iter() - .filter_map(|(tool, enabled)| enabled.then(|| tool.clone())) - .collect::>(), - ) - } - } - }); - } - - menu = menu.separator(); - - let tools_by_source = tool_set.tools_by_source(cx); - - let all_tools_enabled = tool_set.are_all_tools_enabled(); - menu = menu.toggleable_entry("All Tools", all_tools_enabled, icon_position, None, { - let tools = tool_set.clone(); - move |_window, cx| { - if all_tools_enabled { - tools.disable_all_tools(cx); - } else { - tools.enable_all_tools(); - } - } - }); - - for (source, tools) in tools_by_source { - let mut tools = tools - .into_iter() - .map(|tool| { - let source = tool.source(); - let name = tool.name().into(); - let is_enabled = tool_set.is_enabled(&source, &name); - - (source, name, is_enabled) - }) - .collect::>(); - - if ToolSource::Native == source { - tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b)); - } - - menu = match &source { - ToolSource::Native => menu.separator().header("Zed Tools"), - ToolSource::ContextServer { id } => { - let all_tools_from_source_enabled = - tool_set.are_all_tools_from_source_enabled(&source); - - menu.separator().header(id).toggleable_entry( - "All Tools", - all_tools_from_source_enabled, - icon_position, - None, - { - let tools = tool_set.clone(); - let source = source.clone(); - move |_window, cx| { - if all_tools_from_source_enabled { - tools.disable_source(source.clone(), cx); - } else { - tools.enable_source(&source); - } - } - }, - ) - } - }; - - for (source, name, is_enabled) in tools { - menu = menu.toggleable_entry(name.clone(), is_enabled, icon_position, None, { - let tools = tool_set.clone(); - move |_window, _cx| { - if is_enabled { - tools.disable(source.clone(), &[name.clone()]); - } else { - tools.enable(source.clone(), &[name.clone()]); - } - } - }); - } - } - - menu - }) - } -} - -impl Render for ToolSelector { - fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement { - let this = cx.entity().clone(); - PopoverMenu::new("tool-selector") - .menu(move |window, cx| { - Some(this.update(cx, |this, cx| this.build_context_menu(window, cx))) - }) - .trigger_with_tooltip( - IconButton::new("tool-selector-button", IconName::SettingsAlt) - .icon_size(IconSize::Small) - .icon_color(Color::Muted), - Tooltip::text("Customize Tools"), - ) - .anchor(gpui::Corner::BottomLeft) - } -} diff --git a/crates/assistant_settings/src/assistant_settings.rs b/crates/assistant_settings/src/assistant_settings.rs index c937e75fe5..fd8cc67c04 100644 --- a/crates/assistant_settings/src/assistant_settings.rs +++ b/crates/assistant_settings/src/assistant_settings.rs @@ -71,6 +71,7 @@ pub struct AssistantSettings { pub inline_alternatives: Vec, pub using_outdated_settings_version: bool, pub enable_experimental_live_diffs: bool, + pub default_profile: Arc, pub profiles: IndexMap, AgentProfile>, pub always_allow_tool_actions: bool, pub notify_when_agent_waiting: bool, @@ -174,6 +175,7 @@ impl AssistantSettingsContent { editor_model: None, inline_alternatives: None, enable_experimental_live_diffs: None, + default_profile: None, profiles: None, always_allow_tool_actions: None, notify_when_agent_waiting: None, @@ -198,6 +200,7 @@ impl AssistantSettingsContent { editor_model: None, inline_alternatives: None, enable_experimental_live_diffs: None, + default_profile: None, profiles: None, always_allow_tool_actions: None, notify_when_agent_waiting: None, @@ -307,6 +310,18 @@ impl AssistantSettingsContent { } } } + + pub fn set_profile(&mut self, profile_id: Arc) { + match self { + AssistantSettingsContent::Versioned(settings) => match settings { + VersionedAssistantSettingsContent::V2(settings) => { + settings.default_profile = Some(profile_id); + } + VersionedAssistantSettingsContent::V1(_) => {} + }, + AssistantSettingsContent::Legacy(_) => {} + } + } } #[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)] @@ -330,6 +345,7 @@ impl Default for VersionedAssistantSettingsContent { editor_model: None, inline_alternatives: None, enable_experimental_live_diffs: None, + default_profile: None, profiles: None, always_allow_tool_actions: None, notify_when_agent_waiting: None, @@ -370,7 +386,9 @@ pub struct AssistantSettingsContentV2 { /// Default: false enable_experimental_live_diffs: Option, #[schemars(skip)] - profiles: Option, AgentProfileContent>>, + default_profile: Option>, + #[schemars(skip)] + pub profiles: Option, AgentProfileContent>>, /// Whenever a tool action would normally wait for your confirmation /// that you allow it, always choose to allow it. /// @@ -531,6 +549,7 @@ impl Settings for AssistantSettings { &mut settings.notify_when_agent_waiting, value.notify_when_agent_waiting, ); + merge(&mut settings.default_profile, value.default_profile); if let Some(profiles) = value.profiles { settings @@ -621,6 +640,7 @@ mod tests { default_width: None, default_height: None, enable_experimental_live_diffs: None, + default_profile: None, profiles: None, always_allow_tool_actions: None, notify_when_agent_waiting: None,