agent: Add item to add custom MCP server in the panel's menu (#29091)

This is based on user feedback that the Agent Panel menu was only
linking to extensions as a way to add MCP servers while we also support
adding "custom" servers, too, which don't go through the extensions
flow.

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2025-04-19 12:09:50 -03:00 committed by GitHub
parent f0ef3110d3
commit cc2fcb2f42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 59 additions and 23 deletions

View file

@ -404,7 +404,7 @@ impl AssistantConfiguration {
.gap_2() .gap_2()
.child( .child(
h_flex().w_full().child( h_flex().w_full().child(
Button::new("add-context-server", "Add MCPs Directly") Button::new("add-context-server", "Add Custom Server")
.style(ButtonStyle::Filled) .style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface) .layer(ElevationIndex::ModalSurface)
.full_width() .full_width()

View file

@ -2,7 +2,7 @@ use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*}; use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
use serde_json::json; use serde_json::json;
use settings::update_settings_file; use settings::update_settings_file;
use ui::{Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*}; use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use ui_input::SingleLineInput; use ui_input::SingleLineInput;
use workspace::{ModalView, Workspace}; use workspace::{ModalView, Workspace};
@ -34,9 +34,9 @@ impl AddContextServerModal {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let name_editor = let name_editor =
cx.new(|cx| SingleLineInput::new(window, cx, "Your server name").label("Name")); cx.new(|cx| SingleLineInput::new(window, cx, "my-custom-server").label("Name"));
let command_editor = cx.new(|cx| { let command_editor = cx.new(|cx| {
SingleLineInput::new(window, cx, "Command").label("Command to run the context server") SingleLineInput::new(window, cx, "Command").label("Command to run the MCP server")
}); });
Self { Self {
@ -46,7 +46,7 @@ impl AddContextServerModal {
} }
} }
fn confirm(&mut self, cx: &mut Context<Self>) { fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
let name = self let name = self
.name_editor .name_editor
.read(cx) .read(cx)
@ -96,7 +96,7 @@ impl AddContextServerModal {
cx.emit(DismissEvent); cx.emit(DismissEvent);
} }
fn cancel(&mut self, cx: &mut Context<Self>) { fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
cx.emit(DismissEvent); cx.emit(DismissEvent);
} }
} }
@ -112,38 +112,68 @@ impl Focusable for AddContextServerModal {
impl EventEmitter<DismissEvent> for AddContextServerModal {} impl EventEmitter<DismissEvent> for AddContextServerModal {}
impl Render for AddContextServerModal { impl Render for AddContextServerModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_name_empty = self.name_editor.read(cx).is_empty(cx); let is_name_empty = self.name_editor.read(cx).is_empty(cx);
let is_command_empty = self.command_editor.read(cx).is_empty(cx); let is_command_empty = self.command_editor.read(cx).is_empty(cx);
let focus_handle = self.focus_handle(cx);
div() div()
.elevation_3(cx) .elevation_3(cx)
.w(rems(34.)) .w(rems(34.))
.key_context("AddContextServerModal") .key_context("AddContextServerModal")
.on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(cx))) .on_action(
.on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx))) cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
)
.on_action(
cx.listener(|this, _: &menu::Confirm, _window, cx| {
this.confirm(&menu::Confirm, cx)
}),
)
.capture_any_mouse_down(cx.listener(|this, _, window, cx| { .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window); this.focus_handle(cx).focus(window);
})) }))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent))) .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child( .child(
Modal::new("add-context-server", None) Modal::new("add-context-server", None)
.header(ModalHeader::new().headline("Add Context Server")) .header(ModalHeader::new().headline("Add MCP Server"))
.section( .section(
Section::new() Section::new().child(
.child(self.name_editor.clone()) v_flex()
.child(self.command_editor.clone()), .gap_2()
.child(self.name_editor.clone())
.child(self.command_editor.clone()),
),
) )
.footer( .footer(
ModalFooter::new() ModalFooter::new()
.start_slot( .start_slot(
Button::new("cancel", "Cancel").on_click( Button::new("cancel", "Cancel")
cx.listener(|this, _event, _window, cx| this.cancel(cx)), .key_binding(
), KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(cx.listener(|this, _event, _window, cx| {
this.cancel(&menu::Cancel, cx)
})),
) )
.end_slot( .end_slot(
Button::new("add-server", "Add Server") Button::new("add-server", "Add Server")
.disabled(is_name_empty || is_command_empty) .disabled(is_name_empty || is_command_empty)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.map(|button| { .map(|button| {
if is_name_empty { if is_name_empty {
button.tooltip(Tooltip::text("Name is required")) button.tooltip(Tooltip::text("Name is required"))
@ -153,9 +183,9 @@ impl Render for AddContextServerModal {
button button
} }
}) })
.on_click( .on_click(cx.listener(|this, _event, _window, cx| {
cx.listener(|this, _event, _window, cx| this.confirm(cx)), this.confirm(&menu::Confirm, cx)
), })),
), ),
), ),
) )

View file

@ -47,7 +47,7 @@ use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use crate::ui::UsageBanner; use crate::ui::UsageBanner;
use crate::{ use crate::{
AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread, AddContextServer, AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
}; };
@ -1123,14 +1123,16 @@ impl AssistantPanel {
.action("Prompt Library", Box::new(OpenPromptLibrary::default())) .action("Prompt Library", Box::new(OpenPromptLibrary::default()))
.action("Settings", Box::new(OpenConfiguration)) .action("Settings", Box::new(OpenConfiguration))
.separator() .separator()
.header("MCPs")
.action( .action(
"Install MCPs", "View Server Extensions",
Box::new(zed_actions::Extensions { Box::new(zed_actions::Extensions {
category_filter: Some( category_filter: Some(
zed_actions::ExtensionCategoryFilter::ContextServers, zed_actions::ExtensionCategoryFilter::ContextServers,
), ),
}), }),
) )
.action("Add Custom Server", Box::new(AddContextServer))
}, },
)) ))
}), }),

View file

@ -249,10 +249,14 @@ impl ModalFooter {
impl RenderOnce for ModalFooter { impl RenderOnce for ModalFooter {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex() h_flex()
.flex_none()
.w_full() .w_full()
.mt_4()
.p(DynamicSpacing::Base08.rems(cx)) .p(DynamicSpacing::Base08.rems(cx))
.justify_between() .flex_none()
.justify_end()
.gap_1()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot))) .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot)))
.child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot))) .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot)))
} }