add_context_server_modal.rs

  1use context_server::ContextServerCommand;
  2use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
  3use project::project_settings::{ContextServerConfiguration, ProjectSettings};
  4use serde_json::json;
  5use settings::update_settings_file;
  6use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
  7use ui_input::SingleLineInput;
  8use workspace::{ModalView, Workspace};
  9
 10use crate::AddContextServer;
 11
 12pub struct AddContextServerModal {
 13    workspace: WeakEntity<Workspace>,
 14    name_editor: Entity<SingleLineInput>,
 15    command_editor: Entity<SingleLineInput>,
 16}
 17
 18impl AddContextServerModal {
 19    pub fn register(
 20        workspace: &mut Workspace,
 21        _window: Option<&mut Window>,
 22        _cx: &mut Context<Workspace>,
 23    ) {
 24        workspace.register_action(|workspace, _: &AddContextServer, window, cx| {
 25            let workspace_handle = cx.entity().downgrade();
 26            workspace.toggle_modal(window, cx, |window, cx| {
 27                Self::new(workspace_handle, window, cx)
 28            })
 29        });
 30    }
 31
 32    pub fn new(
 33        workspace: WeakEntity<Workspace>,
 34        window: &mut Window,
 35        cx: &mut Context<Self>,
 36    ) -> Self {
 37        let name_editor =
 38            cx.new(|cx| SingleLineInput::new(window, cx, "my-custom-server").label("Name"));
 39        let command_editor = cx.new(|cx| {
 40            SingleLineInput::new(window, cx, "Command").label("Command to run the MCP server")
 41        });
 42
 43        Self {
 44            name_editor,
 45            command_editor,
 46            workspace,
 47        }
 48    }
 49
 50    fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
 51        let name = self
 52            .name_editor
 53            .read(cx)
 54            .editor()
 55            .read(cx)
 56            .text(cx)
 57            .trim()
 58            .to_string();
 59        let command = self
 60            .command_editor
 61            .read(cx)
 62            .editor()
 63            .read(cx)
 64            .text(cx)
 65            .trim()
 66            .to_string();
 67
 68        if name.is_empty() || command.is_empty() {
 69            return;
 70        }
 71
 72        let mut command_parts = command.split(' ').map(|part| part.trim().to_string());
 73        let Some(path) = command_parts.next() else {
 74            return;
 75        };
 76        let args = command_parts.collect::<Vec<_>>();
 77
 78        if let Some(workspace) = self.workspace.upgrade() {
 79            workspace.update(cx, |workspace, cx| {
 80                let fs = workspace.app_state().fs.clone();
 81                update_settings_file::<ProjectSettings>(fs.clone(), cx, |settings, _| {
 82                    settings.context_servers.insert(
 83                        name.into(),
 84                        ContextServerConfiguration {
 85                            command: Some(ContextServerCommand {
 86                                path,
 87                                args,
 88                                env: None,
 89                            }),
 90                            settings: Some(json!({})),
 91                        },
 92                    );
 93                });
 94            });
 95        }
 96
 97        cx.emit(DismissEvent);
 98    }
 99
100    fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
101        cx.emit(DismissEvent);
102    }
103}
104
105impl ModalView for AddContextServerModal {}
106
107impl Focusable for AddContextServerModal {
108    fn focus_handle(&self, cx: &App) -> FocusHandle {
109        self.name_editor.focus_handle(cx).clone()
110    }
111}
112
113impl EventEmitter<DismissEvent> for AddContextServerModal {}
114
115impl Render for AddContextServerModal {
116    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
117        let is_name_empty = self.name_editor.read(cx).is_empty(cx);
118        let is_command_empty = self.command_editor.read(cx).is_empty(cx);
119
120        let focus_handle = self.focus_handle(cx);
121
122        div()
123            .elevation_3(cx)
124            .w(rems(34.))
125            .key_context("AddContextServerModal")
126            .on_action(
127                cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
128            )
129            .on_action(
130                cx.listener(|this, _: &menu::Confirm, _window, cx| {
131                    this.confirm(&menu::Confirm, cx)
132                }),
133            )
134            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
135                this.focus_handle(cx).focus(window);
136            }))
137            .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
138            .child(
139                Modal::new("add-context-server", None)
140                    .header(ModalHeader::new().headline("Add MCP Server"))
141                    .section(
142                        Section::new().child(
143                            v_flex()
144                                .gap_2()
145                                .child(self.name_editor.clone())
146                                .child(self.command_editor.clone()),
147                        ),
148                    )
149                    .footer(
150                        ModalFooter::new().end_slot(
151                            h_flex()
152                                .gap_2()
153                                .child(
154                                    Button::new("cancel", "Cancel")
155                                        .key_binding(
156                                            KeyBinding::for_action_in(
157                                                &menu::Cancel,
158                                                &focus_handle,
159                                                window,
160                                                cx,
161                                            )
162                                            .map(|kb| kb.size(rems_from_px(12.))),
163                                        )
164                                        .on_click(cx.listener(|this, _event, _window, cx| {
165                                            this.cancel(&menu::Cancel, cx)
166                                        })),
167                                )
168                                .child(
169                                    Button::new("add-server", "Add Server")
170                                        .disabled(is_name_empty || is_command_empty)
171                                        .key_binding(
172                                            KeyBinding::for_action_in(
173                                                &menu::Confirm,
174                                                &focus_handle,
175                                                window,
176                                                cx,
177                                            )
178                                            .map(|kb| kb.size(rems_from_px(12.))),
179                                        )
180                                        .map(|button| {
181                                            if is_name_empty {
182                                                button.tooltip(Tooltip::text("Name is required"))
183                                            } else if is_command_empty {
184                                                button.tooltip(Tooltip::text("Command is required"))
185                                            } else {
186                                                button
187                                            }
188                                        })
189                                        .on_click(cx.listener(|this, _event, _window, cx| {
190                                            this.confirm(&menu::Confirm, cx)
191                                        })),
192                                ),
193                        ),
194                    ),
195            )
196    }
197}