configure_context_server_modal.rs

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