assistant2: Add profile selector (#27520)

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
This commit is contained in:
Marshall Bowers 2025-03-26 14:51:38 -04:00 committed by GitHub
parent 7e4320f587
commit cdaad2655a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 268 additions and 181 deletions

1
Cargo.lock generated
View file

@ -491,6 +491,7 @@ dependencies = [
"prompt_store", "prompt_store",
"proto", "proto",
"rand 0.8.5", "rand 0.8.5",
"regex",
"release_channel", "release_channel",
"rope", "rope",
"serde", "serde",

View file

@ -622,6 +622,7 @@
// The model to use. // The model to use.
"model": "claude-3-5-sonnet-latest" "model": "claude-3-5-sonnet-latest"
}, },
"default_profile": "code-writer",
"profiles": { "profiles": {
"read-only": { "read-only": {
"name": "Read-only", "name": "Read-only",

View file

@ -62,6 +62,7 @@ prompt_library.workspace = true
prompt_store.workspace = true prompt_store.workspace = true
proto.workspace = true proto.workspace = true
release_channel.workspace = true release_channel.workspace = true
regex.workspace = true
rope.workspace = true rope.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true

View file

@ -11,12 +11,12 @@ mod history_store;
mod inline_assistant; mod inline_assistant;
mod inline_prompt_editor; mod inline_prompt_editor;
mod message_editor; mod message_editor;
mod profile_selector;
mod terminal_codegen; mod terminal_codegen;
mod terminal_inline_assistant; mod terminal_inline_assistant;
mod thread; mod thread;
mod thread_history; mod thread_history;
mod thread_store; mod thread_store;
mod tool_selector;
mod tool_use; mod tool_use;
mod ui; mod ui;

View file

@ -26,9 +26,9 @@ use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider}; use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
use crate::context_store::{refresh_context_store_text, ContextStore}; use crate::context_store::{refresh_context_store_text, ContextStore};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::thread::{RequestKind, Thread}; use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use crate::tool_selector::ToolSelector;
use crate::{Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker}; use crate::{Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker};
pub struct MessageEditor { pub struct MessageEditor {
@ -43,7 +43,7 @@ pub struct MessageEditor {
inline_context_picker: Entity<ContextPicker>, inline_context_picker: Entity<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>, inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>, model_selector: Entity<AssistantModelSelector>,
tool_selector: Entity<ToolSelector>, profile_selector: Entity<ProfileSelector>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -57,7 +57,6 @@ impl MessageEditor {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let tools = thread.read(cx).tools().clone();
let context_picker_menu_handle = PopoverMenuHandle::default(); let context_picker_menu_handle = PopoverMenuHandle::default();
let inline_context_picker_menu_handle = PopoverMenuHandle::default(); let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default(); let model_selector_menu_handle = PopoverMenuHandle::default();
@ -129,14 +128,14 @@ impl MessageEditor {
inline_context_picker_menu_handle, inline_context_picker_menu_handle,
model_selector: cx.new(|cx| { model_selector: cx.new(|cx| {
AssistantModelSelector::new( AssistantModelSelector::new(
fs, fs.clone(),
model_selector_menu_handle, model_selector_menu_handle,
editor.focus_handle(cx), editor.focus_handle(cx),
window, window,
cx, cx,
) )
}), }),
tool_selector: cx.new(|cx| ToolSelector::new(tools, cx)), profile_selector: cx.new(|cx| ProfileSelector::new(fs, thread_store, cx)),
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
} }
@ -624,7 +623,7 @@ impl Render for MessageEditor {
.child( .child(
h_flex() h_flex()
.justify_between() .justify_between()
.child(h_flex().gap_2().child(self.tool_selector.clone())) .child(h_flex().gap_2().child(self.profile_selector.clone()))
.child( .child(
h_flex().gap_1().child(self.model_selector.clone()).child( h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("submit-message") ButtonLike::new("submit-message")

View file

@ -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)
}
}

View file

@ -3,7 +3,8 @@ use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use assistant_tool::{ToolId, ToolWorkingSet}; use assistant_settings::AssistantSettings;
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use collections::HashMap; use collections::HashMap;
use context_server::manager::ContextServerManager; use context_server::manager::ContextServerManager;
@ -19,6 +20,7 @@ use language_model::{LanguageModelToolUseId, Role};
use project::Project; use project::Project;
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings as _;
use util::ResultExt as _; use util::ResultExt as _;
use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadEvent, ThreadId}; use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadEvent, ThreadId};
@ -57,6 +59,7 @@ impl ThreadStore {
context_server_tool_ids: HashMap::default(), context_server_tool_ids: HashMap::default(),
threads: Vec::new(), threads: Vec::new(),
}; };
this.load_default_profile(cx);
this.register_context_server_handlers(cx); this.register_context_server_handlers(cx);
this.reload(cx).detach_and_log_err(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>) { fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
cx.subscribe( cx.subscribe(
&self.context_server_manager.clone(), &self.context_server_manager.clone(),

View file

@ -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)
}
}

View file

@ -71,6 +71,7 @@ pub struct AssistantSettings {
pub inline_alternatives: Vec<LanguageModelSelection>, pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool, pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool, pub enable_experimental_live_diffs: bool,
pub default_profile: Arc<str>,
pub profiles: IndexMap<Arc<str>, AgentProfile>, pub profiles: IndexMap<Arc<str>, AgentProfile>,
pub always_allow_tool_actions: bool, pub always_allow_tool_actions: bool,
pub notify_when_agent_waiting: bool, pub notify_when_agent_waiting: bool,
@ -174,6 +175,7 @@ impl AssistantSettingsContent {
editor_model: None, editor_model: None,
inline_alternatives: None, inline_alternatives: None,
enable_experimental_live_diffs: None, enable_experimental_live_diffs: None,
default_profile: None,
profiles: None, profiles: None,
always_allow_tool_actions: None, always_allow_tool_actions: None,
notify_when_agent_waiting: None, notify_when_agent_waiting: None,
@ -198,6 +200,7 @@ impl AssistantSettingsContent {
editor_model: None, editor_model: None,
inline_alternatives: None, inline_alternatives: None,
enable_experimental_live_diffs: None, enable_experimental_live_diffs: None,
default_profile: None,
profiles: None, profiles: None,
always_allow_tool_actions: None, always_allow_tool_actions: None,
notify_when_agent_waiting: 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)] #[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
@ -330,6 +345,7 @@ impl Default for VersionedAssistantSettingsContent {
editor_model: None, editor_model: None,
inline_alternatives: None, inline_alternatives: None,
enable_experimental_live_diffs: None, enable_experimental_live_diffs: None,
default_profile: None,
profiles: None, profiles: None,
always_allow_tool_actions: None, always_allow_tool_actions: None,
notify_when_agent_waiting: None, notify_when_agent_waiting: None,
@ -370,7 +386,9 @@ pub struct AssistantSettingsContentV2 {
/// Default: false /// Default: false
enable_experimental_live_diffs: Option<bool>, enable_experimental_live_diffs: Option<bool>,
#[schemars(skip)] #[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 /// Whenever a tool action would normally wait for your confirmation
/// that you allow it, always choose to allow it. /// that you allow it, always choose to allow it.
/// ///
@ -531,6 +549,7 @@ impl Settings for AssistantSettings {
&mut settings.notify_when_agent_waiting, &mut settings.notify_when_agent_waiting,
value.notify_when_agent_waiting, value.notify_when_agent_waiting,
); );
merge(&mut settings.default_profile, value.default_profile);
if let Some(profiles) = value.profiles { if let Some(profiles) = value.profiles {
settings settings
@ -621,6 +640,7 @@ mod tests {
default_width: None, default_width: None,
default_height: None, default_height: None,
enable_experimental_live_diffs: None, enable_experimental_live_diffs: None,
default_profile: None,
profiles: None, profiles: None,
always_allow_tool_actions: None, always_allow_tool_actions: None,
notify_when_agent_waiting: None, notify_when_agent_waiting: None,