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