agent_configuration.rs

   1mod add_llm_provider_modal;
   2pub mod configure_context_server_modal;
   3mod configure_context_server_tools_modal;
   4mod manage_profiles_modal;
   5mod tool_picker;
   6
   7use std::{ops::Range, sync::Arc};
   8
   9use agent::ContextServerRegistry;
  10use anyhow::Result;
  11use client::zed_urls;
  12use cloud_llm_client::{Plan, PlanV1, PlanV2};
  13use collections::HashMap;
  14use context_server::ContextServerId;
  15use editor::{Editor, MultiBufferOffset, SelectionEffects, scroll::Autoscroll};
  16use extension::ExtensionManifest;
  17use extension_host::ExtensionStore;
  18use fs::Fs;
  19use gpui::{
  20    Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
  21    ScrollHandle, Subscription, Task, WeakEntity,
  22};
  23use language::LanguageRegistry;
  24use language_model::{
  25    IconOrSvg, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
  26    ZED_CLOUD_PROVIDER_ID,
  27};
  28use language_models::AllLanguageModelSettings;
  29use notifications::status_toast::{StatusToast, ToastIcon};
  30use project::{
  31    agent_server_store::{
  32        AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME,
  33    },
  34    context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
  35};
  36use settings::{Settings, SettingsStore, update_settings_file};
  37use ui::{
  38    ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, Divider,
  39    DividerColor, ElevationIndex, Indicator, LabelSize, PopoverMenu, Switch, Tooltip,
  40    WithScrollbar, prelude::*,
  41};
  42use util::ResultExt as _;
  43use workspace::{Workspace, create_and_open_local_file};
  44use zed_actions::{ExtensionCategoryFilter, OpenBrowser};
  45
  46pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
  47pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
  48pub(crate) use manage_profiles_modal::ManageProfilesModal;
  49
  50use crate::agent_configuration::add_llm_provider_modal::{
  51    AddLlmProviderModal, LlmCompatibleProvider,
  52};
  53
  54pub struct AgentConfiguration {
  55    fs: Arc<dyn Fs>,
  56    language_registry: Arc<LanguageRegistry>,
  57    agent_server_store: Entity<AgentServerStore>,
  58    workspace: WeakEntity<Workspace>,
  59    focus_handle: FocusHandle,
  60    configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
  61    context_server_store: Entity<ContextServerStore>,
  62    expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>,
  63    context_server_registry: Entity<ContextServerRegistry>,
  64    _registry_subscription: Subscription,
  65    scroll_handle: ScrollHandle,
  66    _check_for_gemini: Task<()>,
  67}
  68
  69impl AgentConfiguration {
  70    pub fn new(
  71        fs: Arc<dyn Fs>,
  72        agent_server_store: Entity<AgentServerStore>,
  73        context_server_store: Entity<ContextServerStore>,
  74        context_server_registry: Entity<ContextServerRegistry>,
  75        language_registry: Arc<LanguageRegistry>,
  76        workspace: WeakEntity<Workspace>,
  77        window: &mut Window,
  78        cx: &mut Context<Self>,
  79    ) -> Self {
  80        let focus_handle = cx.focus_handle();
  81
  82        let registry_subscription = cx.subscribe_in(
  83            &LanguageModelRegistry::global(cx),
  84            window,
  85            |this, _, event: &language_model::Event, window, cx| match event {
  86                language_model::Event::AddedProvider(provider_id) => {
  87                    let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
  88                    if let Some(provider) = provider {
  89                        this.add_provider_configuration_view(&provider, window, cx);
  90                    }
  91                }
  92                language_model::Event::RemovedProvider(provider_id) => {
  93                    this.remove_provider_configuration_view(provider_id);
  94                }
  95                _ => {}
  96            },
  97        );
  98
  99        cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
 100            .detach();
 101
 102        let mut this = Self {
 103            fs,
 104            language_registry,
 105            workspace,
 106            focus_handle,
 107            configuration_views_by_provider: HashMap::default(),
 108            agent_server_store,
 109            context_server_store,
 110            expanded_provider_configurations: HashMap::default(),
 111            context_server_registry,
 112            _registry_subscription: registry_subscription,
 113            scroll_handle: ScrollHandle::new(),
 114            _check_for_gemini: Task::ready(()),
 115        };
 116        this.build_provider_configuration_views(window, cx);
 117        this
 118    }
 119
 120    fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 121        let providers = LanguageModelRegistry::read_global(cx).visible_providers();
 122        for provider in providers {
 123            self.add_provider_configuration_view(&provider, window, cx);
 124        }
 125    }
 126
 127    fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
 128        self.configuration_views_by_provider.remove(provider_id);
 129        self.expanded_provider_configurations.remove(provider_id);
 130    }
 131
 132    fn add_provider_configuration_view(
 133        &mut self,
 134        provider: &Arc<dyn LanguageModelProvider>,
 135        window: &mut Window,
 136        cx: &mut Context<Self>,
 137    ) {
 138        let configuration_view = provider.configuration_view(
 139            language_model::ConfigurationViewTargetAgent::ZedAgent,
 140            window,
 141            cx,
 142        );
 143        self.configuration_views_by_provider
 144            .insert(provider.id(), configuration_view);
 145    }
 146}
 147
 148impl Focusable for AgentConfiguration {
 149    fn focus_handle(&self, _: &App) -> FocusHandle {
 150        self.focus_handle.clone()
 151    }
 152}
 153
 154pub enum AssistantConfigurationEvent {
 155    NewThread(Arc<dyn LanguageModelProvider>),
 156}
 157
 158impl EventEmitter<AssistantConfigurationEvent> for AgentConfiguration {}
 159
 160enum AgentIcon {
 161    Name(IconName),
 162    Path(SharedString),
 163}
 164
 165impl AgentConfiguration {
 166    fn render_section_title(
 167        &mut self,
 168        title: impl Into<SharedString>,
 169        description: impl Into<SharedString>,
 170        menu: AnyElement,
 171    ) -> impl IntoElement {
 172        h_flex()
 173            .p_4()
 174            .pb_0()
 175            .mb_2p5()
 176            .items_start()
 177            .justify_between()
 178            .child(
 179                v_flex()
 180                    .w_full()
 181                    .gap_0p5()
 182                    .child(
 183                        h_flex()
 184                            .pr_1()
 185                            .w_full()
 186                            .gap_2()
 187                            .justify_between()
 188                            .flex_wrap()
 189                            .child(Headline::new(title.into()))
 190                            .child(menu),
 191                    )
 192                    .child(Label::new(description.into()).color(Color::Muted)),
 193            )
 194    }
 195
 196    fn render_provider_configuration_block(
 197        &mut self,
 198        provider: &Arc<dyn LanguageModelProvider>,
 199        cx: &mut Context<Self>,
 200    ) -> impl IntoElement + use<> {
 201        let provider_id = provider.id().0;
 202        let provider_name = provider.name().0;
 203        let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}"));
 204
 205        let configuration_view = self
 206            .configuration_views_by_provider
 207            .get(&provider.id())
 208            .cloned();
 209
 210        let is_expanded = self
 211            .expanded_provider_configurations
 212            .get(&provider.id())
 213            .copied()
 214            .unwrap_or(false);
 215
 216        let is_zed_provider = provider.id() == ZED_CLOUD_PROVIDER_ID;
 217        let current_plan = if is_zed_provider {
 218            self.workspace
 219                .upgrade()
 220                .and_then(|workspace| workspace.read(cx).user_store().read(cx).plan())
 221        } else {
 222            None
 223        };
 224
 225        let is_signed_in = self
 226            .workspace
 227            .read_with(cx, |workspace, _| {
 228                !workspace.client().status().borrow().is_signed_out()
 229            })
 230            .unwrap_or(false);
 231
 232        v_flex()
 233            .w_full()
 234            .when(is_expanded, |this| this.mb_2())
 235            .child(
 236                div()
 237                    .px_2()
 238                    .child(Divider::horizontal().color(DividerColor::BorderFaded)),
 239            )
 240            .child(
 241                h_flex()
 242                    .map(|this| {
 243                        if is_expanded {
 244                            this.mt_2().mb_1()
 245                        } else {
 246                            this.my_2()
 247                        }
 248                    })
 249                    .w_full()
 250                    .justify_between()
 251                    .child(
 252                        h_flex()
 253                            .id(provider_id_string.clone())
 254                            .px_2()
 255                            .py_0p5()
 256                            .w_full()
 257                            .justify_between()
 258                            .rounded_sm()
 259                            .hover(|hover| hover.bg(cx.theme().colors().element_hover))
 260                            .child(
 261                                h_flex()
 262                                    .w_full()
 263                                    .gap_1p5()
 264                                    .child(
 265                                        match provider.icon() {
 266                                            IconOrSvg::Svg(path) => Icon::from_external_svg(path),
 267                                            IconOrSvg::Icon(name) => Icon::new(name),
 268                                        }
 269                                        .size(IconSize::Small)
 270                                        .color(Color::Muted),
 271                                    )
 272                                    .child(
 273                                        h_flex()
 274                                            .w_full()
 275                                            .gap_1()
 276                                            .child(Label::new(provider_name.clone()))
 277                                            .map(|this| {
 278                                                if is_zed_provider && is_signed_in {
 279                                                    this.child(
 280                                                        self.render_zed_plan_info(current_plan, cx),
 281                                                    )
 282                                                } else {
 283                                                    this.when(
 284                                                        provider.is_authenticated(cx)
 285                                                            && !is_expanded,
 286                                                        |parent| {
 287                                                            parent.child(
 288                                                                Icon::new(IconName::Check)
 289                                                                    .color(Color::Success),
 290                                                            )
 291                                                        },
 292                                                    )
 293                                                }
 294                                            }),
 295                                    ),
 296                            )
 297                            .child(
 298                                Disclosure::new(provider_id_string, is_expanded)
 299                                    .opened_icon(IconName::ChevronUp)
 300                                    .closed_icon(IconName::ChevronDown),
 301                            )
 302                            .on_click(cx.listener({
 303                                let provider_id = provider.id();
 304                                move |this, _event, _window, _cx| {
 305                                    let is_expanded = this
 306                                        .expanded_provider_configurations
 307                                        .entry(provider_id.clone())
 308                                        .or_insert(false);
 309
 310                                    *is_expanded = !*is_expanded;
 311                                }
 312                            })),
 313                    ),
 314            )
 315            .child(
 316                v_flex()
 317                    .w_full()
 318                    .px_2()
 319                    .gap_1()
 320                    .when(is_expanded, |parent| match configuration_view {
 321                        Some(configuration_view) => parent.child(configuration_view),
 322                        None => parent.child(Label::new(format!(
 323                            "No configuration view for {provider_name}",
 324                        ))),
 325                    })
 326                    .when(is_expanded && provider.is_authenticated(cx), |parent| {
 327                        parent.child(
 328                            Button::new(
 329                                SharedString::from(format!("new-thread-{provider_id}")),
 330                                "Start New Thread",
 331                            )
 332                            .full_width()
 333                            .style(ButtonStyle::Outlined)
 334                            .layer(ElevationIndex::ModalSurface)
 335                            .icon_position(IconPosition::Start)
 336                            .icon(IconName::Thread)
 337                            .icon_size(IconSize::Small)
 338                            .icon_color(Color::Muted)
 339                            .label_size(LabelSize::Small)
 340                            .on_click(cx.listener({
 341                                let provider = provider.clone();
 342                                move |_this, _event, _window, cx| {
 343                                    cx.emit(AssistantConfigurationEvent::NewThread(
 344                                        provider.clone(),
 345                                    ))
 346                                }
 347                            })),
 348                        )
 349                    })
 350                    .when(
 351                        is_expanded && is_removable_provider(&provider.id(), cx),
 352                        |this| {
 353                            this.child(
 354                                Button::new(
 355                                    SharedString::from(format!("delete-provider-{provider_id}")),
 356                                    "Remove Provider",
 357                                )
 358                                .full_width()
 359                                .style(ButtonStyle::Outlined)
 360                                .icon_position(IconPosition::Start)
 361                                .icon(IconName::Trash)
 362                                .icon_size(IconSize::Small)
 363                                .icon_color(Color::Muted)
 364                                .label_size(LabelSize::Small)
 365                                .on_click(cx.listener({
 366                                    let provider = provider.clone();
 367                                    move |this, _event, window, cx| {
 368                                        this.delete_provider(provider.clone(), window, cx);
 369                                    }
 370                                })),
 371                            )
 372                        },
 373                    ),
 374            )
 375    }
 376
 377    fn delete_provider(
 378        &mut self,
 379        provider: Arc<dyn LanguageModelProvider>,
 380        window: &mut Window,
 381        cx: &mut Context<Self>,
 382    ) {
 383        let fs = self.fs.clone();
 384        let provider_id = provider.id();
 385
 386        cx.spawn_in(window, async move |_, cx| {
 387            cx.update(|_window, cx| {
 388                update_settings_file(fs.clone(), cx, {
 389                    let provider_id = provider_id.clone();
 390                    move |settings, _| {
 391                        if let Some(ref mut openai_compatible) = settings
 392                            .language_models
 393                            .as_mut()
 394                            .and_then(|lm| lm.openai_compatible.as_mut())
 395                        {
 396                            let key_to_remove: Arc<str> = Arc::from(provider_id.0.as_ref());
 397                            openai_compatible.remove(&key_to_remove);
 398                        }
 399                    }
 400                });
 401            })
 402            .log_err();
 403
 404            cx.update(|_window, cx| {
 405                LanguageModelRegistry::global(cx).update(cx, {
 406                    let provider_id = provider_id.clone();
 407                    move |registry, cx| {
 408                        registry.unregister_provider(provider_id, cx);
 409                    }
 410                })
 411            })
 412            .log_err();
 413
 414            anyhow::Ok(())
 415        })
 416        .detach_and_log_err(cx);
 417    }
 418
 419    fn render_provider_configuration_section(
 420        &mut self,
 421        cx: &mut Context<Self>,
 422    ) -> impl IntoElement {
 423        let providers = LanguageModelRegistry::read_global(cx).visible_providers();
 424
 425        let popover_menu = PopoverMenu::new("add-provider-popover")
 426            .trigger(
 427                Button::new("add-provider", "Add Provider")
 428                    .style(ButtonStyle::Outlined)
 429                    .icon_position(IconPosition::Start)
 430                    .icon(IconName::Plus)
 431                    .icon_size(IconSize::Small)
 432                    .icon_color(Color::Muted)
 433                    .label_size(LabelSize::Small),
 434            )
 435            .menu({
 436                let workspace = self.workspace.clone();
 437                move |window, cx| {
 438                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
 439                        menu.header("Compatible APIs").entry("OpenAI", None, {
 440                            let workspace = workspace.clone();
 441                            move |window, cx| {
 442                                workspace
 443                                    .update(cx, |workspace, cx| {
 444                                        AddLlmProviderModal::toggle(
 445                                            LlmCompatibleProvider::OpenAi,
 446                                            workspace,
 447                                            window,
 448                                            cx,
 449                                        );
 450                                    })
 451                                    .log_err();
 452                            }
 453                        })
 454                    }))
 455                }
 456            })
 457            .anchor(gpui::Corner::TopRight)
 458            .offset(gpui::Point {
 459                x: px(0.0),
 460                y: px(2.0),
 461            });
 462
 463        v_flex()
 464            .w_full()
 465            .child(self.render_section_title(
 466                "LLM Providers",
 467                "Add at least one provider to use AI-powered features with Zed's native agent.",
 468                popover_menu.into_any_element(),
 469            ))
 470            .child(
 471                div()
 472                    .w_full()
 473                    .pl(DynamicSpacing::Base08.rems(cx))
 474                    .pr(DynamicSpacing::Base20.rems(cx))
 475                    .children(
 476                        providers.into_iter().map(|provider| {
 477                            self.render_provider_configuration_block(&provider, cx)
 478                        }),
 479                    ),
 480            )
 481    }
 482
 483    fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement {
 484        if let Some(plan) = plan {
 485            let free_chip_bg = cx
 486                .theme()
 487                .colors()
 488                .editor_background
 489                .opacity(0.5)
 490                .blend(cx.theme().colors().text_accent.opacity(0.05));
 491
 492            let pro_chip_bg = cx
 493                .theme()
 494                .colors()
 495                .editor_background
 496                .opacity(0.5)
 497                .blend(cx.theme().colors().text_accent.opacity(0.2));
 498
 499            let (plan_name, label_color, bg_color) = match plan {
 500                Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree) => {
 501                    ("Free", Color::Default, free_chip_bg)
 502                }
 503                Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial) => {
 504                    ("Pro Trial", Color::Accent, pro_chip_bg)
 505                }
 506                Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro) => {
 507                    ("Pro", Color::Accent, pro_chip_bg)
 508                }
 509            };
 510
 511            Chip::new(plan_name.to_string())
 512                .bg_color(bg_color)
 513                .label_color(label_color)
 514                .into_any_element()
 515        } else {
 516            div().into_any_element()
 517        }
 518    }
 519
 520    fn render_context_servers_section(
 521        &mut self,
 522        window: &mut Window,
 523        cx: &mut Context<Self>,
 524    ) -> impl IntoElement {
 525        let mut context_server_ids = self
 526            .context_server_store
 527            .read(cx)
 528            .server_ids(cx)
 529            .into_iter()
 530            .collect::<Vec<_>>();
 531
 532        // Sort context servers: ones without mcp-server- prefix first, then prefixed ones
 533        context_server_ids.sort_by(|a, b| {
 534            const MCP_PREFIX: &str = "mcp-server-";
 535            match (a.0.strip_prefix(MCP_PREFIX), b.0.strip_prefix(MCP_PREFIX)) {
 536                // If one has mcp-server- prefix and other doesn't, non-mcp comes first
 537                (Some(_), None) => std::cmp::Ordering::Greater,
 538                (None, Some(_)) => std::cmp::Ordering::Less,
 539                // If both have same prefix status, sort by appropriate key
 540                (Some(a), Some(b)) => a.cmp(b),
 541                (None, None) => a.0.cmp(&b.0),
 542            }
 543        });
 544
 545        let add_server_popover = PopoverMenu::new("add-server-popover")
 546            .trigger(
 547                Button::new("add-server", "Add Server")
 548                    .style(ButtonStyle::Outlined)
 549                    .icon_position(IconPosition::Start)
 550                    .icon(IconName::Plus)
 551                    .icon_size(IconSize::Small)
 552                    .icon_color(Color::Muted)
 553                    .label_size(LabelSize::Small),
 554            )
 555            .menu({
 556                move |window, cx| {
 557                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
 558                        menu.entry("Add Custom Server", None, {
 559                            |window, cx| {
 560                                window.dispatch_action(crate::AddContextServer.boxed_clone(), cx)
 561                            }
 562                        })
 563                        .entry("Install from Extensions", None, {
 564                            |window, cx| {
 565                                window.dispatch_action(
 566                                    zed_actions::Extensions {
 567                                        category_filter: Some(
 568                                            ExtensionCategoryFilter::ContextServers,
 569                                        ),
 570                                        id: None,
 571                                    }
 572                                    .boxed_clone(),
 573                                    cx,
 574                                )
 575                            }
 576                        })
 577                    }))
 578                }
 579            })
 580            .anchor(gpui::Corner::TopRight)
 581            .offset(gpui::Point {
 582                x: px(0.0),
 583                y: px(2.0),
 584            });
 585
 586        v_flex()
 587            .border_b_1()
 588            .border_color(cx.theme().colors().border)
 589            .child(self.render_section_title(
 590                "Model Context Protocol (MCP) Servers",
 591                "All MCP servers connected directly or via a Zed extension.",
 592                add_server_popover.into_any_element(),
 593            ))
 594            .child(
 595                v_flex()
 596                    .pl_4()
 597                    .pb_4()
 598                    .pr_5()
 599                    .w_full()
 600                    .gap_1()
 601                    .map(|mut parent| {
 602                        if context_server_ids.is_empty() {
 603                            parent.child(
 604                                h_flex()
 605                                    .p_4()
 606                                    .justify_center()
 607                                    .border_1()
 608                                    .border_dashed()
 609                                    .border_color(cx.theme().colors().border.opacity(0.6))
 610                                    .rounded_sm()
 611                                    .child(
 612                                        Label::new("No MCP servers added yet.")
 613                                            .color(Color::Muted)
 614                                            .size(LabelSize::Small),
 615                                    ),
 616                            )
 617                        } else {
 618                            for (index, context_server_id) in
 619                                context_server_ids.into_iter().enumerate()
 620                            {
 621                                if index > 0 {
 622                                    parent = parent.child(
 623                                        Divider::horizontal()
 624                                            .color(DividerColor::BorderFaded)
 625                                            .into_any_element(),
 626                                    );
 627                                }
 628                                parent = parent.child(self.render_context_server(
 629                                    context_server_id,
 630                                    window,
 631                                    cx,
 632                                ));
 633                            }
 634                            parent
 635                        }
 636                    }),
 637            )
 638    }
 639
 640    fn render_context_server(
 641        &self,
 642        context_server_id: ContextServerId,
 643        window: &mut Window,
 644        cx: &mut Context<Self>,
 645    ) -> impl use<> + IntoElement {
 646        let server_status = self
 647            .context_server_store
 648            .read(cx)
 649            .status_for_server(&context_server_id)
 650            .unwrap_or(ContextServerStatus::Stopped);
 651        let server_configuration = self
 652            .context_server_store
 653            .read(cx)
 654            .configuration_for_server(&context_server_id);
 655
 656        let is_running = matches!(server_status, ContextServerStatus::Running);
 657        let item_id = SharedString::from(context_server_id.0.clone());
 658        // Servers without a configuration can only be provided by extensions.
 659        let provided_by_extension = server_configuration.as_ref().is_none_or(|config| {
 660            matches!(
 661                config.as_ref(),
 662                ContextServerConfiguration::Extension { .. }
 663            )
 664        });
 665
 666        let error = if let ContextServerStatus::Error(error) = server_status.clone() {
 667            Some(error)
 668        } else {
 669            None
 670        };
 671
 672        let tool_count = self
 673            .context_server_registry
 674            .read(cx)
 675            .tools_for_server(&context_server_id)
 676            .count();
 677
 678        let (source_icon, source_tooltip) = if provided_by_extension {
 679            (
 680                IconName::ZedSrcExtension,
 681                "This MCP server was installed from an extension.",
 682            )
 683        } else {
 684            (
 685                IconName::ZedSrcCustom,
 686                "This custom MCP server was installed directly.",
 687            )
 688        };
 689
 690        let (status_indicator, tooltip_text) = match server_status {
 691            ContextServerStatus::Starting => (
 692                Icon::new(IconName::LoadCircle)
 693                    .size(IconSize::XSmall)
 694                    .color(Color::Accent)
 695                    .with_keyed_rotate_animation(
 696                        SharedString::from(format!("{}-starting", context_server_id.0)),
 697                        3,
 698                    )
 699                    .into_any_element(),
 700                "Server is starting.",
 701            ),
 702            ContextServerStatus::Running => (
 703                Indicator::dot().color(Color::Success).into_any_element(),
 704                "Server is active.",
 705            ),
 706            ContextServerStatus::Error(_) => (
 707                Indicator::dot().color(Color::Error).into_any_element(),
 708                "Server has an error.",
 709            ),
 710            ContextServerStatus::Stopped => (
 711                Indicator::dot().color(Color::Muted).into_any_element(),
 712                "Server is stopped.",
 713            ),
 714        };
 715        let is_remote = server_configuration
 716            .as_ref()
 717            .map(|config| matches!(config.as_ref(), ContextServerConfiguration::Http { .. }))
 718            .unwrap_or(false);
 719        let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu")
 720            .trigger_with_tooltip(
 721                IconButton::new("context-server-config-menu", IconName::Settings)
 722                    .icon_color(Color::Muted)
 723                    .icon_size(IconSize::Small),
 724                Tooltip::text("Configure MCP Server"),
 725            )
 726            .anchor(Corner::TopRight)
 727            .menu({
 728                let fs = self.fs.clone();
 729                let context_server_id = context_server_id.clone();
 730                let language_registry = self.language_registry.clone();
 731                let workspace = self.workspace.clone();
 732                let context_server_registry = self.context_server_registry.clone();
 733
 734                move |window, cx| {
 735                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
 736                        menu.entry("Configure Server", None, {
 737                            let context_server_id = context_server_id.clone();
 738                            let language_registry = language_registry.clone();
 739                            let workspace = workspace.clone();
 740                            move |window, cx| {
 741                                if is_remote {
 742                                    crate::agent_configuration::configure_context_server_modal::ConfigureContextServerModal::show_modal_for_existing_server(
 743                                        context_server_id.clone(),
 744                                        language_registry.clone(),
 745                                        workspace.clone(),
 746                                        window,
 747                                        cx,
 748                                    )
 749                                    .detach();
 750                                } else {
 751                                    ConfigureContextServerModal::show_modal_for_existing_server(
 752                                        context_server_id.clone(),
 753                                        language_registry.clone(),
 754                                        workspace.clone(),
 755                                        window,
 756                                        cx,
 757                                    )
 758                                    .detach();
 759                                }
 760                            }
 761                        }).when(tool_count > 0, |this| this.entry("View Tools", None, {
 762                            let context_server_id = context_server_id.clone();
 763                            let context_server_registry = context_server_registry.clone();
 764                            let workspace = workspace.clone();
 765                            move |window, cx| {
 766                                let context_server_id = context_server_id.clone();
 767                                workspace.update(cx, |workspace, cx| {
 768                                    ConfigureContextServerToolsModal::toggle(
 769                                        context_server_id,
 770                                        context_server_registry.clone(),
 771                                        workspace,
 772                                        window,
 773                                        cx,
 774                                    );
 775                                })
 776                                .ok();
 777                            }
 778                        }))
 779                        .separator()
 780                        .entry("Uninstall", None, {
 781                            let fs = fs.clone();
 782                            let context_server_id = context_server_id.clone();
 783                            let workspace = workspace.clone();
 784                            move |_, cx| {
 785                                let uninstall_extension_task = match (
 786                                    provided_by_extension,
 787                                    resolve_extension_for_context_server(&context_server_id, cx),
 788                                ) {
 789                                    (true, Some((id, manifest))) => {
 790                                        if extension_only_provides_context_server(manifest.as_ref())
 791                                        {
 792                                            ExtensionStore::global(cx).update(cx, |store, cx| {
 793                                                store.uninstall_extension(id, cx)
 794                                            })
 795                                        } else {
 796                                            workspace.update(cx, |workspace, cx| {
 797                                                show_unable_to_uninstall_extension_with_context_server(workspace, context_server_id.clone(), cx);
 798                                            }).log_err();
 799                                            Task::ready(Ok(()))
 800                                        }
 801                                    }
 802                                    _ => Task::ready(Ok(())),
 803                                };
 804
 805                                cx.spawn({
 806                                    let fs = fs.clone();
 807                                    let context_server_id = context_server_id.clone();
 808                                    async move |cx| {
 809                                        uninstall_extension_task.await?;
 810                                        cx.update(|cx| {
 811                                            update_settings_file(
 812                                                fs.clone(),
 813                                                cx,
 814                                                {
 815                                                    let context_server_id =
 816                                                        context_server_id.clone();
 817                                                    move |settings, _| {
 818                                                        settings.project
 819                                                            .context_servers
 820                                                            .remove(&context_server_id.0);
 821                                                    }
 822                                                },
 823                                            )
 824                                        })
 825                                    }
 826                                })
 827                                .detach_and_log_err(cx);
 828                            }
 829                        })
 830                    }))
 831                }
 832            });
 833
 834        v_flex()
 835            .id(item_id.clone())
 836            .child(
 837                h_flex()
 838                    .justify_between()
 839                    .child(
 840                        h_flex()
 841                            .flex_1()
 842                            .min_w_0()
 843                            .child(
 844                                h_flex()
 845                                    .id(format!("tooltip-{}", item_id))
 846                                    .h_full()
 847                                    .w_3()
 848                                    .mr_2()
 849                                    .justify_center()
 850                                    .tooltip(Tooltip::text(tooltip_text))
 851                                    .child(status_indicator),
 852                            )
 853                            .child(Label::new(item_id).truncate())
 854                            .child(
 855                                div()
 856                                    .id("extension-source")
 857                                    .mt_0p5()
 858                                    .mx_1()
 859                                    .flex_none()
 860                                    .tooltip(Tooltip::text(source_tooltip))
 861                                    .child(
 862                                        Icon::new(source_icon)
 863                                            .size(IconSize::Small)
 864                                            .color(Color::Muted),
 865                                    ),
 866                            )
 867                            .when(is_running, |this| {
 868                                this.child(
 869                                    Label::new(if tool_count == 1 {
 870                                        SharedString::from("1 tool")
 871                                    } else {
 872                                        SharedString::from(format!("{} tools", tool_count))
 873                                    })
 874                                    .color(Color::Muted)
 875                                    .size(LabelSize::Small),
 876                                )
 877                            }),
 878                    )
 879                    .child(
 880                        h_flex()
 881                            .gap_0p5()
 882                            .flex_none()
 883                            .child(context_server_configuration_menu)
 884                            .child(
 885                            Switch::new("context-server-switch", is_running.into())
 886                                .on_click({
 887                                    let context_server_manager = self.context_server_store.clone();
 888                                    let fs = self.fs.clone();
 889
 890                                    move |state, _window, cx| {
 891                                        let is_enabled = match state {
 892                                            ToggleState::Unselected
 893                                            | ToggleState::Indeterminate => {
 894                                                context_server_manager.update(cx, |this, cx| {
 895                                                    this.stop_server(&context_server_id, cx)
 896                                                        .log_err();
 897                                                });
 898                                                false
 899                                            }
 900                                            ToggleState::Selected => {
 901                                                context_server_manager.update(cx, |this, cx| {
 902                                                    if let Some(server) =
 903                                                        this.get_server(&context_server_id)
 904                                                    {
 905                                                        this.start_server(server, cx);
 906                                                    }
 907                                                });
 908                                                true
 909                                            }
 910                                        };
 911                                        update_settings_file(fs.clone(), cx, {
 912                                            let context_server_id = context_server_id.clone();
 913
 914                                            move |settings, _| {
 915                                                settings
 916                                                    .project
 917                                                    .context_servers
 918                                                    .entry(context_server_id.0)
 919                                                    .or_insert_with(|| {
 920                                                        settings::ContextServerSettingsContent::Extension {
 921                                                            enabled: is_enabled,
 922                                                            settings: serde_json::json!({}),
 923                                                        }
 924                                                    })
 925                                                    .set_enabled(is_enabled);
 926                                            }
 927                                        });
 928                                    }
 929                                }),
 930                        ),
 931                    ),
 932            )
 933            .map(|parent| {
 934                if let Some(error) = error {
 935                    return parent.child(
 936                        h_flex()
 937                            .gap_2()
 938                            .pr_4()
 939                            .items_start()
 940                            .child(
 941                                h_flex()
 942                                    .flex_none()
 943                                    .h(window.line_height() / 1.6_f32)
 944                                    .justify_center()
 945                                    .child(
 946                                        Icon::new(IconName::XCircle)
 947                                            .size(IconSize::XSmall)
 948                                            .color(Color::Error),
 949                                    ),
 950                            )
 951                            .child(
 952                                div().w_full().child(
 953                                    Label::new(error)
 954                                        .buffer_font(cx)
 955                                        .color(Color::Muted)
 956                                        .size(LabelSize::Small),
 957                                ),
 958                            ),
 959                    );
 960                }
 961                parent
 962            })
 963    }
 964
 965    fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
 966        let agent_server_store = self.agent_server_store.read(cx);
 967
 968        let user_defined_agents = agent_server_store
 969            .external_agents()
 970            .filter(|name| {
 971                name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
 972            })
 973            .cloned()
 974            .collect::<Vec<_>>();
 975
 976        let user_defined_agents: Vec<_> = user_defined_agents
 977            .into_iter()
 978            .map(|name| {
 979                let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
 980                    AgentIcon::Path(icon_path)
 981                } else {
 982                    AgentIcon::Name(IconName::Sparkle)
 983                };
 984                let display_name = agent_server_store
 985                    .agent_display_name(&name)
 986                    .unwrap_or_else(|| name.0.clone());
 987                (name, icon, display_name)
 988            })
 989            .collect();
 990
 991        let add_agent_popover = PopoverMenu::new("add-agent-server-popover")
 992            .trigger(
 993                Button::new("add-agent", "Add Agent")
 994                    .style(ButtonStyle::Outlined)
 995                    .icon_position(IconPosition::Start)
 996                    .icon(IconName::Plus)
 997                    .icon_size(IconSize::Small)
 998                    .icon_color(Color::Muted)
 999                    .label_size(LabelSize::Small),
1000            )
1001            .menu({
1002                move |window, cx| {
1003                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
1004                        menu.entry("Install from Extensions", None, {
1005                            |window, cx| {
1006                                window.dispatch_action(
1007                                    zed_actions::Extensions {
1008                                        category_filter: Some(
1009                                            ExtensionCategoryFilter::AgentServers,
1010                                        ),
1011                                        id: None,
1012                                    }
1013                                    .boxed_clone(),
1014                                    cx,
1015                                )
1016                            }
1017                        })
1018                        .entry("Add Custom Agent", None, {
1019                            move |window, cx| {
1020                                if let Some(workspace) = window.root().flatten() {
1021                                    let workspace = workspace.downgrade();
1022                                    window
1023                                        .spawn(cx, async |cx| {
1024                                            open_new_agent_servers_entry_in_settings_editor(
1025                                                workspace, cx,
1026                                            )
1027                                            .await
1028                                        })
1029                                        .detach_and_log_err(cx);
1030                                }
1031                            }
1032                        })
1033                        .separator()
1034                        .header("Learn More")
1035                        .item(
1036                            ContextMenuEntry::new("Agent Servers Docs")
1037                                .icon(IconName::ArrowUpRight)
1038                                .icon_color(Color::Muted)
1039                                .icon_position(IconPosition::End)
1040                                .handler({
1041                                    move |window, cx| {
1042                                        window.dispatch_action(
1043                                            Box::new(OpenBrowser {
1044                                                url: zed_urls::agent_server_docs(cx),
1045                                            }),
1046                                            cx,
1047                                        );
1048                                    }
1049                                }),
1050                        )
1051                        .item(
1052                            ContextMenuEntry::new("ACP Docs")
1053                                .icon(IconName::ArrowUpRight)
1054                                .icon_color(Color::Muted)
1055                                .icon_position(IconPosition::End)
1056                                .handler({
1057                                    move |window, cx| {
1058                                        window.dispatch_action(
1059                                            Box::new(OpenBrowser {
1060                                                url: "https://agentclientprotocol.com/".into(),
1061                                            }),
1062                                            cx,
1063                                        );
1064                                    }
1065                                }),
1066                        )
1067                    }))
1068                }
1069            })
1070            .anchor(gpui::Corner::TopRight)
1071            .offset(gpui::Point {
1072                x: px(0.0),
1073                y: px(2.0),
1074            });
1075
1076        v_flex()
1077            .border_b_1()
1078            .border_color(cx.theme().colors().border)
1079            .child(
1080                v_flex()
1081                    .child(self.render_section_title(
1082                        "External Agents",
1083                        "All agents connected through the Agent Client Protocol.",
1084                        add_agent_popover.into_any_element(),
1085                    ))
1086                    .child(
1087                        v_flex()
1088                            .p_4()
1089                            .pt_0()
1090                            .gap_2()
1091                            .child(self.render_agent_server(
1092                                AgentIcon::Name(IconName::AiClaude),
1093                                "Claude Code",
1094                                "Claude Code",
1095                                false,
1096                                cx,
1097                            ))
1098                            .child(Divider::horizontal().color(DividerColor::BorderFaded))
1099                            .child(self.render_agent_server(
1100                                AgentIcon::Name(IconName::AiOpenAi),
1101                                "Codex CLI",
1102                                "Codex CLI",
1103                                false,
1104                                cx,
1105                            ))
1106                            .child(Divider::horizontal().color(DividerColor::BorderFaded))
1107                            .child(self.render_agent_server(
1108                                AgentIcon::Name(IconName::AiGemini),
1109                                "Gemini CLI",
1110                                "Gemini CLI",
1111                                false,
1112                                cx,
1113                            ))
1114                            .map(|mut parent| {
1115                                for (name, icon, display_name) in user_defined_agents {
1116                                    parent = parent
1117                                        .child(
1118                                            Divider::horizontal().color(DividerColor::BorderFaded),
1119                                        )
1120                                        .child(self.render_agent_server(
1121                                            icon,
1122                                            name,
1123                                            display_name,
1124                                            true,
1125                                            cx,
1126                                        ));
1127                                }
1128                                parent
1129                            }),
1130                    ),
1131            )
1132    }
1133
1134    fn render_agent_server(
1135        &self,
1136        icon: AgentIcon,
1137        id: impl Into<SharedString>,
1138        display_name: impl Into<SharedString>,
1139        external: bool,
1140        cx: &mut Context<Self>,
1141    ) -> impl IntoElement {
1142        let id = id.into();
1143        let display_name = display_name.into();
1144
1145        let icon = match icon {
1146            AgentIcon::Name(icon_name) => Icon::new(icon_name)
1147                .size(IconSize::Small)
1148                .color(Color::Muted),
1149            AgentIcon::Path(icon_path) => Icon::from_external_svg(icon_path)
1150                .size(IconSize::Small)
1151                .color(Color::Muted),
1152        };
1153
1154        let tooltip_id = SharedString::new(format!("agent-source-{}", id));
1155        let tooltip_message = format!(
1156            "The {} agent was installed from an extension.",
1157            display_name
1158        );
1159
1160        let agent_server_name = ExternalAgentServerName(id.clone());
1161
1162        let uninstall_btn_id = SharedString::from(format!("uninstall-{}", id));
1163        let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash)
1164            .icon_color(Color::Muted)
1165            .icon_size(IconSize::Small)
1166            .tooltip(Tooltip::text("Uninstall Agent Extension"))
1167            .on_click(cx.listener(move |this, _, _window, cx| {
1168                let agent_name = agent_server_name.clone();
1169
1170                if let Some(ext_id) = this.agent_server_store.update(cx, |store, _cx| {
1171                    store.get_extension_id_for_agent(&agent_name)
1172                }) {
1173                    ExtensionStore::global(cx)
1174                        .update(cx, |store, cx| store.uninstall_extension(ext_id, cx))
1175                        .detach_and_log_err(cx);
1176                }
1177            }));
1178
1179        h_flex()
1180            .gap_1()
1181            .justify_between()
1182            .child(
1183                h_flex()
1184                    .gap_1p5()
1185                    .child(icon)
1186                    .child(Label::new(display_name))
1187                    .when(external, |this| {
1188                        this.child(
1189                            div()
1190                                .id(tooltip_id)
1191                                .flex_none()
1192                                .tooltip(Tooltip::text(tooltip_message))
1193                                .child(
1194                                    Icon::new(IconName::ZedSrcExtension)
1195                                        .size(IconSize::Small)
1196                                        .color(Color::Muted),
1197                                ),
1198                        )
1199                    })
1200                    .child(
1201                        Icon::new(IconName::Check)
1202                            .color(Color::Success)
1203                            .size(IconSize::Small),
1204                    ),
1205            )
1206            .when(external, |this| this.child(uninstall_button))
1207    }
1208}
1209
1210impl Render for AgentConfiguration {
1211    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1212        v_flex()
1213            .id("assistant-configuration")
1214            .key_context("AgentConfiguration")
1215            .track_focus(&self.focus_handle(cx))
1216            .relative()
1217            .size_full()
1218            .pb_8()
1219            .bg(cx.theme().colors().panel_background)
1220            .child(
1221                div()
1222                    .size_full()
1223                    .child(
1224                        v_flex()
1225                            .id("assistant-configuration-content")
1226                            .track_scroll(&self.scroll_handle)
1227                            .size_full()
1228                            .overflow_y_scroll()
1229                            .child(self.render_agent_servers_section(cx))
1230                            .child(self.render_context_servers_section(window, cx))
1231                            .child(self.render_provider_configuration_section(cx)),
1232                    )
1233                    .vertical_scrollbar_for(&self.scroll_handle, window, cx),
1234            )
1235    }
1236}
1237
1238fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool {
1239    manifest.context_servers.len() == 1
1240        && manifest.themes.is_empty()
1241        && manifest.icon_themes.is_empty()
1242        && manifest.languages.is_empty()
1243        && manifest.grammars.is_empty()
1244        && manifest.language_servers.is_empty()
1245        && manifest.slash_commands.is_empty()
1246        && manifest.snippets.is_none()
1247        && manifest.debug_locators.is_empty()
1248}
1249
1250pub(crate) fn resolve_extension_for_context_server(
1251    id: &ContextServerId,
1252    cx: &App,
1253) -> Option<(Arc<str>, Arc<ExtensionManifest>)> {
1254    ExtensionStore::global(cx)
1255        .read(cx)
1256        .installed_extensions()
1257        .iter()
1258        .find(|(_, entry)| entry.manifest.context_servers.contains_key(&id.0))
1259        .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
1260}
1261
1262// This notification appears when trying to delete
1263// an MCP server extension that not only provides
1264// the server, but other things, too, like language servers and more.
1265fn show_unable_to_uninstall_extension_with_context_server(
1266    workspace: &mut Workspace,
1267    id: ContextServerId,
1268    cx: &mut App,
1269) {
1270    let workspace_handle = workspace.weak_handle();
1271    let context_server_id = id.clone();
1272
1273    let status_toast = StatusToast::new(
1274        format!(
1275            "The {} extension provides more than just the MCP server. Proceed to uninstall anyway?",
1276            id.0
1277        ),
1278        cx,
1279        move |this, _cx| {
1280            let workspace_handle = workspace_handle.clone();
1281
1282            this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
1283                .dismiss_button(true)
1284                .action("Uninstall", move |_, _cx| {
1285                    if let Some((extension_id, _)) =
1286                        resolve_extension_for_context_server(&context_server_id, _cx)
1287                    {
1288                        ExtensionStore::global(_cx).update(_cx, |store, cx| {
1289                            store
1290                                .uninstall_extension(extension_id, cx)
1291                                .detach_and_log_err(cx);
1292                        });
1293
1294                        workspace_handle
1295                            .update(_cx, |workspace, cx| {
1296                                let fs = workspace.app_state().fs.clone();
1297                                cx.spawn({
1298                                    let context_server_id = context_server_id.clone();
1299                                    async move |_workspace_handle, cx| {
1300                                        cx.update(|cx| {
1301                                            update_settings_file(fs, cx, move |settings, _| {
1302                                                settings
1303                                                    .project
1304                                                    .context_servers
1305                                                    .remove(&context_server_id.0);
1306                                            });
1307                                        })?;
1308                                        anyhow::Ok(())
1309                                    }
1310                                })
1311                                .detach_and_log_err(cx);
1312                            })
1313                            .log_err();
1314                    }
1315                })
1316        },
1317    );
1318
1319    workspace.toggle_status_toast(status_toast, cx);
1320}
1321
1322async fn open_new_agent_servers_entry_in_settings_editor(
1323    workspace: WeakEntity<Workspace>,
1324    cx: &mut AsyncWindowContext,
1325) -> Result<()> {
1326    let settings_editor = workspace
1327        .update_in(cx, |_, window, cx| {
1328            create_and_open_local_file(paths::settings_file(), window, cx, || {
1329                settings::initial_user_settings_content().as_ref().into()
1330            })
1331        })?
1332        .await?
1333        .downcast::<Editor>()
1334        .unwrap();
1335
1336    settings_editor
1337        .downgrade()
1338        .update_in(cx, |item, window, cx| {
1339            let text = item.buffer().read(cx).snapshot(cx).text();
1340
1341            let settings = cx.global::<SettingsStore>();
1342
1343            let mut unique_server_name = None;
1344            let edits = settings.edits_for_update(&text, |settings| {
1345                let server_name: Option<SharedString> = (0..u8::MAX)
1346                    .map(|i| {
1347                        if i == 0 {
1348                            "your_agent".into()
1349                        } else {
1350                            format!("your_agent_{}", i).into()
1351                        }
1352                    })
1353                    .find(|name| {
1354                        !settings
1355                            .agent_servers
1356                            .as_ref()
1357                            .is_some_and(|agent_servers| agent_servers.custom.contains_key(name))
1358                    });
1359                if let Some(server_name) = server_name {
1360                    unique_server_name = Some(server_name.clone());
1361                    settings
1362                        .agent_servers
1363                        .get_or_insert_default()
1364                        .custom
1365                        .insert(
1366                            server_name,
1367                            settings::CustomAgentServerSettings::Custom {
1368                                path: "path_to_executable".into(),
1369                                args: vec![],
1370                                env: Some(HashMap::default()),
1371                                default_mode: None,
1372                                default_model: None,
1373                                favorite_models: vec![],
1374                                default_config_options: Default::default(),
1375                                favorite_config_option_values: Default::default(),
1376                            },
1377                        );
1378                }
1379            });
1380
1381            if edits.is_empty() {
1382                return;
1383            }
1384
1385            let ranges = edits
1386                .iter()
1387                .map(|(range, _)| range.clone())
1388                .collect::<Vec<_>>();
1389
1390            item.edit(
1391                edits.into_iter().map(|(range, s)| {
1392                    (
1393                        MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
1394                        s,
1395                    )
1396                }),
1397                cx,
1398            );
1399            if let Some((unique_server_name, buffer)) =
1400                unique_server_name.zip(item.buffer().read(cx).as_singleton())
1401            {
1402                let snapshot = buffer.read(cx).snapshot();
1403                if let Some(range) =
1404                    find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
1405                {
1406                    item.change_selections(
1407                        SelectionEffects::scroll(Autoscroll::newest()),
1408                        window,
1409                        cx,
1410                        |selections| {
1411                            selections.select_ranges(vec![
1412                                MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
1413                            ]);
1414                        },
1415                    );
1416                }
1417            }
1418        })
1419}
1420
1421fn find_text_in_buffer(
1422    text: &str,
1423    start: usize,
1424    snapshot: &language::BufferSnapshot,
1425) -> Option<Range<usize>> {
1426    let chars = text.chars().collect::<Vec<char>>();
1427
1428    let mut offset = start;
1429    let mut char_offset = 0;
1430    for c in snapshot.chars_at(start) {
1431        if char_offset >= chars.len() {
1432            break;
1433        }
1434        offset += 1;
1435
1436        if c == chars[char_offset] {
1437            char_offset += 1;
1438        } else {
1439            char_offset = 0;
1440        }
1441    }
1442
1443    if char_offset == chars.len() {
1444        Some(offset.saturating_sub(chars.len())..offset)
1445    } else {
1446        None
1447    }
1448}
1449
1450// OpenAI-compatible providers are user-configured and can be removed,
1451// whereas built-in providers (like Anthropic, OpenAI, Google, etc.) can't.
1452//
1453// If in the future we have more "API-compatible-type" of providers,
1454// they should be included here as removable providers.
1455fn is_removable_provider(provider_id: &LanguageModelProviderId, cx: &App) -> bool {
1456    AllLanguageModelSettings::get_global(cx)
1457        .openai_compatible
1458        .contains_key(provider_id.0.as_ref())
1459}