Cargo.lock 🔗
@@ -491,6 +491,7 @@ dependencies = [
"prompt_store",
"proto",
"rand 0.8.5",
+ "regex",
"release_channel",
"rope",
"serde",
Marshall Bowers created
This PR replaces the tool selector with a new profile selector.
<img width="1394" alt="Screenshot 2025-03-26 at 2 35 42 PM"
src="https://github.com/user-attachments/assets/9631c6e9-9c47-411e-b9fc-5d61ed9ca1fe"
/>
<img width="1394" alt="Screenshot 2025-03-26 at 2 35 50 PM"
src="https://github.com/user-attachments/assets/3abe4e08-d044-4d3f-aa95-f472938452a8"
/>
Release Notes:
- N/A
Cargo.lock | 1
assets/settings/default.json | 1
crates/assistant2/Cargo.toml | 1
crates/assistant2/src/assistant.rs | 2
crates/assistant2/src/message_editor.rs | 11
crates/assistant2/src/profile_selector.rs | 202 +++++++++++++++
crates/assistant2/src/thread_store.rs | 37 ++
crates/assistant2/src/tool_selector.rs | 172 ------------
crates/assistant_settings/src/assistant_settings.rs | 22 +
9 files changed, 268 insertions(+), 181 deletions(-)
@@ -491,6 +491,7 @@ dependencies = [
"prompt_store",
"proto",
"rand 0.8.5",
+ "regex",
"release_channel",
"rope",
"serde",
@@ -622,6 +622,7 @@
// The model to use.
"model": "claude-3-5-sonnet-latest"
},
+ "default_profile": "code-writer",
"profiles": {
"read-only": {
"name": "Read-only",
@@ -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
@@ -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;
@@ -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<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
- tool_selector: Entity<ToolSelector>,
+ profile_selector: Entity<ProfileSelector>,
_subscriptions: Vec<Subscription>,
}
@@ -57,7 +57,6 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context<Self>,
) -> 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")
@@ -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<Arc<str>, AgentProfile>,
+ fs: Arc<dyn Fs>,
+ thread_store: WeakEntity<ThreadStore>,
+ _subscriptions: Vec<Subscription>,
+}
+
+impl ProfileSelector {
+ pub fn new(
+ fs: Arc<dyn Fs>,
+ thread_store: WeakEntity<ThreadStore>,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let settings_subscription = cx.observe_global::<SettingsStore>(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<Self>) {
+ let settings = AssistantSettings::get_global(cx);
+
+ self.profiles = settings.profiles.clone();
+ }
+
+ fn build_context_menu(
+ &self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Entity<ContextMenu> {
+ 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::<AssistantSettings>(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<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 {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> 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)
+ }
+}
@@ -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<Self>) {
+ 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::<Vec<_>>(),
+ );
+
+ 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::<Vec<_>>(),
+ )
+ }
+ }
+ }
+
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
cx.subscribe(
&self.context_server_manager.clone(),
@@ -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<Arc<str>, AgentProfile>,
- tools: Arc<ToolWorkingSet>,
- _subscriptions: Vec<Subscription>,
-}
-
-impl ToolSelector {
- pub fn new(tools: Arc<ToolWorkingSet>, cx: &mut Context<Self>) -> Self {
- let settings_subscription = cx.observe_global::<SettingsStore>(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<Self>) {
- let settings = AssistantSettings::get_global(cx);
-
- self.profiles = settings.profiles.clone();
- }
-
- fn build_context_menu(
- &self,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Entity<ContextMenu> {
- 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::<Vec<_>>(),
- );
-
- 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::<Vec<_>>(),
- )
- }
- }
- });
- }
-
- 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::<Vec<_>>();
-
- 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)
- }
-}
@@ -71,6 +71,7 @@ pub struct AssistantSettings {
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool,
+ pub default_profile: Arc<str>,
pub profiles: IndexMap<Arc<str>, 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<str>) {
+ 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<bool>,
#[schemars(skip)]
- profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
+ default_profile: Option<Arc<str>>,
+ #[schemars(skip)]
+ pub profiles: Option<IndexMap<Arc<str>, 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,