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

Danilo Leal created

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

Change summary

crates/agent/src/assistant_configuration.rs                          |  2 
crates/agent/src/assistant_configuration/add_context_server_modal.rs | 66 
crates/agent/src/assistant_panel.rs                                  |  6 
crates/ui/src/components/modal.rs                                    |  8 
4 files changed, 59 insertions(+), 23 deletions(-)

Detailed changes

crates/agent/src/assistant_configuration.rs 🔗

@@ -404,7 +404,7 @@ impl AssistantConfiguration {
                     .gap_2()
                     .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)
                                 .layer(ElevationIndex::ModalSurface)
                                 .full_width()

crates/agent/src/assistant_configuration/add_context_server_modal.rs 🔗

@@ -2,7 +2,7 @@ use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
 use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
 use serde_json::json;
 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 workspace::{ModalView, Workspace};
 
@@ -34,9 +34,9 @@ impl AddContextServerModal {
         cx: &mut Context<Self>,
     ) -> Self {
         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| {
-            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 {
@@ -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
             .name_editor
             .read(cx)
@@ -96,7 +96,7 @@ impl AddContextServerModal {
         cx.emit(DismissEvent);
     }
 
-    fn cancel(&mut self, cx: &mut Context<Self>) {
+    fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
         cx.emit(DismissEvent);
     }
 }
@@ -112,38 +112,68 @@ impl Focusable for AddContextServerModal {
 impl EventEmitter<DismissEvent> 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_command_empty = self.command_editor.read(cx).is_empty(cx);
 
+        let focus_handle = self.focus_handle(cx);
+
         div()
             .elevation_3(cx)
             .w(rems(34.))
             .key_context("AddContextServerModal")
-            .on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(cx)))
-            .on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
+            .on_action(
+                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| {
                 this.focus_handle(cx).focus(window);
             }))
             .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
             .child(
                 Modal::new("add-context-server", None)
-                    .header(ModalHeader::new().headline("Add Context Server"))
+                    .header(ModalHeader::new().headline("Add MCP Server"))
                     .section(
-                        Section::new()
-                            .child(self.name_editor.clone())
-                            .child(self.command_editor.clone()),
+                        Section::new().child(
+                            v_flex()
+                                .gap_2()
+                                .child(self.name_editor.clone())
+                                .child(self.command_editor.clone()),
+                        ),
                     )
                     .footer(
                         ModalFooter::new()
                             .start_slot(
-                                Button::new("cancel", "Cancel").on_click(
-                                    cx.listener(|this, _event, _window, cx| this.cancel(cx)),
-                                ),
+                                Button::new("cancel", "Cancel")
+                                    .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(
                                 Button::new("add-server", "Add Server")
                                     .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| {
                                         if is_name_empty {
                                             button.tooltip(Tooltip::text("Name is required"))
@@ -153,9 +183,9 @@ impl Render for AddContextServerModal {
                                             button
                                         }
                                     })
-                                    .on_click(
-                                        cx.listener(|this, _event, _window, cx| this.confirm(cx)),
-                                    ),
+                                    .on_click(cx.listener(|this, _event, _window, cx| {
+                                        this.confirm(&menu::Confirm, cx)
+                                    })),
                             ),
                     ),
             )

crates/agent/src/assistant_panel.rs 🔗

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

crates/ui/src/components/modal.rs 🔗

@@ -249,10 +249,14 @@ impl ModalFooter {
 impl RenderOnce for ModalFooter {
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
         h_flex()
-            .flex_none()
             .w_full()
+            .mt_4()
             .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.end_slot, |this, end_slot| this.child(end_slot)))
     }