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