configure_context_server_modal.rs

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