configure_context_server_modal.rs

  1use std::{
  2    sync::{Arc, Mutex},
  3    time::Duration,
  4};
  5
  6use anyhow::Context as _;
  7use context_server::manager::{ContextServerManager, ContextServerStatus};
  8use editor::{Editor, EditorElement, EditorStyle};
  9use extension::ContextServerConfiguration;
 10use gpui::{
 11    Animation, AnimationExt, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
 12    TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, percentage,
 13};
 14use language::{Language, LanguageRegistry};
 15use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 16use notifications::status_toast::{StatusToast, ToastIcon};
 17use settings::{Settings as _, update_settings_file};
 18use theme::ThemeSettings;
 19use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*};
 20use util::ResultExt;
 21use workspace::{ModalView, Workspace};
 22
 23pub(crate) struct ConfigureContextServerModal {
 24    workspace: WeakEntity<Workspace>,
 25    context_servers_to_setup: Vec<ConfigureContextServer>,
 26    context_server_manager: Entity<ContextServerManager>,
 27}
 28
 29struct ConfigureContextServer {
 30    id: Arc<str>,
 31    installation_instructions: Entity<markdown::Markdown>,
 32    settings_validator: Option<jsonschema::Validator>,
 33    settings_editor: Entity<Editor>,
 34    last_error: Option<SharedString>,
 35    waiting_for_context_server: bool,
 36}
 37
 38impl ConfigureContextServerModal {
 39    pub fn new(
 40        configurations: impl Iterator<Item = (Arc<str>, ContextServerConfiguration)>,
 41        jsonc_language: Option<Arc<Language>>,
 42        context_server_manager: Entity<ContextServerManager>,
 43        language_registry: Arc<LanguageRegistry>,
 44        workspace: WeakEntity<Workspace>,
 45        window: &mut Window,
 46        cx: &mut App,
 47    ) -> Option<Self> {
 48        let context_servers_to_setup = configurations
 49            .map(|(id, manifest)| {
 50                let jsonc_language = jsonc_language.clone();
 51                let settings_validator = jsonschema::validator_for(&manifest.settings_schema)
 52                    .context("Failed to load JSON schema for context server settings")
 53                    .log_err();
 54                ConfigureContextServer {
 55                    id: id.clone(),
 56                    installation_instructions: cx.new(|cx| {
 57                        Markdown::new(
 58                            manifest.installation_instructions.clone().into(),
 59                            Some(language_registry.clone()),
 60                            None,
 61                            cx,
 62                        )
 63                    }),
 64                    settings_validator,
 65                    settings_editor: cx.new(|cx| {
 66                        let mut editor = Editor::auto_height(16, window, cx);
 67                        editor.set_text(manifest.default_settings.trim(), window, cx);
 68                        editor.set_show_gutter(false, cx);
 69                        editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
 70                        if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
 71                            buffer.update(cx, |buffer, cx| buffer.set_language(jsonc_language, cx))
 72                        }
 73                        editor
 74                    }),
 75                    waiting_for_context_server: false,
 76                    last_error: None,
 77                }
 78            })
 79            .collect::<Vec<_>>();
 80
 81        if context_servers_to_setup.is_empty() {
 82            return None;
 83        }
 84
 85        Some(Self {
 86            workspace,
 87            context_servers_to_setup,
 88            context_server_manager,
 89        })
 90    }
 91}
 92
 93impl ConfigureContextServerModal {
 94    pub fn confirm(&mut self, cx: &mut Context<Self>) {
 95        if self.context_servers_to_setup.is_empty() {
 96            return;
 97        }
 98
 99        let Some(workspace) = self.workspace.upgrade() else {
100            return;
101        };
102
103        let configuration = &mut self.context_servers_to_setup[0];
104        configuration.last_error.take();
105        if configuration.waiting_for_context_server {
106            return;
107        }
108
109        let settings_value = match serde_json_lenient::from_str::<serde_json::Value>(
110            &configuration.settings_editor.read(cx).text(cx),
111        ) {
112            Ok(value) => value,
113            Err(error) => {
114                configuration.last_error = Some(error.to_string().into());
115                cx.notify();
116                return;
117            }
118        };
119
120        if let Some(validator) = configuration.settings_validator.as_ref() {
121            if let Err(error) = validator.validate(&settings_value) {
122                configuration.last_error = Some(error.to_string().into());
123                cx.notify();
124                return;
125            }
126        }
127        let id = configuration.id.clone();
128
129        let settings_changed = context_server::ContextServerSettings::get_global(cx)
130            .context_servers
131            .get(&id)
132            .map_or(true, |config| {
133                config.settings.as_ref() != Some(&settings_value)
134            });
135
136        let is_running = self.context_server_manager.read(cx).status_for_server(&id)
137            == Some(ContextServerStatus::Running);
138
139        if !settings_changed && is_running {
140            self.complete_setup(id, cx);
141            return;
142        }
143
144        configuration.waiting_for_context_server = true;
145
146        let task = wait_for_context_server(&self.context_server_manager, id.clone(), cx);
147        cx.spawn({
148            let id = id.clone();
149            async move |this, cx| {
150                let result = task.await;
151                this.update(cx, |this, cx| match result {
152                    Ok(_) => {
153                        this.complete_setup(id, cx);
154                    }
155                    Err(err) => {
156                        if let Some(configuration) = this.context_servers_to_setup.get_mut(0) {
157                            configuration.last_error = Some(err.into());
158                            configuration.waiting_for_context_server = false;
159                        } else {
160                            this.dismiss(cx);
161                        }
162                        cx.notify();
163                    }
164                })
165            }
166        })
167        .detach();
168
169        // When we write the settings to the file, the context server will be restarted.
170        update_settings_file::<context_server::ContextServerSettings>(
171            workspace.read(cx).app_state().fs.clone(),
172            cx,
173            {
174                let id = id.clone();
175                |settings, _| {
176                    if let Some(server_config) = settings.context_servers.get_mut(&id) {
177                        server_config.settings = Some(settings_value);
178                    } else {
179                        settings.context_servers.insert(
180                            id,
181                            context_server::ServerConfig {
182                                settings: Some(settings_value),
183                                ..Default::default()
184                            },
185                        );
186                    }
187                }
188            },
189        );
190    }
191
192    fn complete_setup(&mut self, id: Arc<str>, cx: &mut Context<Self>) {
193        self.context_servers_to_setup.remove(0);
194        cx.notify();
195
196        if !self.context_servers_to_setup.is_empty() {
197            return;
198        }
199
200        self.workspace
201            .update(cx, {
202                |workspace, cx| {
203                    let status_toast = StatusToast::new(
204                        format!("{} configured successfully.", id),
205                        cx,
206                        |this, _cx| {
207                            this.icon(ToastIcon::new(IconName::Hammer).color(Color::Muted))
208                                .action("Dismiss", |_, _| {})
209                        },
210                    );
211
212                    workspace.toggle_status_toast(status_toast, cx);
213                }
214            })
215            .log_err();
216
217        self.dismiss(cx);
218    }
219
220    fn dismiss(&self, cx: &mut Context<Self>) {
221        cx.emit(DismissEvent);
222    }
223}
224
225fn wait_for_context_server(
226    context_server_manager: &Entity<ContextServerManager>,
227    context_server_id: Arc<str>,
228    cx: &mut App,
229) -> Task<Result<(), Arc<str>>> {
230    let (tx, rx) = futures::channel::oneshot::channel();
231    let tx = Arc::new(Mutex::new(Some(tx)));
232
233    let subscription = cx.subscribe(context_server_manager, move |_, event, _cx| match event {
234        context_server::manager::Event::ServerStatusChanged { server_id, status } => match status {
235            Some(ContextServerStatus::Running) => {
236                if server_id == &context_server_id {
237                    if let Some(tx) = tx.lock().unwrap().take() {
238                        let _ = tx.send(Ok(()));
239                    }
240                }
241            }
242            Some(ContextServerStatus::Error(error)) => {
243                if server_id == &context_server_id {
244                    if let Some(tx) = tx.lock().unwrap().take() {
245                        let _ = tx.send(Err(error.clone()));
246                    }
247                }
248            }
249            _ => {}
250        },
251    });
252
253    cx.spawn(async move |_cx| {
254        let result = rx.await.unwrap();
255        drop(subscription);
256        result
257    })
258}
259
260impl Render for ConfigureContextServerModal {
261    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
262        let Some(configuration) = self.context_servers_to_setup.first() else {
263            return div().child("No context servers to setup");
264        };
265
266        let focus_handle = self.focus_handle(cx);
267
268        div()
269            .elevation_3(cx)
270            .w(rems(42.))
271            .key_context("ConfigureContextServerModal")
272            .on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
273            .on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.dismiss(cx)))
274            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
275                this.focus_handle(cx).focus(window);
276            }))
277            .child(
278                Modal::new("configure-context-server", None)
279                    .header(ModalHeader::new().headline(format!("Configure {}", configuration.id)))
280                    .section(
281                        Section::new()
282                            .child(div().pb_2().text_sm().child(MarkdownElement::new(
283                                configuration.installation_instructions.clone(),
284                                default_markdown_style(window, cx),
285                            )))
286                            .child(
287                                div()
288                                    .p_2()
289                                    .rounded_md()
290                                    .border_1()
291                                    .border_color(cx.theme().colors().border_variant)
292                                    .bg(cx.theme().colors().editor_background)
293                                    .gap_1()
294                                    .child({
295                                        let settings = ThemeSettings::get_global(cx);
296                                        let text_style = TextStyle {
297                                            color: cx.theme().colors().text,
298                                            font_family: settings.buffer_font.family.clone(),
299                                            font_fallbacks: settings.buffer_font.fallbacks.clone(),
300                                            font_size: settings.buffer_font_size(cx).into(),
301                                            font_weight: settings.buffer_font.weight,
302                                            line_height: relative(
303                                                settings.buffer_line_height.value(),
304                                            ),
305                                            ..Default::default()
306                                        };
307                                        EditorElement::new(
308                                            &configuration.settings_editor,
309                                            EditorStyle {
310                                                background: cx.theme().colors().editor_background,
311                                                local_player: cx.theme().players().local(),
312                                                text: text_style,
313                                                syntax: cx.theme().syntax().clone(),
314                                                ..Default::default()
315                                            },
316                                        )
317                                    })
318                                    .when_some(configuration.last_error.clone(), |this, error| {
319                                        this.child(
320                                            h_flex()
321                                                .gap_2()
322                                                .px_2()
323                                                .py_1()
324                                                .child(
325                                                    Icon::new(IconName::Warning)
326                                                        .size(IconSize::XSmall)
327                                                        .color(Color::Warning),
328                                                )
329                                                .child(
330                                                    div().w_full().child(
331                                                        Label::new(error)
332                                                            .size(LabelSize::Small)
333                                                            .color(Color::Muted),
334                                                    ),
335                                                ),
336                                        )
337                                    }),
338                            )
339                            .when(configuration.waiting_for_context_server, |this| {
340                                this.child(
341                                    h_flex()
342                                        .gap_1p5()
343                                        .child(
344                                            Icon::new(IconName::ArrowCircle)
345                                                .size(IconSize::XSmall)
346                                                .color(Color::Info)
347                                                .with_animation(
348                                                    "arrow-circle",
349                                                    Animation::new(Duration::from_secs(2)).repeat(),
350                                                    |icon, delta| {
351                                                        icon.transform(Transformation::rotate(
352                                                            percentage(delta),
353                                                        ))
354                                                    },
355                                                )
356                                                .into_any_element(),
357                                        )
358                                        .child(
359                                            Label::new("Waiting for Context Server")
360                                                .size(LabelSize::Small)
361                                                .color(Color::Muted),
362                                        ),
363                                )
364                            }),
365                    )
366                    .footer(
367                        ModalFooter::new().end_slot(
368                            h_flex()
369                                .gap_1()
370                                .child(
371                                    Button::new("cancel", "Cancel")
372                                        .key_binding(
373                                            KeyBinding::for_action_in(
374                                                &menu::Cancel,
375                                                &focus_handle,
376                                                window,
377                                                cx,
378                                            )
379                                            .map(|kb| kb.size(rems_from_px(12.))),
380                                        )
381                                        .on_click(cx.listener(|this, _event, _window, cx| {
382                                            this.dismiss(cx)
383                                        })),
384                                )
385                                .child(
386                                    Button::new("configure-server", "Configure MCP")
387                                        .disabled(configuration.waiting_for_context_server)
388                                        .key_binding(
389                                            KeyBinding::for_action_in(
390                                                &menu::Confirm,
391                                                &focus_handle,
392                                                window,
393                                                cx,
394                                            )
395                                            .map(|kb| kb.size(rems_from_px(12.))),
396                                        )
397                                        .on_click(cx.listener(|this, _event, _window, cx| {
398                                            this.confirm(cx)
399                                        })),
400                                ),
401                        ),
402                    ),
403            )
404    }
405}
406
407pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
408    let theme_settings = ThemeSettings::get_global(cx);
409    let colors = cx.theme().colors();
410    let mut text_style = window.text_style();
411    text_style.refine(&TextStyleRefinement {
412        font_family: Some(theme_settings.ui_font.family.clone()),
413        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
414        font_features: Some(theme_settings.ui_font.features.clone()),
415        font_size: Some(TextSize::XSmall.rems(cx).into()),
416        color: Some(colors.text_muted),
417        ..Default::default()
418    });
419
420    MarkdownStyle {
421        base_text_style: text_style.clone(),
422        selection_background_color: cx.theme().players().local().selection,
423        link: TextStyleRefinement {
424            background_color: Some(colors.editor_foreground.opacity(0.025)),
425            underline: Some(UnderlineStyle {
426                color: Some(colors.text_accent.opacity(0.5)),
427                thickness: px(1.),
428                ..Default::default()
429            }),
430            ..Default::default()
431        },
432        ..Default::default()
433    }
434}
435
436impl ModalView for ConfigureContextServerModal {}
437impl EventEmitter<DismissEvent> for ConfigureContextServerModal {}
438impl Focusable for ConfigureContextServerModal {
439    fn focus_handle(&self, cx: &App) -> FocusHandle {
440        if let Some(current) = self.context_servers_to_setup.first() {
441            current.settings_editor.read(cx).focus_handle(cx)
442        } else {
443            cx.focus_handle()
444        }
445    }
446}