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