diff --git a/Cargo.lock b/Cargo.lock
index 70d7a433ae..6a7e05d563 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -79,7 +79,6 @@ dependencies = [
"heed",
"html_to_markdown",
"http_client",
- "indexmap",
"indoc",
"itertools 0.14.0",
"jsonschema",
diff --git a/assets/icons/message_bubble_dashed.svg b/assets/icons/message_bubble_dashed.svg
new file mode 100644
index 0000000000..02d44c7362
--- /dev/null
+++ b/assets/icons/message_bubble_dashed.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/scissors.svg b/assets/icons/scissors.svg
new file mode 100644
index 0000000000..e7fb6005f4
--- /dev/null
+++ b/assets/icons/scissors.svg
@@ -0,0 +1 @@
+
diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml
index 8594551e80..36c4f3f042 100644
--- a/crates/agent/Cargo.toml
+++ b/crates/agent/Cargo.toml
@@ -46,7 +46,6 @@ gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
-indexmap.workspace = true
itertools.workspace = true
jsonschema.workspace = true
language.workspace = true
diff --git a/crates/agent/src/assistant_configuration/manage_profiles_modal.rs b/crates/agent/src/assistant_configuration/manage_profiles_modal.rs
index 6f5172a8d4..6e4bafc154 100644
--- a/crates/agent/src/assistant_configuration/manage_profiles_modal.rs
+++ b/crates/agent/src/assistant_configuration/manage_profiles_modal.rs
@@ -2,7 +2,7 @@ mod profile_modal_header;
use std::sync::Arc;
-use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
+use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings, builtin_profiles};
use assistant_tool::ToolWorkingSet;
use convert_case::{Case, Casing as _};
use editor::Editor;
@@ -22,6 +22,8 @@ use crate::assistant_configuration::manage_profiles_modal::profile_modal_header:
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AssistantPanel, ManageProfiles, ThreadStore};
+use super::tool_picker::ToolPickerMode;
+
enum Mode {
ChooseProfile(ChooseProfileMode),
NewProfile(NewProfileMode),
@@ -31,26 +33,39 @@ enum Mode {
tool_picker: Entity,
_subscription: Subscription,
},
+ ConfigureMcps {
+ profile_id: AgentProfileId,
+ tool_picker: Entity,
+ _subscription: Subscription,
+ },
}
impl Mode {
pub fn choose_profile(_window: &mut Window, cx: &mut Context) -> Self {
let settings = AssistantSettings::get_global(cx);
- let mut profiles = settings.profiles.clone();
- profiles.sort_unstable_by(|_, a, _, b| a.name.cmp(&b.name));
+ let mut builtin_profiles = Vec::new();
+ let mut custom_profiles = Vec::new();
- let profiles = profiles
- .into_iter()
- .map(|(id, profile)| ProfileEntry {
- id,
- name: profile.name,
+ for (profile_id, profile) in settings.profiles.iter() {
+ let entry = ProfileEntry {
+ id: profile_id.clone(),
+ name: profile.name.clone(),
navigation: NavigableEntry::focusable(cx),
- })
- .collect::>();
+ };
+ if builtin_profiles::is_builtin(profile_id) {
+ builtin_profiles.push(entry);
+ } else {
+ custom_profiles.push(entry);
+ }
+ }
+
+ builtin_profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name));
+ custom_profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name));
Self::ChooseProfile(ChooseProfileMode {
- profiles,
+ builtin_profiles,
+ custom_profiles,
add_new_profile: NavigableEntry::focusable(cx),
})
}
@@ -65,7 +80,8 @@ struct ProfileEntry {
#[derive(Clone)]
pub struct ChooseProfileMode {
- profiles: Vec,
+ builtin_profiles: Vec,
+ custom_profiles: Vec,
add_new_profile: NavigableEntry,
}
@@ -74,6 +90,8 @@ pub struct ViewProfileMode {
profile_id: AgentProfileId,
fork_profile: NavigableEntry,
configure_tools: NavigableEntry,
+ configure_mcps: NavigableEntry,
+ cancel_item: NavigableEntry,
}
#[derive(Clone)]
@@ -166,10 +184,50 @@ impl ManageProfilesModal {
profile_id,
fork_profile: NavigableEntry::focusable(cx),
configure_tools: NavigableEntry::focusable(cx),
+ configure_mcps: NavigableEntry::focusable(cx),
+ cancel_item: NavigableEntry::focusable(cx),
});
self.focus_handle(cx).focus(window);
}
+ fn configure_mcps(
+ &mut self,
+ profile_id: AgentProfileId,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ let settings = AssistantSettings::get_global(cx);
+ let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
+ return;
+ };
+
+ let tool_picker = cx.new(|cx| {
+ let delegate = ToolPickerDelegate::new(
+ ToolPickerMode::McpTools,
+ self.fs.clone(),
+ self.tools.clone(),
+ self.thread_store.clone(),
+ profile_id.clone(),
+ profile,
+ cx,
+ );
+ ToolPicker::mcp_tools(delegate, window, cx)
+ });
+ let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
+ let profile_id = profile_id.clone();
+ move |this, _tool_picker, _: &DismissEvent, window, cx| {
+ this.view_profile(profile_id.clone(), window, cx);
+ }
+ });
+
+ self.mode = Mode::ConfigureMcps {
+ profile_id,
+ tool_picker,
+ _subscription: dismiss_subscription,
+ };
+ self.focus_handle(cx).focus(window);
+ }
+
fn configure_tools(
&mut self,
profile_id: AgentProfileId,
@@ -183,6 +241,7 @@ impl ManageProfilesModal {
let tool_picker = cx.new(|cx| {
let delegate = ToolPickerDelegate::new(
+ ToolPickerMode::BuiltinTools,
self.fs.clone(),
self.tools.clone(),
self.thread_store.clone(),
@@ -190,7 +249,7 @@ impl ManageProfilesModal {
profile,
cx,
);
- ToolPicker::new(delegate, window, cx)
+ ToolPicker::builtin_tools(delegate, window, cx)
});
let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
let profile_id = profile_id.clone();
@@ -241,6 +300,7 @@ impl ManageProfilesModal {
}
Mode::ViewProfile(_) => {}
Mode::ConfigureTools { .. } => {}
+ Mode::ConfigureMcps { .. } => {}
}
}
@@ -257,7 +317,12 @@ impl ManageProfilesModal {
}
}
Mode::ViewProfile(_) => self.choose_profile(window, cx),
- Mode::ConfigureTools { .. } => {}
+ Mode::ConfigureTools { profile_id, .. } => {
+ self.view_profile(profile_id.clone(), window, cx)
+ }
+ Mode::ConfigureMcps { profile_id, .. } => {
+ self.view_profile(profile_id.clone(), window, cx)
+ }
}
}
@@ -284,6 +349,7 @@ impl Focusable for ManageProfilesModal {
Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
Mode::ViewProfile(_) => self.focus_handle.clone(),
Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
+ Mode::ConfigureMcps { tool_picker, .. } => tool_picker.focus_handle(cx),
}
}
}
@@ -291,6 +357,51 @@ impl Focusable for ManageProfilesModal {
impl EventEmitter for ManageProfilesModal {}
impl ManageProfilesModal {
+ fn render_profile(
+ &self,
+ profile: &ProfileEntry,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> impl IntoElement + use<> {
+ div()
+ .id(SharedString::from(format!("profile-{}", profile.id)))
+ .track_focus(&profile.navigation.focus_handle)
+ .on_action({
+ let profile_id = profile.id.clone();
+ cx.listener(move |this, _: &menu::Confirm, window, cx| {
+ this.view_profile(profile_id.clone(), window, cx);
+ })
+ })
+ .child(
+ ListItem::new(SharedString::from(format!("profile-{}", profile.id)))
+ .toggle_state(profile.navigation.focus_handle.contains_focused(window, cx))
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .child(Label::new(profile.name.clone()))
+ .end_slot(
+ h_flex()
+ .gap_1()
+ .child(
+ Label::new("Customize")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .children(KeyBinding::for_action_in(
+ &menu::Confirm,
+ &self.focus_handle,
+ window,
+ cx,
+ )),
+ )
+ .on_click({
+ let profile_id = profile.id.clone();
+ cx.listener(move |this, _, window, cx| {
+ this.view_profile(profile_id.clone(), window, cx);
+ })
+ }),
+ )
+ }
+
fn render_choose_profile(
&mut self,
mode: ChooseProfileMode,
@@ -301,57 +412,31 @@ impl ManageProfilesModal {
div()
.track_focus(&self.focus_handle(cx))
.size_full()
- .child(ProfileModalHeader::new(
- "Agent Profiles",
- IconName::ZedAssistant,
- ))
+ .child(ProfileModalHeader::new("Agent Profiles", None))
.child(
v_flex()
.pb_1()
.child(ListSeparator)
- .children(mode.profiles.iter().map(|profile| {
- div()
- .id(SharedString::from(format!("profile-{}", profile.id)))
- .track_focus(&profile.navigation.focus_handle)
- .on_action({
- let profile_id = profile.id.clone();
- cx.listener(move |this, _: &menu::Confirm, window, cx| {
- this.view_profile(profile_id.clone(), window, cx);
- })
- })
+ .children(
+ mode.builtin_profiles
+ .iter()
+ .map(|profile| self.render_profile(profile, window, cx)),
+ )
+ .when(!mode.custom_profiles.is_empty(), |this| {
+ this.child(ListSeparator)
.child(
- ListItem::new(SharedString::from(format!(
- "profile-{}",
- profile.id
- )))
- .toggle_state(
- profile
- .navigation
- .focus_handle
- .contains_focused(window, cx),
- )
- .inset(true)
- .spacing(ListItemSpacing::Sparse)
- .child(Label::new(profile.name.clone()))
- .end_slot(
- h_flex()
- .gap_1()
- .child(Label::new("Customize").size(LabelSize::Small))
- .children(KeyBinding::for_action_in(
- &menu::Confirm,
- &self.focus_handle,
- window,
- cx,
- )),
- )
- .on_click({
- let profile_id = profile.id.clone();
- cx.listener(move |this, _, window, cx| {
- this.view_profile(profile_id.clone(), window, cx);
- })
- }),
+ div().pl_2().pb_1().child(
+ Label::new("Custom Profiles")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
)
- }))
+ .children(
+ mode.custom_profiles
+ .iter()
+ .map(|profile| self.render_profile(profile, window, cx)),
+ )
+ })
.child(ListSeparator)
.child(
div()
@@ -382,7 +467,10 @@ impl ManageProfilesModal {
.into_any_element(),
)
.map(|mut navigable| {
- for profile in mode.profiles {
+ for profile in mode.builtin_profiles {
+ navigable = navigable.entry(profile.navigation);
+ }
+ for profile in mode.custom_profiles {
navigable = navigable.entry(profile.navigation);
}
@@ -411,11 +499,14 @@ impl ManageProfilesModal {
.id("new-profile")
.track_focus(&self.focus_handle(cx))
.child(ProfileModalHeader::new(
- match base_profile_name {
+ match &base_profile_name {
Some(base_profile) => format!("Fork {base_profile}"),
None => "New Profile".into(),
},
- IconName::Plus,
+ match base_profile_name {
+ Some(_) => Some(IconName::Scissors),
+ None => Some(IconName::Plus),
+ },
))
.child(ListSeparator)
.child(h_flex().p_2().child(mode.name_editor.clone()))
@@ -429,20 +520,24 @@ impl ManageProfilesModal {
) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
+ let profile_id = &settings.default_profile;
let profile_name = settings
.profiles
.get(&mode.profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
+ let icon = match profile_id.as_str() {
+ "write" => IconName::Pencil,
+ "ask" => IconName::MessageBubbles,
+ _ => IconName::UserRoundPen,
+ };
+
Navigable::new(
div()
.track_focus(&self.focus_handle(cx))
.size_full()
- .child(ProfileModalHeader::new(
- profile_name,
- IconName::ZedAssistant,
- ))
+ .child(ProfileModalHeader::new(profile_name, Some(icon)))
.child(
v_flex()
.pb_1()
@@ -466,7 +561,11 @@ impl ManageProfilesModal {
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
- .start_slot(Icon::new(IconName::GitBranch))
+ .start_slot(
+ Icon::new(IconName::Scissors)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
.child(Label::new("Fork Profile"))
.on_click({
let profile_id = mode.profile_id.clone();
@@ -499,7 +598,11 @@ impl ManageProfilesModal {
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
- .start_slot(Icon::new(IconName::Cog))
+ .start_slot(
+ Icon::new(IconName::Settings)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
.child(Label::new("Configure Tools"))
.on_click({
let profile_id = mode.profile_id.clone();
@@ -512,12 +615,90 @@ impl ManageProfilesModal {
})
}),
),
+ )
+ .child(
+ div()
+ .id("configure-mcps")
+ .track_focus(&mode.configure_mcps.focus_handle)
+ .on_action({
+ let profile_id = mode.profile_id.clone();
+ cx.listener(move |this, _: &menu::Confirm, window, cx| {
+ this.configure_mcps(profile_id.clone(), window, cx);
+ })
+ })
+ .child(
+ ListItem::new("configure-mcps")
+ .toggle_state(
+ mode.configure_mcps
+ .focus_handle
+ .contains_focused(window, cx),
+ )
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .start_slot(
+ Icon::new(IconName::Hammer)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Label::new("Configure MCP Servers"))
+ .on_click({
+ let profile_id = mode.profile_id.clone();
+ cx.listener(move |this, _, window, cx| {
+ this.configure_mcps(profile_id.clone(), window, cx);
+ })
+ }),
+ ),
+ )
+ .child(ListSeparator)
+ .child(
+ div()
+ .id("cancel-item")
+ .track_focus(&mode.cancel_item.focus_handle)
+ .on_action({
+ cx.listener(move |this, _: &menu::Confirm, window, cx| {
+ this.cancel(window, cx);
+ })
+ })
+ .child(
+ ListItem::new("cancel-item")
+ .toggle_state(
+ mode.cancel_item
+ .focus_handle
+ .contains_focused(window, cx),
+ )
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .start_slot(
+ Icon::new(IconName::ArrowLeft)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Label::new("Go Back"))
+ .end_slot(
+ div().children(
+ KeyBinding::for_action_in(
+ &menu::Cancel,
+ &self.focus_handle,
+ window,
+ cx,
+ )
+ .map(|kb| kb.size(rems_from_px(12.))),
+ ),
+ )
+ .on_click({
+ cx.listener(move |this, _, window, cx| {
+ this.cancel(window, cx);
+ })
+ }),
+ ),
),
)
.into_any_element(),
)
.entry(mode.fork_profile)
.entry(mode.configure_tools)
+ .entry(mode.configure_mcps)
+ .entry(mode.cancel_item)
}
}
@@ -525,6 +706,43 @@ impl Render for ManageProfilesModal {
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
+ let go_back_item = div()
+ .id("cancel-item")
+ .track_focus(&self.focus_handle)
+ .on_action({
+ cx.listener(move |this, _: &menu::Confirm, window, cx| {
+ this.cancel(window, cx);
+ })
+ })
+ .child(
+ ListItem::new("cancel-item")
+ .toggle_state(self.focus_handle.contains_focused(window, cx))
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .start_slot(
+ Icon::new(IconName::ArrowLeft)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Label::new("Go Back"))
+ .end_slot(
+ div().children(
+ KeyBinding::for_action_in(
+ &menu::Cancel,
+ &self.focus_handle,
+ window,
+ cx,
+ )
+ .map(|kb| kb.size(rems_from_px(12.))),
+ ),
+ )
+ .on_click({
+ cx.listener(move |this, _, window, cx| {
+ this.cancel(window, cx);
+ })
+ }),
+ );
+
div()
.elevation_3(cx)
.w(rems(34.))
@@ -556,13 +774,39 @@ impl Render for ManageProfilesModal {
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
- div()
+ v_flex()
+ .pb_1()
.child(ProfileModalHeader::new(
- format!("{profile_name}: Configure Tools"),
- IconName::Cog,
+ format!("{profile_name} — Configure Tools"),
+ Some(IconName::Cog),
))
.child(ListSeparator)
.child(tool_picker.clone())
+ .child(ListSeparator)
+ .child(go_back_item)
+ .into_any_element()
+ }
+ Mode::ConfigureMcps {
+ profile_id,
+ tool_picker,
+ ..
+ } => {
+ let profile_name = settings
+ .profiles
+ .get(profile_id)
+ .map(|profile| profile.name.clone())
+ .unwrap_or_else(|| "Unknown".into());
+
+ v_flex()
+ .pb_1()
+ .child(ProfileModalHeader::new(
+ format!("{profile_name} — Configure MCP Servers"),
+ Some(IconName::Hammer),
+ ))
+ .child(ListSeparator)
+ .child(tool_picker.clone())
+ .child(ListSeparator)
+ .child(go_back_item)
.into_any_element()
}
})
diff --git a/crates/agent/src/assistant_configuration/manage_profiles_modal/profile_modal_header.rs b/crates/agent/src/assistant_configuration/manage_profiles_modal/profile_modal_header.rs
index 401c4a9bff..4eca9d721c 100644
--- a/crates/agent/src/assistant_configuration/manage_profiles_modal/profile_modal_header.rs
+++ b/crates/agent/src/assistant_configuration/manage_profiles_modal/profile_modal_header.rs
@@ -3,11 +3,11 @@ use ui::prelude::*;
#[derive(IntoElement)]
pub struct ProfileModalHeader {
label: SharedString,
- icon: IconName,
+ icon: Option,
}
impl ProfileModalHeader {
- pub fn new(label: impl Into, icon: IconName) -> Self {
+ pub fn new(label: impl Into, icon: Option) -> Self {
Self {
label: label.into(),
icon,
@@ -17,22 +17,26 @@ impl ProfileModalHeader {
impl RenderOnce for ProfileModalHeader {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
- h_flex()
+ let mut container = h_flex()
.w_full()
.px(DynamicSpacing::Base12.rems(cx))
.pt(DynamicSpacing::Base08.rems(cx))
.pb(DynamicSpacing::Base04.rems(cx))
.rounded_t_sm()
- .gap_1p5()
- .child(Icon::new(self.icon).size(IconSize::XSmall))
- .child(
- h_flex().gap_1().overflow_x_hidden().child(
- div()
- .max_w_96()
- .overflow_x_hidden()
- .text_ellipsis()
- .child(Headline::new(self.label).size(HeadlineSize::XSmall)),
- ),
- )
+ .gap_1p5();
+
+ if let Some(icon) = self.icon {
+ container = container.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
+ }
+
+ container.child(
+ h_flex().gap_1().overflow_x_hidden().child(
+ div()
+ .max_w_96()
+ .overflow_x_hidden()
+ .text_ellipsis()
+ .child(Headline::new(self.label).size(HeadlineSize::XSmall)),
+ ),
+ )
}
}
diff --git a/crates/agent/src/assistant_configuration/tool_picker.rs b/crates/agent/src/assistant_configuration/tool_picker.rs
index 2c48792446..bc6fd80019 100644
--- a/crates/agent/src/assistant_configuration/tool_picker.rs
+++ b/crates/agent/src/assistant_configuration/tool_picker.rs
@@ -1,4 +1,4 @@
-use std::sync::Arc;
+use std::{collections::BTreeMap, sync::Arc};
use assistant_settings::{
AgentProfile, AgentProfileContent, AgentProfileId, AssistantSettings, AssistantSettingsContent,
@@ -6,11 +6,10 @@ use assistant_settings::{
};
use assistant_tool::{ToolSource, ToolWorkingSet};
use fs::Fs;
-use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
use picker::{Picker, PickerDelegate};
use settings::{Settings as _, update_settings_file};
-use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
+use ui::{ListItem, ListItemSpacing, prelude::*};
use util::ResultExt as _;
use crate::ThreadStore;
@@ -19,11 +18,30 @@ pub struct ToolPicker {
picker: Entity>,
}
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum ToolPickerMode {
+ BuiltinTools,
+ McpTools,
+}
+
impl ToolPicker {
- pub fn new(delegate: ToolPickerDelegate, window: &mut Window, cx: &mut Context) -> Self {
+ pub fn builtin_tools(
+ delegate: ToolPickerDelegate,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
Self { picker }
}
+
+ pub fn mcp_tools(
+ delegate: ToolPickerDelegate,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> Self {
+ let picker = cx.new(|cx| Picker::list(delegate, window, cx).modal(false));
+ Self { picker }
+ }
}
impl EventEmitter for ToolPicker {}
@@ -41,24 +59,31 @@ impl Render for ToolPicker {
}
#[derive(Debug, Clone)]
-pub struct ToolEntry {
- pub name: Arc,
- pub source: ToolSource,
+pub enum PickerItem {
+ Tool {
+ server_id: Option>,
+ name: Arc,
+ },
+ ContextServer {
+ server_id: Arc,
+ },
}
pub struct ToolPickerDelegate {
tool_picker: WeakEntity,
thread_store: WeakEntity,
fs: Arc,
- tools: Vec,
+ items: Arc>,
profile_id: AgentProfileId,
profile: AgentProfile,
- matches: Vec,
+ filtered_items: Vec,
selected_index: usize,
+ mode: ToolPickerMode,
}
impl ToolPickerDelegate {
pub fn new(
+ mode: ToolPickerMode,
fs: Arc,
tool_set: Entity,
thread_store: WeakEntity,
@@ -66,33 +91,60 @@ impl ToolPickerDelegate {
profile: AgentProfile,
cx: &mut Context,
) -> Self {
- let mut tool_entries = Vec::new();
-
- for (source, tools) in tool_set.read(cx).tools_by_source(cx) {
- tool_entries.extend(tools.into_iter().map(|tool| ToolEntry {
- name: tool.name().into(),
- source: source.clone(),
- }));
- }
+ let items = Arc::new(Self::resolve_items(mode, &tool_set, cx));
Self {
tool_picker: cx.entity().downgrade(),
thread_store,
fs,
- tools: tool_entries,
+ items,
profile_id,
profile,
- matches: Vec::new(),
+ filtered_items: Vec::new(),
selected_index: 0,
+ mode,
}
}
+
+ fn resolve_items(
+ mode: ToolPickerMode,
+ tool_set: &Entity,
+ cx: &mut App,
+ ) -> Vec {
+ let mut items = Vec::new();
+ for (source, tools) in tool_set.read(cx).tools_by_source(cx) {
+ match source {
+ ToolSource::Native => {
+ if mode == ToolPickerMode::BuiltinTools {
+ items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
+ name: tool.name().into(),
+ server_id: None,
+ }));
+ }
+ }
+ ToolSource::ContextServer { id } => {
+ if mode == ToolPickerMode::McpTools && !tools.is_empty() {
+ let server_id: Arc = id.clone().into();
+ items.push(PickerItem::ContextServer {
+ server_id: server_id.clone(),
+ });
+ items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
+ name: tool.name().into(),
+ server_id: Some(server_id.clone()),
+ }));
+ }
+ }
+ }
+ }
+ items
+ }
}
impl PickerDelegate for ToolPickerDelegate {
- type ListItem = ListItem;
+ type ListItem = AnyElement;
fn match_count(&self) -> usize {
- self.matches.len()
+ self.filtered_items.len()
}
fn selected_index(&self) -> usize {
@@ -108,8 +160,25 @@ impl PickerDelegate for ToolPickerDelegate {
self.selected_index = ix;
}
+ fn can_select(
+ &mut self,
+ ix: usize,
+ _window: &mut Window,
+ _cx: &mut Context>,
+ ) -> bool {
+ let item = &self.filtered_items[ix];
+ match item {
+ PickerItem::Tool { .. } => true,
+ PickerItem::ContextServer { .. } => false,
+ }
+ }
+
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc {
- "Search tools…".into()
+ match self.mode {
+ ToolPickerMode::BuiltinTools => "Search built-in tools…",
+ ToolPickerMode::McpTools => "Search MCP servers…",
+ }
+ .into()
}
fn update_matches(
@@ -118,74 +187,76 @@ impl PickerDelegate for ToolPickerDelegate {
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::>();
+ let all_items = self.items.clone();
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
- };
+ let filtered_items = cx
+ .background_spawn(async move {
+ let mut tools_by_provider: BTreeMap