assistant2: Add modal for adding context servers (#27434)

Marshall Bowers created

This PR adds a modal for adding context servers from the Assistant 2
configuration view:

<img width="1394" alt="Screenshot 2025-03-25 at 12 22 50 PM"
src="https://github.com/user-attachments/assets/52fe194f-7d88-4f3b-aee1-8c6385136e6b"
/>

Release Notes:

- N/A

Change summary

crates/assistant2/src/assistant.rs                                        |   3 
crates/assistant2/src/assistant_configuration.rs                          |  15 
crates/assistant2/src/assistant_configuration/add_context_server_modal.rs | 148 
3 files changed, 161 insertions(+), 5 deletions(-)

Detailed changes

crates/assistant2/src/assistant.rs 🔗

@@ -32,6 +32,7 @@ use prompt_store::PromptBuilder;
 use settings::Settings as _;
 
 pub use crate::active_thread::ActiveThread;
+use crate::assistant_configuration::AddContextServerModal;
 pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
 pub use crate::inline_assistant::InlineAssistant;
 pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
@@ -46,6 +47,7 @@ actions!(
         RemoveAllContext,
         OpenHistory,
         OpenConfiguration,
+        AddContextServer,
         RemoveSelectedThread,
         Chat,
         ChatMode,
@@ -86,6 +88,7 @@ pub fn init(
         client.telemetry().clone(),
         cx,
     );
+    cx.observe_new(AddContextServerModal::register).detach();
 
     feature_gate_assistant2_actions(cx);
 }

crates/assistant2/src/assistant_configuration.rs 🔗

@@ -1,3 +1,5 @@
+mod add_context_server_modal;
+
 use std::sync::Arc;
 
 use assistant_tool::{ToolSource, ToolWorkingSet};
@@ -5,12 +7,14 @@ use collections::HashMap;
 use context_server::manager::ContextServerManager;
 use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
 use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
-use ui::{
-    prelude::*, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, Tooltip,
-};
+use ui::{prelude::*, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch};
 use util::ResultExt as _;
 use zed_actions::ExtensionCategoryFilter;
 
+pub(crate) use add_context_server_modal::AddContextServerModal;
+
+use crate::AddContextServer;
+
 pub struct AssistantConfiguration {
     focus_handle: FocusHandle,
     configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
@@ -307,8 +311,9 @@ impl AssistantConfiguration {
                                 .icon(IconName::Plus)
                                 .icon_size(IconSize::Small)
                                 .icon_position(IconPosition::Start)
-                                .disabled(true)
-                                .tooltip(Tooltip::text("Not yet implemented")),
+                                .on_click(|_event, window, cx| {
+                                    window.dispatch_action(AddContextServer.boxed_clone(), cx)
+                                }),
                         ),
                     )
                     .child(

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

@@ -0,0 +1,148 @@
+use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
+use editor::Editor;
+use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity};
+use serde_json::json;
+use settings::update_settings_file;
+use ui::{prelude::*, Modal, ModalFooter, ModalHeader, Section};
+use workspace::{ModalView, Workspace};
+
+use crate::AddContextServer;
+
+pub struct AddContextServerModal {
+    workspace: WeakEntity<Workspace>,
+    name_editor: Entity<Editor>,
+    command_editor: Entity<Editor>,
+}
+
+impl AddContextServerModal {
+    pub fn register(
+        workspace: &mut Workspace,
+        _window: Option<&mut Window>,
+        _cx: &mut Context<Workspace>,
+    ) {
+        workspace.register_action(|workspace, _: &AddContextServer, window, cx| {
+            let workspace_handle = cx.entity().downgrade();
+            workspace.toggle_modal(window, cx, |window, cx| {
+                Self::new(workspace_handle, window, cx)
+            })
+        });
+    }
+
+    pub fn new(
+        workspace: WeakEntity<Workspace>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let name_editor = cx.new(|cx| Editor::single_line(window, cx));
+        let command_editor = cx.new(|cx| Editor::single_line(window, cx));
+
+        name_editor.update(cx, |editor, cx| {
+            editor.set_placeholder_text("Context server name", cx);
+        });
+
+        command_editor.update(cx, |editor, cx| {
+            editor.set_placeholder_text("Command to run the context server", cx);
+        });
+
+        Self {
+            name_editor,
+            command_editor,
+            workspace,
+        }
+    }
+
+    fn confirm(&mut self, cx: &mut Context<Self>) {
+        let name = self.name_editor.read(cx).text(cx).trim().to_string();
+        let command = self.command_editor.read(cx).text(cx).trim().to_string();
+
+        if name.is_empty() || command.is_empty() {
+            return;
+        }
+
+        let mut command_parts = command.split(' ').map(|part| part.trim().to_string());
+        let Some(path) = command_parts.next() else {
+            return;
+        };
+        let args = command_parts.collect::<Vec<_>>();
+
+        if let Some(workspace) = self.workspace.upgrade() {
+            workspace.update(cx, |workspace, cx| {
+                let fs = workspace.app_state().fs.clone();
+                update_settings_file::<ContextServerSettings>(fs.clone(), cx, |settings, _| {
+                    settings.context_servers.insert(
+                        name.into(),
+                        ServerConfig {
+                            command: Some(ServerCommand {
+                                path,
+                                args,
+                                env: None,
+                            }),
+                            settings: Some(json!({})),
+                        },
+                    );
+                });
+            });
+        }
+
+        cx.emit(DismissEvent);
+    }
+
+    fn cancel(&mut self, cx: &mut Context<Self>) {
+        cx.emit(DismissEvent);
+    }
+}
+
+impl ModalView for AddContextServerModal {}
+
+impl Focusable for AddContextServerModal {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        self.name_editor.focus_handle(cx).clone()
+    }
+}
+
+impl EventEmitter<DismissEvent> for AddContextServerModal {}
+
+impl Render for AddContextServerModal {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        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)))
+            .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"))
+                    .section(
+                        Section::new()
+                            .child(
+                                v_flex()
+                                    .gap_1()
+                                    .child(Label::new("Name"))
+                                    .child(self.name_editor.clone()),
+                            )
+                            .child(
+                                v_flex()
+                                    .gap_1()
+                                    .child(Label::new("Command"))
+                                    .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)),
+                                ),
+                            )
+                            .end_slot(Button::new("add-server", "Add Server").on_click(
+                                cx.listener(|this, _event, _window, cx| this.confirm(cx)),
+                            )),
+                    ),
+            )
+    }
+}