add_context_server_modal.rs

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