@@ -1,5 +1,6 @@
mod add_llm_provider_modal;
mod configure_context_server_modal;
+mod configure_context_server_tools_modal;
mod manage_profiles_modal;
mod tool_picker;
@@ -42,12 +43,12 @@ use workspace::{Workspace, create_and_open_local_file};
use zed_actions::ExtensionCategoryFilter;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
+pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::{
- AddContextServer, ExternalAgent, NewExternalAgentThread,
+ AddContextServer,
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
- placeholder_command,
};
pub struct AgentConfiguration {
@@ -200,9 +201,8 @@ impl AgentConfiguration {
.when(is_expanded, |this| this.mb_2())
.child(
div()
- .opacity(0.6)
.px_2()
- .child(Divider::horizontal().color(DividerColor::Border)),
+ .child(Divider::horizontal().color(DividerColor::BorderFaded)),
)
.child(
h_flex()
@@ -227,7 +227,7 @@ impl AgentConfiguration {
.child(
h_flex()
.w_full()
- .gap_2()
+ .gap_1p5()
.child(
Icon::new(provider.icon())
.size(IconSize::Small)
@@ -345,6 +345,8 @@ impl AgentConfiguration {
PopoverMenu::new("add-provider-popover")
.trigger(
Button::new("add-provider", "Add Provider")
+ .style(ButtonStyle::Filled)
+ .layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
@@ -533,10 +535,6 @@ impl AgentConfiguration {
}
}
- fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
- cx.theme().colors().background.opacity(0.25)
- }
-
fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
cx.theme().colors().border.opacity(0.6)
}
@@ -546,7 +544,73 @@ impl AgentConfiguration {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
- let context_server_ids = self.context_server_store.read(cx).configured_server_ids();
+ let mut registry_descriptors = self
+ .context_server_store
+ .read(cx)
+ .all_registry_descriptor_ids(cx);
+ let server_count = registry_descriptors.len();
+
+ // Sort context servers: non-mcp-server ones first, then mcp-server ones
+ registry_descriptors.sort_by(|a, b| {
+ let has_mcp_prefix_a = a.0.starts_with("mcp-server-");
+ let has_mcp_prefix_b = b.0.starts_with("mcp-server-");
+
+ match (has_mcp_prefix_a, has_mcp_prefix_b) {
+ // If one has mcp-server- prefix and other doesn't, non-mcp comes first
+ (true, false) => std::cmp::Ordering::Greater,
+ (false, true) => std::cmp::Ordering::Less,
+ // If both have same prefix status, sort by appropriate key
+ _ => {
+ let get_sort_key = |server_id: &str| -> String {
+ if let Some(suffix) = server_id.strip_prefix("mcp-server-") {
+ suffix.to_string()
+ } else {
+ server_id.to_string()
+ }
+ };
+
+ let key_a = get_sort_key(&a.0);
+ let key_b = get_sort_key(&b.0);
+ key_a.cmp(&key_b)
+ }
+ }
+ });
+
+ let add_server_popover = PopoverMenu::new("add-server-popover")
+ .trigger(
+ Button::new("add-server", "Add Server")
+ .style(ButtonStyle::Filled)
+ .layer(ElevationIndex::ModalSurface)
+ .icon_position(IconPosition::Start)
+ .icon(IconName::Plus)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .label_size(LabelSize::Small),
+ )
+ .anchor(gpui::Corner::TopRight)
+ .menu({
+ move |window, cx| {
+ Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
+ menu.entry("Add Custom Server", None, {
+ |window, cx| window.dispatch_action(AddContextServer.boxed_clone(), cx)
+ })
+ .entry("Install from Extensions", None, {
+ |window, cx| {
+ window.dispatch_action(
+ zed_actions::Extensions {
+ category_filter: Some(
+ ExtensionCategoryFilter::ContextServers,
+ ),
+ id: None,
+ }
+ .boxed_clone(),
+ cx,
+ )
+ }
+ })
+ }))
+ }
+ });
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
@@ -555,18 +619,26 @@ impl AgentConfiguration {
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
- v_flex()
- .gap_0p5()
- .child(Headline::new("Model Context Protocol (MCP) Servers"))
+ h_flex()
+ .w_full()
+ .items_start()
+ .justify_between()
+ .gap_1()
.child(
- Label::new(
- "All context servers connected through the Model Context Protocol.",
- )
- .color(Color::Muted),
- ),
+ v_flex()
+ .gap_0p5()
+ .child(Headline::new("Model Context Protocol (MCP) Servers"))
+ .child(
+ Label::new(
+ "All MCP servers connected directly or via a Zed extension.",
+ )
+ .color(Color::Muted),
+ ),
+ )
+ .child(add_server_popover),
)
- .map(|parent| {
- if context_server_ids.is_empty() {
+ .child(v_flex().w_full().gap_1().map(|parent| {
+ if registry_descriptors.is_empty() {
parent.child(
h_flex()
.p_4()
@@ -582,56 +654,28 @@ impl AgentConfiguration {
),
)
} else {
- parent.children(context_server_ids.into_iter().map(|context_server_id| {
- self.render_context_server(context_server_id, window, cx)
- }))
+ {
+ parent.children(registry_descriptors.into_iter().enumerate().flat_map(
+ |(index, context_server_id)| {
+ let mut elements: Vec<AnyElement> = vec![
+ self.render_context_server(context_server_id, window, cx)
+ .into_any_element(),
+ ];
+
+ if index < server_count - 1 {
+ elements.push(
+ Divider::horizontal()
+ .color(DividerColor::BorderFaded)
+ .into_any_element(),
+ );
+ }
+
+ elements
+ },
+ ))
+ }
}
- })
- .child(
- h_flex()
- .justify_between()
- .gap_1p5()
- .child(
- h_flex().w_full().child(
- Button::new("add-context-server", "Add Custom Server")
- .style(ButtonStyle::Filled)
- .layer(ElevationIndex::ModalSurface)
- .full_width()
- .icon(IconName::Plus)
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .on_click(|_event, window, cx| {
- window.dispatch_action(AddContextServer.boxed_clone(), cx)
- }),
- ),
- )
- .child(
- h_flex().w_full().child(
- Button::new(
- "install-context-server-extensions",
- "Install MCP Extensions",
- )
- .style(ButtonStyle::Filled)
- .layer(ElevationIndex::ModalSurface)
- .full_width()
- .icon(IconName::ToolHammer)
- .icon_size(IconSize::Small)
- .icon_position(IconPosition::Start)
- .on_click(|_event, window, cx| {
- window.dispatch_action(
- zed_actions::Extensions {
- category_filter: Some(
- ExtensionCategoryFilter::ContextServers,
- ),
- id: None,
- }
- .boxed_clone(),
- cx,
- )
- }),
- ),
- ),
- )
+ }))
}
fn render_context_server(
@@ -724,7 +768,7 @@ impl AgentConfiguration {
IconButton::new("context-server-config-menu", IconName::Settings)
.icon_color(Color::Muted)
.icon_size(IconSize::Small),
- Tooltip::text("Open MCP server options"),
+ Tooltip::text("Configure MCP Server"),
)
.anchor(Corner::TopRight)
.menu({
@@ -733,6 +777,8 @@ impl AgentConfiguration {
let language_registry = self.language_registry.clone();
let context_server_store = self.context_server_store.clone();
let workspace = self.workspace.clone();
+ let tools = self.tools.clone();
+
move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.entry("Configure Server", None, {
@@ -749,7 +795,28 @@ impl AgentConfiguration {
)
.detach_and_log_err(cx);
}
- })
+ }).when(tool_count >= 1, |this| this.entry("View Tools", None, {
+ let context_server_id = context_server_id.clone();
+ let tools = tools.clone();
+ let workspace = workspace.clone();
+
+ move |window, cx| {
+ let context_server_id = context_server_id.clone();
+ let tools = tools.clone();
+ let workspace = workspace.clone();
+
+ workspace.update(cx, |workspace, cx| {
+ ConfigureContextServerToolsModal::toggle(
+ context_server_id,
+ tools,
+ workspace,
+ window,
+ cx,
+ );
+ })
+ .ok();
+ }
+ }))
.separator()
.entry("Uninstall", None, {
let fs = fs.clone();
@@ -820,17 +887,11 @@ impl AgentConfiguration {
v_flex()
.id(item_id.clone())
- .border_1()
- .rounded_md()
- .border_color(self.card_item_border_color(cx))
- .bg(self.card_item_bg_color(cx))
- .overflow_hidden()
.child(
h_flex()
- .p_1()
.justify_between()
.when(
- error.is_some() || are_tools_expanded && tool_count >= 1,
+ error.is_none() && are_tools_expanded && tool_count >= 1,
|element| {
element
.border_b_1()
@@ -841,31 +902,12 @@ impl AgentConfiguration {
h_flex()
.flex_1()
.min_w_0()
- .child(
- Disclosure::new(
- "tool-list-disclosure",
- are_tools_expanded || error.is_some(),
- )
- .disabled(tool_count == 0)
- .on_click(cx.listener({
- let context_server_id = context_server_id.clone();
- move |this, _event, _window, _cx| {
- let is_open = this
- .expanded_context_server_tools
- .entry(context_server_id.clone())
- .or_insert(false);
-
- *is_open = !*is_open;
- }
- })),
- )
.child(
h_flex()
.id(SharedString::from(format!("tooltip-{}", item_id)))
.h_full()
.w_3()
- .ml_1()
- .mr_1p5()
+ .mr_2()
.justify_center()
.tooltip(Tooltip::text(tooltip_text))
.child(status_indicator),
@@ -969,8 +1011,8 @@ impl AgentConfiguration {
if let Some(error) = error {
return parent.child(
h_flex()
- .p_2()
.gap_2()
+ .pr_4()
.items_start()
.child(
h_flex()
@@ -998,37 +1040,11 @@ impl AgentConfiguration {
return parent;
}
- parent.child(v_flex().py_1p5().px_1().gap_1().children(
- tools.iter().enumerate().map(|(ix, tool)| {
- h_flex()
- .id(("tool-item", ix))
- .px_1()
- .gap_2()
- .justify_between()
- .hover(|style| style.bg(cx.theme().colors().element_hover))
- .rounded_sm()
- .child(
- Label::new(tool.name())
- .buffer_font(cx)
- .size(LabelSize::Small),
- )
- .child(
- Icon::new(IconName::Info)
- .size(IconSize::Small)
- .color(Color::Ignored),
- )
- .tooltip(Tooltip::text(tool.description()))
- }),
- ))
+ parent
})
}
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
- let custom_settings = cx
- .global::<SettingsStore>()
- .get::<AllAgentServersSettings>(None)
- .custom
- .clone();
let user_defined_agents = self
.agent_server_store
.read(cx)
@@ -1036,22 +1052,12 @@ impl AgentConfiguration {
.filter(|name| name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME)
.cloned()
.collect::<Vec<_>>();
+
let user_defined_agents = user_defined_agents
.into_iter()
.map(|name| {
- self.render_agent_server(
- IconName::Ai,
- name.clone(),
- ExternalAgent::Custom {
- name: name.clone().into(),
- command: custom_settings
- .get(&name.0)
- .map(|settings| settings.command.clone())
- .unwrap_or(placeholder_command()),
- },
- cx,
- )
- .into_any_element()
+ self.render_agent_server(IconName::Ai, name)
+ .into_any_element()
})
.collect::<Vec<_>>();
@@ -1075,6 +1081,8 @@ impl AgentConfiguration {
.child(Headline::new("External Agents"))
.child(
Button::new("add-agent", "Add Agent")
+ .style(ButtonStyle::Filled)
+ .layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
@@ -1107,14 +1115,11 @@ impl AgentConfiguration {
.child(self.render_agent_server(
IconName::AiGemini,
"Gemini CLI",
- ExternalAgent::Gemini,
- cx,
))
+ .child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
IconName::AiClaude,
"Claude Code",
- ExternalAgent::ClaudeCode,
- cx,
))
.children(user_defined_agents),
)
@@ -1124,47 +1129,18 @@ impl AgentConfiguration {
&self,
icon: IconName,
name: impl Into<SharedString>,
- agent: ExternalAgent,
- cx: &mut Context<Self>,
) -> impl IntoElement {
- let name = name.into();
- h_flex()
- .p_1()
- .pl_2()
- .gap_1p5()
- .justify_between()
- .border_1()
- .rounded_md()
- .border_color(self.card_item_border_color(cx))
- .bg(self.card_item_bg_color(cx))
- .overflow_hidden()
- .child(
- h_flex()
- .gap_1p5()
- .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
- .child(Label::new(name.clone())),
- )
- .child(
- Button::new(
- SharedString::from(format!("start_acp_thread-{name}")),
- "Start New Thread",
- )
- .layer(ElevationIndex::ModalSurface)
- .label_size(LabelSize::Small)
- .icon(IconName::Thread)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .on_click(move |_, window, cx| {
- window.dispatch_action(
- NewExternalAgentThread {
- agent: Some(agent.clone()),
- }
- .boxed_clone(),
- cx,
- );
- }),
- )
+ h_flex().gap_1p5().justify_between().child(
+ h_flex()
+ .gap_1p5()
+ .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
+ .child(Label::new(name.into()))
+ .child(
+ Icon::new(IconName::Check)
+ .size(IconSize::Small)
+ .color(Color::Success),
+ ),
+ )
}
}
@@ -0,0 +1,176 @@
+use assistant_tool::{ToolSource, ToolWorkingSet};
+use context_server::ContextServerId;
+use gpui::{
+ DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Window, prelude::*,
+};
+use ui::{Divider, DividerColor, Modal, ModalHeader, WithScrollbar, prelude::*};
+use workspace::{ModalView, Workspace};
+
+pub struct ConfigureContextServerToolsModal {
+ context_server_id: ContextServerId,
+ tools: Entity<ToolWorkingSet>,
+ focus_handle: FocusHandle,
+ expanded_tools: std::collections::HashMap<String, bool>,
+ scroll_handle: ScrollHandle,
+}
+
+impl ConfigureContextServerToolsModal {
+ fn new(
+ context_server_id: ContextServerId,
+ tools: Entity<ToolWorkingSet>,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ Self {
+ context_server_id,
+ tools,
+ focus_handle: cx.focus_handle(),
+ expanded_tools: std::collections::HashMap::new(),
+ scroll_handle: ScrollHandle::new(),
+ }
+ }
+
+ pub fn toggle(
+ context_server_id: ContextServerId,
+ tools: Entity<ToolWorkingSet>,
+ workspace: &mut Workspace,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ ) {
+ workspace.toggle_modal(window, cx, |window, cx| {
+ Self::new(context_server_id, tools, window, cx)
+ });
+ }
+
+ fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+ cx.emit(DismissEvent)
+ }
+
+ fn render_modal_content(
+ &self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> impl IntoElement {
+ let tools_by_source = self.tools.read(cx).tools_by_source(cx);
+ let server_tools = tools_by_source
+ .get(&ToolSource::ContextServer {
+ id: self.context_server_id.0.clone().into(),
+ })
+ .map(|tools| tools.as_slice())
+ .unwrap_or(&[]);
+
+ div()
+ .size_full()
+ .pb_2()
+ .child(
+ v_flex()
+ .id("modal_content")
+ .px_2()
+ .gap_1()
+ .max_h_128()
+ .overflow_y_scroll()
+ .track_scroll(&self.scroll_handle)
+ .children(server_tools.iter().enumerate().flat_map(|(index, tool)| {
+ let tool_name = tool.name();
+ let is_expanded = self
+ .expanded_tools
+ .get(&tool_name)
+ .copied()
+ .unwrap_or(false);
+
+ let icon = if is_expanded {
+ IconName::ChevronUp
+ } else {
+ IconName::ChevronDown
+ };
+
+ let mut items = vec![
+ v_flex()
+ .child(
+ h_flex()
+ .id(SharedString::from(format!("tool-header-{}", index)))
+ .py_1()
+ .pl_1()
+ .pr_2()
+ .w_full()
+ .justify_between()
+ .rounded_sm()
+ .hover(|s| s.bg(cx.theme().colors().element_hover))
+ .child(
+ Label::new(tool_name.clone())
+ .buffer_font(cx)
+ .size(LabelSize::Small),
+ )
+ .child(
+ Icon::new(icon)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .on_click(cx.listener({
+ move |this, _event, _window, _cx| {
+ let current = this
+ .expanded_tools
+ .get(&tool_name)
+ .copied()
+ .unwrap_or(false);
+ this.expanded_tools
+ .insert(tool_name.clone(), !current);
+ _cx.notify();
+ }
+ })),
+ )
+ .when(is_expanded, |this| {
+ this.child(
+ Label::new(tool.description()).color(Color::Muted).mx_1(),
+ )
+ })
+ .into_any_element(),
+ ];
+
+ if index < server_tools.len() - 1 {
+ items.push(
+ h_flex()
+ .w_full()
+ .child(Divider::horizontal().color(DividerColor::BorderVariant))
+ .into_any_element(),
+ );
+ }
+
+ items
+ })),
+ )
+ .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
+ .into_any_element()
+ }
+}
+
+impl ModalView for ConfigureContextServerToolsModal {}
+
+impl Focusable for ConfigureContextServerToolsModal {
+ fn focus_handle(&self, _cx: &App) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl EventEmitter<DismissEvent> for ConfigureContextServerToolsModal {}
+
+impl Render for ConfigureContextServerToolsModal {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .key_context("ContextServerToolsModal")
+ .occlude()
+ .elevation_3(cx)
+ .w(rems(34.))
+ .on_action(cx.listener(Self::cancel))
+ .track_focus(&self.focus_handle)
+ .child(
+ Modal::new("configure-context-server-tools", None::<ScrollHandle>)
+ .header(
+ ModalHeader::new()
+ .headline(format!("Tools from {}", self.context_server_id.0))
+ .show_dismiss_button(true),
+ )
+ .child(self.render_modal_content(window, cx)),
+ )
+ }
+}