configure_context_server_modal.rs

  1use std::{
  2    path::PathBuf,
  3    sync::{Arc, Mutex},
  4};
  5
  6use anyhow::{Context as _, Result};
  7use collections::HashMap;
  8use context_server::{ContextServerCommand, ContextServerId};
  9use editor::{Editor, EditorElement, EditorStyle};
 10use gpui::{
 11    AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle,
 12    Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
 13};
 14use language::{Language, LanguageRegistry};
 15use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 16use notifications::status_toast::{StatusToast, ToastIcon};
 17use project::{
 18    context_server_store::{
 19        ContextServerStatus, ContextServerStore, registry::ContextServerDescriptorRegistry,
 20    },
 21    project_settings::{ContextServerSettings, ProjectSettings},
 22    worktree_store::WorktreeStore,
 23};
 24use serde::Deserialize;
 25use settings::{Settings as _, update_settings_file};
 26use theme::ThemeSettings;
 27use ui::{
 28    CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip,
 29    WithScrollbar, prelude::*,
 30};
 31use util::ResultExt as _;
 32use workspace::{ModalView, Workspace};
 33
 34use crate::AddContextServer;
 35
 36enum ConfigurationTarget {
 37    New,
 38    Existing {
 39        id: ContextServerId,
 40        command: ContextServerCommand,
 41    },
 42    ExistingHttp {
 43        id: ContextServerId,
 44        url: String,
 45        headers: HashMap<String, String>,
 46    },
 47    Extension {
 48        id: ContextServerId,
 49        repository_url: Option<SharedString>,
 50        installation: Option<extension::ContextServerConfiguration>,
 51    },
 52}
 53
 54enum ConfigurationSource {
 55    New {
 56        editor: Entity<Editor>,
 57        is_http: bool,
 58    },
 59    Existing {
 60        editor: Entity<Editor>,
 61        is_http: bool,
 62    },
 63    Extension {
 64        id: ContextServerId,
 65        editor: Option<Entity<Editor>>,
 66        repository_url: Option<SharedString>,
 67        installation_instructions: Option<Entity<markdown::Markdown>>,
 68        settings_validator: Option<jsonschema::Validator>,
 69    },
 70}
 71
 72impl ConfigurationSource {
 73    fn has_configuration_options(&self) -> bool {
 74        !matches!(self, ConfigurationSource::Extension { editor: None, .. })
 75    }
 76
 77    fn is_new(&self) -> bool {
 78        matches!(self, ConfigurationSource::New { .. })
 79    }
 80
 81    fn from_target(
 82        target: ConfigurationTarget,
 83        language_registry: Arc<LanguageRegistry>,
 84        jsonc_language: Option<Arc<Language>>,
 85        window: &mut Window,
 86        cx: &mut App,
 87    ) -> Self {
 88        fn create_editor(
 89            json: String,
 90            jsonc_language: Option<Arc<Language>>,
 91            window: &mut Window,
 92            cx: &mut App,
 93        ) -> Entity<Editor> {
 94            cx.new(|cx| {
 95                let mut editor = Editor::auto_height(4, 16, window, cx);
 96                editor.set_text(json, window, cx);
 97                editor.set_show_gutter(false, cx);
 98                editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
 99                if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
100                    buffer.update(cx, |buffer, cx| buffer.set_language(jsonc_language, cx))
101                }
102                editor
103            })
104        }
105
106        match target {
107            ConfigurationTarget::New => ConfigurationSource::New {
108                editor: create_editor(context_server_input(None), jsonc_language, window, cx),
109                is_http: false,
110            },
111            ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
112                editor: create_editor(
113                    context_server_input(Some((id, command))),
114                    jsonc_language,
115                    window,
116                    cx,
117                ),
118                is_http: false,
119            },
120            ConfigurationTarget::ExistingHttp {
121                id,
122                url,
123                headers: auth,
124            } => ConfigurationSource::Existing {
125                editor: create_editor(
126                    context_server_http_input(Some((id, url, auth))),
127                    jsonc_language,
128                    window,
129                    cx,
130                ),
131                is_http: true,
132            },
133            ConfigurationTarget::Extension {
134                id,
135                repository_url,
136                installation,
137            } => {
138                let settings_validator = installation.as_ref().and_then(|installation| {
139                    jsonschema::validator_for(&installation.settings_schema)
140                        .context("Failed to load JSON schema for context server settings")
141                        .log_err()
142                });
143                let installation_instructions = installation.as_ref().map(|installation| {
144                    cx.new(|cx| {
145                        Markdown::new(
146                            installation.installation_instructions.clone().into(),
147                            Some(language_registry.clone()),
148                            None,
149                            cx,
150                        )
151                    })
152                });
153                ConfigurationSource::Extension {
154                    id,
155                    repository_url,
156                    installation_instructions,
157                    settings_validator,
158                    editor: installation.map(|installation| {
159                        create_editor(installation.default_settings, jsonc_language, window, cx)
160                    }),
161                }
162            }
163        }
164    }
165
166    fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
167        match self {
168            ConfigurationSource::New { editor, is_http }
169            | ConfigurationSource::Existing { editor, is_http } => {
170                if *is_http {
171                    parse_http_input(&editor.read(cx).text(cx)).map(|(id, url, auth)| {
172                        (
173                            id,
174                            ContextServerSettings::Http {
175                                enabled: true,
176                                url,
177                                headers: auth,
178                            },
179                        )
180                    })
181                } else {
182                    parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
183                        (
184                            id,
185                            ContextServerSettings::Stdio {
186                                enabled: true,
187                                command,
188                            },
189                        )
190                    })
191                }
192            }
193            ConfigurationSource::Extension {
194                id,
195                editor,
196                settings_validator,
197                ..
198            } => {
199                let text = editor
200                    .as_ref()
201                    .context("No output available")?
202                    .read(cx)
203                    .text(cx);
204                let settings = serde_json_lenient::from_str::<serde_json::Value>(&text)?;
205                if let Some(settings_validator) = settings_validator
206                    && let Err(error) = settings_validator.validate(&settings)
207                {
208                    return Err(anyhow::anyhow!(error.to_string()));
209                }
210                Ok((
211                    id.clone(),
212                    ContextServerSettings::Extension {
213                        enabled: true,
214                        settings,
215                    },
216                ))
217            }
218        }
219    }
220}
221
222fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
223    let (name, command, args, env) = match existing {
224        Some((id, cmd)) => {
225            let args = serde_json::to_string(&cmd.args).unwrap();
226            let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
227            (id.0.to_string(), cmd.path, args, env)
228        }
229        None => (
230            "some-mcp-server".to_string(),
231            PathBuf::new(),
232            "[]".to_string(),
233            "{}".to_string(),
234        ),
235    };
236
237    format!(
238        r#"{{
239  /// The name of your MCP server
240  "{name}": {{
241    /// The command which runs the MCP server
242    "command": "{}",
243    /// The arguments to pass to the MCP server
244    "args": {args},
245    /// The environment variables to set
246    "env": {env}
247  }}
248}}"#,
249        command.display()
250    )
251}
252
253fn context_server_http_input(
254    existing: Option<(ContextServerId, String, HashMap<String, String>)>,
255) -> String {
256    let (name, url, headers) = match existing {
257        Some((id, url, headers)) => {
258            let header = if headers.is_empty() {
259                r#"// "Authorization": "Bearer <token>"#.to_string()
260            } else {
261                let json = serde_json::to_string_pretty(&headers).unwrap();
262                let mut lines = json.split("\n").collect::<Vec<_>>();
263                if lines.len() > 1 {
264                    lines.remove(0);
265                    lines.pop();
266                }
267                lines
268                    .into_iter()
269                    .map(|line| format!("  {}", line))
270                    .collect::<String>()
271            };
272            (id.0.to_string(), url, header)
273        }
274        None => (
275            "some-remote-server".to_string(),
276            "https://example.com/mcp".to_string(),
277            r#"// "Authorization": "Bearer <token>"#.to_string(),
278        ),
279    };
280
281    format!(
282        r#"{{
283  /// The name of your remote MCP server
284  "{name}": {{
285    /// The URL of the remote MCP server
286    "url": "{url}",
287    "headers": {{
288     /// Any headers to send along
289     {headers}
290    }}
291  }}
292}}"#
293    )
294}
295
296fn parse_http_input(text: &str) -> Result<(ContextServerId, String, HashMap<String, String>)> {
297    #[derive(Deserialize)]
298    struct Temp {
299        url: String,
300        #[serde(default)]
301        headers: HashMap<String, String>,
302    }
303    let value: HashMap<String, Temp> = serde_json_lenient::from_str(text)?;
304    if value.len() != 1 {
305        anyhow::bail!("Expected exactly one context server configuration");
306    }
307
308    let (key, value) = value.into_iter().next().unwrap();
309
310    Ok((ContextServerId(key.into()), value.url, value.headers))
311}
312
313fn resolve_context_server_extension(
314    id: ContextServerId,
315    worktree_store: Entity<WorktreeStore>,
316    cx: &mut App,
317) -> Task<Option<ConfigurationTarget>> {
318    let registry = ContextServerDescriptorRegistry::default_global(cx).read(cx);
319
320    let Some(descriptor) = registry.context_server_descriptor(&id.0) else {
321        return Task::ready(None);
322    };
323
324    let extension = crate::agent_configuration::resolve_extension_for_context_server(&id, cx);
325    cx.spawn(async move |cx| {
326        let installation = descriptor
327            .configuration(worktree_store, cx)
328            .await
329            .context("Failed to resolve context server configuration")
330            .log_err()
331            .flatten();
332
333        Some(ConfigurationTarget::Extension {
334            id,
335            repository_url: extension
336                .and_then(|(_, manifest)| manifest.repository.clone().map(SharedString::from)),
337            installation,
338        })
339    })
340}
341
342enum State {
343    Idle,
344    Waiting,
345    Error(SharedString),
346}
347
348pub struct ConfigureContextServerModal {
349    context_server_store: Entity<ContextServerStore>,
350    workspace: WeakEntity<Workspace>,
351    source: ConfigurationSource,
352    state: State,
353    original_server_id: Option<ContextServerId>,
354    scroll_handle: ScrollHandle,
355}
356
357impl ConfigureContextServerModal {
358    pub fn register(
359        workspace: &mut Workspace,
360        language_registry: Arc<LanguageRegistry>,
361        _window: Option<&mut Window>,
362        _cx: &mut Context<Workspace>,
363    ) {
364        workspace.register_action({
365            move |_workspace, _: &AddContextServer, window, cx| {
366                let workspace_handle = cx.weak_entity();
367                let language_registry = language_registry.clone();
368                window
369                    .spawn(cx, async move |cx| {
370                        Self::show_modal(
371                            ConfigurationTarget::New,
372                            language_registry,
373                            workspace_handle,
374                            cx,
375                        )
376                        .await
377                    })
378                    .detach_and_log_err(cx);
379            }
380        });
381    }
382
383    pub fn show_modal_for_existing_server(
384        server_id: ContextServerId,
385        language_registry: Arc<LanguageRegistry>,
386        workspace: WeakEntity<Workspace>,
387        window: &mut Window,
388        cx: &mut App,
389    ) -> Task<Result<()>> {
390        let Some(settings) = ProjectSettings::get_global(cx)
391            .context_servers
392            .get(&server_id.0)
393            .cloned()
394            .or_else(|| {
395                ContextServerDescriptorRegistry::default_global(cx)
396                    .read(cx)
397                    .context_server_descriptor(&server_id.0)
398                    .map(|_| ContextServerSettings::default_extension())
399            })
400        else {
401            return Task::ready(Err(anyhow::anyhow!("Context server not found")));
402        };
403
404        window.spawn(cx, async move |cx| {
405            let target = match settings {
406                ContextServerSettings::Stdio {
407                    enabled: _,
408                    command,
409                } => Some(ConfigurationTarget::Existing {
410                    id: server_id,
411                    command,
412                }),
413                ContextServerSettings::Http {
414                    enabled: _,
415                    url,
416                    headers,
417                } => Some(ConfigurationTarget::ExistingHttp {
418                    id: server_id,
419                    url,
420                    headers,
421                }),
422                ContextServerSettings::Extension { .. } => {
423                    match workspace
424                        .update(cx, |workspace, cx| {
425                            resolve_context_server_extension(
426                                server_id,
427                                workspace.project().read(cx).worktree_store(),
428                                cx,
429                            )
430                        })
431                        .ok()
432                    {
433                        Some(task) => task.await,
434                        None => None,
435                    }
436                }
437            };
438
439            match target {
440                Some(target) => Self::show_modal(target, language_registry, workspace, cx).await,
441                None => Err(anyhow::anyhow!("Failed to resolve context server")),
442            }
443        })
444    }
445
446    fn show_modal(
447        target: ConfigurationTarget,
448        language_registry: Arc<LanguageRegistry>,
449        workspace: WeakEntity<Workspace>,
450        cx: &mut AsyncWindowContext,
451    ) -> Task<Result<()>> {
452        cx.spawn(async move |cx| {
453            let jsonc_language = language_registry.language_for_name("jsonc").await.ok();
454            workspace.update_in(cx, |workspace, window, cx| {
455                let workspace_handle = cx.weak_entity();
456                let context_server_store = workspace.project().read(cx).context_server_store();
457                workspace.toggle_modal(window, cx, |window, cx| Self {
458                    context_server_store,
459                    workspace: workspace_handle,
460                    state: State::Idle,
461                    original_server_id: match &target {
462                        ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
463                        ConfigurationTarget::ExistingHttp { id, .. } => Some(id.clone()),
464                        ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
465                        ConfigurationTarget::New => None,
466                    },
467                    source: ConfigurationSource::from_target(
468                        target,
469                        language_registry,
470                        jsonc_language,
471                        window,
472                        cx,
473                    ),
474                    scroll_handle: ScrollHandle::new(),
475                })
476            })
477        })
478    }
479
480    fn set_error(&mut self, err: impl Into<SharedString>, cx: &mut Context<Self>) {
481        self.state = State::Error(err.into());
482        cx.notify();
483    }
484
485    fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
486        self.state = State::Idle;
487        let Some(workspace) = self.workspace.upgrade() else {
488            return;
489        };
490
491        let (id, settings) = match self.source.output(cx) {
492            Ok(val) => val,
493            Err(error) => {
494                self.set_error(error.to_string(), cx);
495                return;
496            }
497        };
498
499        self.state = State::Waiting;
500
501        let existing_server = self.context_server_store.read(cx).get_running_server(&id);
502        if existing_server.is_some() {
503            self.context_server_store.update(cx, |store, cx| {
504                store.stop_server(&id, cx).log_err();
505            });
506        }
507
508        let wait_for_context_server_task =
509            wait_for_context_server(&self.context_server_store, id.clone(), cx);
510        cx.spawn({
511            let id = id.clone();
512            async move |this, cx| {
513                let result = wait_for_context_server_task.await;
514                this.update(cx, |this, cx| match result {
515                    Ok(_) => {
516                        this.state = State::Idle;
517                        this.show_configured_context_server_toast(id, cx);
518                        cx.emit(DismissEvent);
519                    }
520                    Err(err) => {
521                        this.set_error(err, cx);
522                    }
523                })
524            }
525        })
526        .detach();
527
528        let settings_changed =
529            ProjectSettings::get_global(cx).context_servers.get(&id.0) != Some(&settings);
530
531        if settings_changed {
532            // When we write the settings to the file, the context server will be restarted.
533            workspace.update(cx, |workspace, cx| {
534                let fs = workspace.app_state().fs.clone();
535                let original_server_id = self.original_server_id.clone();
536                update_settings_file(fs.clone(), cx, move |current, _| {
537                    if let Some(original_id) = original_server_id {
538                        if original_id != id {
539                            current.project.context_servers.remove(&original_id.0);
540                        }
541                    }
542                    current
543                        .project
544                        .context_servers
545                        .insert(id.0, settings.into());
546                });
547            });
548        } else if let Some(existing_server) = existing_server {
549            self.context_server_store
550                .update(cx, |store, cx| store.start_server(existing_server, cx));
551        }
552    }
553
554    fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
555        cx.emit(DismissEvent);
556    }
557
558    fn show_configured_context_server_toast(&self, id: ContextServerId, cx: &mut App) {
559        self.workspace
560            .update(cx, {
561                |workspace, cx| {
562                    let status_toast = StatusToast::new(
563                        format!("{} configured successfully.", id.0),
564                        cx,
565                        |this, _cx| {
566                            this.icon(ToastIcon::new(IconName::ToolHammer).color(Color::Muted))
567                                .action("Dismiss", |_, _| {})
568                        },
569                    );
570
571                    workspace.toggle_status_toast(status_toast, cx);
572                }
573            })
574            .log_err();
575    }
576}
577
578fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
579    let value: serde_json::Value = serde_json_lenient::from_str(text)?;
580    let object = value.as_object().context("Expected object")?;
581    anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
582    let (context_server_name, value) = object.into_iter().next().unwrap();
583    let command: ContextServerCommand = serde_json::from_value(value.clone())?;
584    Ok((ContextServerId(context_server_name.clone().into()), command))
585}
586
587impl ModalView for ConfigureContextServerModal {}
588
589impl Focusable for ConfigureContextServerModal {
590    fn focus_handle(&self, cx: &App) -> FocusHandle {
591        match &self.source {
592            ConfigurationSource::New { editor, .. } => editor.focus_handle(cx),
593            ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx),
594            ConfigurationSource::Extension { editor, .. } => editor
595                .as_ref()
596                .map(|editor| editor.focus_handle(cx))
597                .unwrap_or_else(|| cx.focus_handle()),
598        }
599    }
600}
601
602impl EventEmitter<DismissEvent> for ConfigureContextServerModal {}
603
604impl ConfigureContextServerModal {
605    fn render_modal_header(&self) -> ModalHeader {
606        let text: SharedString = match &self.source {
607            ConfigurationSource::New { .. } => "Add MCP Server".into(),
608            ConfigurationSource::Existing { .. } => "Configure MCP Server".into(),
609            ConfigurationSource::Extension { id, .. } => format!("Configure {}", id.0).into(),
610        };
611        ModalHeader::new().headline(text)
612    }
613
614    fn render_modal_description(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
615        const MODAL_DESCRIPTION: &str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables.";
616
617        if let ConfigurationSource::Extension {
618            installation_instructions: Some(installation_instructions),
619            ..
620        } = &self.source
621        {
622            div()
623                .pb_2()
624                .text_sm()
625                .child(MarkdownElement::new(
626                    installation_instructions.clone(),
627                    default_markdown_style(window, cx),
628                ))
629                .into_any_element()
630        } else {
631            Label::new(MODAL_DESCRIPTION)
632                .color(Color::Muted)
633                .into_any_element()
634        }
635    }
636
637    fn render_modal_content(&self, cx: &App) -> AnyElement {
638        let editor = match &self.source {
639            ConfigurationSource::New { editor, .. } => editor,
640            ConfigurationSource::Existing { editor, .. } => editor,
641            ConfigurationSource::Extension { editor, .. } => {
642                let Some(editor) = editor else {
643                    return div().into_any_element();
644                };
645                editor
646            }
647        };
648
649        div()
650            .p_2()
651            .rounded_md()
652            .border_1()
653            .border_color(cx.theme().colors().border_variant)
654            .bg(cx.theme().colors().editor_background)
655            .child({
656                let settings = ThemeSettings::get_global(cx);
657                let text_style = TextStyle {
658                    color: cx.theme().colors().text,
659                    font_family: settings.buffer_font.family.clone(),
660                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
661                    font_size: settings.buffer_font_size(cx).into(),
662                    font_weight: settings.buffer_font.weight,
663                    line_height: relative(settings.buffer_line_height.value()),
664                    ..Default::default()
665                };
666                EditorElement::new(
667                    editor,
668                    EditorStyle {
669                        background: cx.theme().colors().editor_background,
670                        local_player: cx.theme().players().local(),
671                        text: text_style,
672                        syntax: cx.theme().syntax().clone(),
673                        ..Default::default()
674                    },
675                )
676            })
677            .into_any_element()
678    }
679
680    fn render_modal_footer(&self, cx: &mut Context<Self>) -> ModalFooter {
681        let focus_handle = self.focus_handle(cx);
682        let is_connecting = matches!(self.state, State::Waiting);
683
684        ModalFooter::new()
685            .start_slot::<Button>(
686                if let ConfigurationSource::Extension {
687                    repository_url: Some(repository_url),
688                    ..
689                } = &self.source
690                {
691                    Some(
692                        Button::new("open-repository", "Open Repository")
693                            .icon(IconName::ArrowUpRight)
694                            .icon_color(Color::Muted)
695                            .icon_size(IconSize::Small)
696                            .tooltip({
697                                let repository_url = repository_url.clone();
698                                move |_window, cx| {
699                                    Tooltip::with_meta(
700                                        "Open Repository",
701                                        None,
702                                        repository_url.clone(),
703                                        cx,
704                                    )
705                                }
706                            })
707                            .on_click({
708                                let repository_url = repository_url.clone();
709                                move |_, _, cx| cx.open_url(&repository_url)
710                            }),
711                    )
712                } else if let ConfigurationSource::New { is_http, .. } = &self.source {
713                    let label = if *is_http {
714                        "Configure Local"
715                    } else {
716                        "Configure Remote"
717                    };
718                    let tooltip = if *is_http {
719                        "Configure an MCP server that runs on stdin/stdout."
720                    } else {
721                        "Configure an MCP server that you connect to over HTTP"
722                    };
723
724                    Some(
725                        Button::new("toggle-kind", label)
726                            .tooltip(Tooltip::text(tooltip))
727                            .on_click(cx.listener(|this, _, window, cx| match &mut this.source {
728                                ConfigurationSource::New { editor, is_http } => {
729                                    *is_http = !*is_http;
730                                    let new_text = if *is_http {
731                                        context_server_http_input(None)
732                                    } else {
733                                        context_server_input(None)
734                                    };
735                                    editor.update(cx, |editor, cx| {
736                                        editor.set_text(new_text, window, cx);
737                                    })
738                                }
739                                _ => {}
740                            })),
741                    )
742                } else {
743                    None
744                },
745            )
746            .end_slot(
747                h_flex()
748                    .gap_2()
749                    .child(
750                        Button::new(
751                            "cancel",
752                            if self.source.has_configuration_options() {
753                                "Cancel"
754                            } else {
755                                "Dismiss"
756                            },
757                        )
758                        .key_binding(
759                            KeyBinding::for_action_in(&menu::Cancel, &focus_handle, cx)
760                                .map(|kb| kb.size(rems_from_px(12.))),
761                        )
762                        .on_click(
763                            cx.listener(|this, _event, _window, cx| this.cancel(&menu::Cancel, cx)),
764                        ),
765                    )
766                    .children(self.source.has_configuration_options().then(|| {
767                        Button::new(
768                            "add-server",
769                            if self.source.is_new() {
770                                "Add Server"
771                            } else {
772                                "Configure Server"
773                            },
774                        )
775                        .disabled(is_connecting)
776                        .key_binding(
777                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
778                                .map(|kb| kb.size(rems_from_px(12.))),
779                        )
780                        .on_click(
781                            cx.listener(|this, _event, _window, cx| {
782                                this.confirm(&menu::Confirm, cx)
783                            }),
784                        )
785                    })),
786            )
787    }
788
789    fn render_waiting_for_context_server() -> Div {
790        h_flex()
791            .gap_2()
792            .child(
793                Icon::new(IconName::ArrowCircle)
794                    .size(IconSize::XSmall)
795                    .color(Color::Info)
796                    .with_rotate_animation(2)
797                    .into_any_element(),
798            )
799            .child(
800                Label::new("Waiting for Context Server")
801                    .size(LabelSize::Small)
802                    .color(Color::Muted),
803            )
804    }
805
806    fn render_modal_error(error: SharedString) -> Div {
807        h_flex()
808            .gap_2()
809            .child(
810                Icon::new(IconName::Warning)
811                    .size(IconSize::XSmall)
812                    .color(Color::Warning),
813            )
814            .child(
815                div()
816                    .w_full()
817                    .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)),
818            )
819    }
820}
821
822impl Render for ConfigureContextServerModal {
823    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
824        div()
825            .elevation_3(cx)
826            .w(rems(34.))
827            .key_context("ConfigureContextServerModal")
828            .on_action(
829                cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
830            )
831            .on_action(
832                cx.listener(|this, _: &menu::Confirm, _window, cx| {
833                    this.confirm(&menu::Confirm, cx)
834                }),
835            )
836            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
837                this.focus_handle(cx).focus(window);
838            }))
839            .child(
840                Modal::new("configure-context-server", None)
841                    .header(self.render_modal_header())
842                    .section(
843                        Section::new().child(
844                            div()
845                                .size_full()
846                                .child(
847                                    div()
848                                        .id("modal-content")
849                                        .max_h(vh(0.7, window))
850                                        .overflow_y_scroll()
851                                        .track_scroll(&self.scroll_handle)
852                                        .child(self.render_modal_description(window, cx))
853                                        .child(self.render_modal_content(cx))
854                                        .child(match &self.state {
855                                            State::Idle => div(),
856                                            State::Waiting => {
857                                                Self::render_waiting_for_context_server()
858                                            }
859                                            State::Error(error) => {
860                                                Self::render_modal_error(error.clone())
861                                            }
862                                        }),
863                                )
864                                .vertical_scrollbar_for(&self.scroll_handle, window, cx),
865                        ),
866                    )
867                    .footer(self.render_modal_footer(cx)),
868            )
869    }
870}
871
872fn wait_for_context_server(
873    context_server_store: &Entity<ContextServerStore>,
874    context_server_id: ContextServerId,
875    cx: &mut App,
876) -> Task<Result<(), Arc<str>>> {
877    let (tx, rx) = futures::channel::oneshot::channel();
878    let tx = Arc::new(Mutex::new(Some(tx)));
879
880    let subscription = cx.subscribe(context_server_store, move |_, event, _cx| match event {
881        project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
882            match status {
883                ContextServerStatus::Running => {
884                    if server_id == &context_server_id
885                        && let Some(tx) = tx.lock().unwrap().take()
886                    {
887                        let _ = tx.send(Ok(()));
888                    }
889                }
890                ContextServerStatus::Stopped => {
891                    if server_id == &context_server_id
892                        && let Some(tx) = tx.lock().unwrap().take()
893                    {
894                        let _ = tx.send(Err("Context server stopped running".into()));
895                    }
896                }
897                ContextServerStatus::Error(error) => {
898                    if server_id == &context_server_id
899                        && let Some(tx) = tx.lock().unwrap().take()
900                    {
901                        let _ = tx.send(Err(error.clone()));
902                    }
903                }
904                _ => {}
905            }
906        }
907    });
908
909    cx.spawn(async move |_cx| {
910        let result = rx
911            .await
912            .map_err(|_| Arc::from("Context server store was dropped"))?;
913        drop(subscription);
914        result
915    })
916}
917
918pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
919    let theme_settings = ThemeSettings::get_global(cx);
920    let colors = cx.theme().colors();
921    let mut text_style = window.text_style();
922    text_style.refine(&TextStyleRefinement {
923        font_family: Some(theme_settings.ui_font.family.clone()),
924        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
925        font_features: Some(theme_settings.ui_font.features.clone()),
926        font_size: Some(TextSize::XSmall.rems(cx).into()),
927        color: Some(colors.text_muted),
928        ..Default::default()
929    });
930
931    MarkdownStyle {
932        base_text_style: text_style.clone(),
933        selection_background_color: colors.element_selection_background,
934        link: TextStyleRefinement {
935            background_color: Some(colors.editor_foreground.opacity(0.025)),
936            underline: Some(UnderlineStyle {
937                color: Some(colors.text_accent.opacity(0.5)),
938                thickness: px(1.),
939                ..Default::default()
940            }),
941            ..Default::default()
942        },
943        ..Default::default()
944    }
945}