configure_context_server_modal.rs

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