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, 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::V2(PlanV2::ZedFree) => ("Free", Color::Default, free_chip_bg),
 501                Plan::V2(PlanV2::ZedProTrial) => ("Pro Trial", Color::Accent, pro_chip_bg),
 502                Plan::V2(PlanV2::ZedPro) => ("Pro", Color::Accent, pro_chip_bg),
 503            };
 504
 505            Chip::new(plan_name.to_string())
 506                .bg_color(bg_color)
 507                .label_color(label_color)
 508                .into_any_element()
 509        } else {
 510            div().into_any_element()
 511        }
 512    }
 513
 514    fn render_context_servers_section(
 515        &mut self,
 516        window: &mut Window,
 517        cx: &mut Context<Self>,
 518    ) -> impl IntoElement {
 519        let mut context_server_ids = self
 520            .context_server_store
 521            .read(cx)
 522            .server_ids(cx)
 523            .into_iter()
 524            .collect::<Vec<_>>();
 525
 526        // Sort context servers: ones without mcp-server- prefix first, then prefixed ones
 527        context_server_ids.sort_by(|a, b| {
 528            const MCP_PREFIX: &str = "mcp-server-";
 529            match (a.0.strip_prefix(MCP_PREFIX), b.0.strip_prefix(MCP_PREFIX)) {
 530                // If one has mcp-server- prefix and other doesn't, non-mcp comes first
 531                (Some(_), None) => std::cmp::Ordering::Greater,
 532                (None, Some(_)) => std::cmp::Ordering::Less,
 533                // If both have same prefix status, sort by appropriate key
 534                (Some(a), Some(b)) => a.cmp(b),
 535                (None, None) => a.0.cmp(&b.0),
 536            }
 537        });
 538
 539        let add_server_popover = PopoverMenu::new("add-server-popover")
 540            .trigger(
 541                Button::new("add-server", "Add Server")
 542                    .style(ButtonStyle::Outlined)
 543                    .icon_position(IconPosition::Start)
 544                    .icon(IconName::Plus)
 545                    .icon_size(IconSize::Small)
 546                    .icon_color(Color::Muted)
 547                    .label_size(LabelSize::Small),
 548            )
 549            .menu({
 550                move |window, cx| {
 551                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
 552                        menu.entry("Add Custom Server", None, {
 553                            |window, cx| {
 554                                window.dispatch_action(crate::AddContextServer.boxed_clone(), cx)
 555                            }
 556                        })
 557                        .entry("Install from Extensions", None, {
 558                            |window, cx| {
 559                                window.dispatch_action(
 560                                    zed_actions::Extensions {
 561                                        category_filter: Some(
 562                                            ExtensionCategoryFilter::ContextServers,
 563                                        ),
 564                                        id: None,
 565                                    }
 566                                    .boxed_clone(),
 567                                    cx,
 568                                )
 569                            }
 570                        })
 571                    }))
 572                }
 573            })
 574            .anchor(gpui::Corner::TopRight)
 575            .offset(gpui::Point {
 576                x: px(0.0),
 577                y: px(2.0),
 578            });
 579
 580        v_flex()
 581            .border_b_1()
 582            .border_color(cx.theme().colors().border)
 583            .child(self.render_section_title(
 584                "Model Context Protocol (MCP) Servers",
 585                "All MCP servers connected directly or via a Zed extension.",
 586                add_server_popover.into_any_element(),
 587            ))
 588            .child(
 589                v_flex()
 590                    .pl_4()
 591                    .pb_4()
 592                    .pr_5()
 593                    .w_full()
 594                    .gap_1()
 595                    .map(|mut parent| {
 596                        if context_server_ids.is_empty() {
 597                            parent.child(
 598                                h_flex()
 599                                    .p_4()
 600                                    .justify_center()
 601                                    .border_1()
 602                                    .border_dashed()
 603                                    .border_color(cx.theme().colors().border.opacity(0.6))
 604                                    .rounded_sm()
 605                                    .child(
 606                                        Label::new("No MCP servers added yet.")
 607                                            .color(Color::Muted)
 608                                            .size(LabelSize::Small),
 609                                    ),
 610                            )
 611                        } else {
 612                            for (index, context_server_id) in
 613                                context_server_ids.into_iter().enumerate()
 614                            {
 615                                if index > 0 {
 616                                    parent = parent.child(
 617                                        Divider::horizontal()
 618                                            .color(DividerColor::BorderFaded)
 619                                            .into_any_element(),
 620                                    );
 621                                }
 622                                parent = parent.child(self.render_context_server(
 623                                    context_server_id,
 624                                    window,
 625                                    cx,
 626                                ));
 627                            }
 628                            parent
 629                        }
 630                    }),
 631            )
 632    }
 633
 634    fn render_context_server(
 635        &self,
 636        context_server_id: ContextServerId,
 637        window: &mut Window,
 638        cx: &mut Context<Self>,
 639    ) -> impl use<> + IntoElement {
 640        let server_status = self
 641            .context_server_store
 642            .read(cx)
 643            .status_for_server(&context_server_id)
 644            .unwrap_or(ContextServerStatus::Stopped);
 645        let server_configuration = self
 646            .context_server_store
 647            .read(cx)
 648            .configuration_for_server(&context_server_id);
 649
 650        let is_running = matches!(server_status, ContextServerStatus::Running);
 651        let item_id = SharedString::from(context_server_id.0.clone());
 652        // Servers without a configuration can only be provided by extensions.
 653        let provided_by_extension = server_configuration.as_ref().is_none_or(|config| {
 654            matches!(
 655                config.as_ref(),
 656                ContextServerConfiguration::Extension { .. }
 657            )
 658        });
 659
 660        let error = if let ContextServerStatus::Error(error) = server_status.clone() {
 661            Some(error)
 662        } else {
 663            None
 664        };
 665
 666        let tool_count = self
 667            .context_server_registry
 668            .read(cx)
 669            .tools_for_server(&context_server_id)
 670            .count();
 671
 672        let (source_icon, source_tooltip) = if provided_by_extension {
 673            (
 674                IconName::ZedSrcExtension,
 675                "This MCP server was installed from an extension.",
 676            )
 677        } else {
 678            (
 679                IconName::ZedSrcCustom,
 680                "This custom MCP server was installed directly.",
 681            )
 682        };
 683
 684        let (status_indicator, tooltip_text) = match server_status {
 685            ContextServerStatus::Starting => (
 686                Icon::new(IconName::LoadCircle)
 687                    .size(IconSize::XSmall)
 688                    .color(Color::Accent)
 689                    .with_keyed_rotate_animation(
 690                        SharedString::from(format!("{}-starting", context_server_id.0)),
 691                        3,
 692                    )
 693                    .into_any_element(),
 694                "Server is starting.",
 695            ),
 696            ContextServerStatus::Running => (
 697                Indicator::dot().color(Color::Success).into_any_element(),
 698                "Server is active.",
 699            ),
 700            ContextServerStatus::Error(_) => (
 701                Indicator::dot().color(Color::Error).into_any_element(),
 702                "Server has an error.",
 703            ),
 704            ContextServerStatus::Stopped => (
 705                Indicator::dot().color(Color::Muted).into_any_element(),
 706                "Server is stopped.",
 707            ),
 708        };
 709        let is_remote = server_configuration
 710            .as_ref()
 711            .map(|config| matches!(config.as_ref(), ContextServerConfiguration::Http { .. }))
 712            .unwrap_or(false);
 713        let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu")
 714            .trigger_with_tooltip(
 715                IconButton::new("context-server-config-menu", IconName::Settings)
 716                    .icon_color(Color::Muted)
 717                    .icon_size(IconSize::Small),
 718                Tooltip::text("Configure MCP Server"),
 719            )
 720            .anchor(Corner::TopRight)
 721            .menu({
 722                let fs = self.fs.clone();
 723                let context_server_id = context_server_id.clone();
 724                let language_registry = self.language_registry.clone();
 725                let workspace = self.workspace.clone();
 726                let context_server_registry = self.context_server_registry.clone();
 727
 728                move |window, cx| {
 729                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
 730                        menu.entry("Configure Server", None, {
 731                            let context_server_id = context_server_id.clone();
 732                            let language_registry = language_registry.clone();
 733                            let workspace = workspace.clone();
 734                            move |window, cx| {
 735                                if is_remote {
 736                                    crate::agent_configuration::configure_context_server_modal::ConfigureContextServerModal::show_modal_for_existing_server(
 737                                        context_server_id.clone(),
 738                                        language_registry.clone(),
 739                                        workspace.clone(),
 740                                        window,
 741                                        cx,
 742                                    )
 743                                    .detach();
 744                                } else {
 745                                    ConfigureContextServerModal::show_modal_for_existing_server(
 746                                        context_server_id.clone(),
 747                                        language_registry.clone(),
 748                                        workspace.clone(),
 749                                        window,
 750                                        cx,
 751                                    )
 752                                    .detach();
 753                                }
 754                            }
 755                        }).when(tool_count > 0, |this| this.entry("View Tools", None, {
 756                            let context_server_id = context_server_id.clone();
 757                            let context_server_registry = context_server_registry.clone();
 758                            let workspace = workspace.clone();
 759                            move |window, cx| {
 760                                let context_server_id = context_server_id.clone();
 761                                workspace.update(cx, |workspace, cx| {
 762                                    ConfigureContextServerToolsModal::toggle(
 763                                        context_server_id,
 764                                        context_server_registry.clone(),
 765                                        workspace,
 766                                        window,
 767                                        cx,
 768                                    );
 769                                })
 770                                .ok();
 771                            }
 772                        }))
 773                        .separator()
 774                        .entry("Uninstall", None, {
 775                            let fs = fs.clone();
 776                            let context_server_id = context_server_id.clone();
 777                            let workspace = workspace.clone();
 778                            move |_, cx| {
 779                                let uninstall_extension_task = match (
 780                                    provided_by_extension,
 781                                    resolve_extension_for_context_server(&context_server_id, cx),
 782                                ) {
 783                                    (true, Some((id, manifest))) => {
 784                                        if extension_only_provides_context_server(manifest.as_ref())
 785                                        {
 786                                            ExtensionStore::global(cx).update(cx, |store, cx| {
 787                                                store.uninstall_extension(id, cx)
 788                                            })
 789                                        } else {
 790                                            workspace.update(cx, |workspace, cx| {
 791                                                show_unable_to_uninstall_extension_with_context_server(workspace, context_server_id.clone(), cx);
 792                                            }).log_err();
 793                                            Task::ready(Ok(()))
 794                                        }
 795                                    }
 796                                    _ => Task::ready(Ok(())),
 797                                };
 798
 799                                cx.spawn({
 800                                    let fs = fs.clone();
 801                                    let context_server_id = context_server_id.clone();
 802                                    async move |cx| {
 803                                        uninstall_extension_task.await?;
 804                                        cx.update(|cx| {
 805                                            update_settings_file(
 806                                                fs.clone(),
 807                                                cx,
 808                                                {
 809                                                    let context_server_id =
 810                                                        context_server_id.clone();
 811                                                    move |settings, _| {
 812                                                        settings.project
 813                                                            .context_servers
 814                                                            .remove(&context_server_id.0);
 815                                                    }
 816                                                },
 817                                            )
 818                                        });
 819                                        anyhow::Ok(())
 820                                    }
 821                                })
 822                                .detach_and_log_err(cx);
 823                            }
 824                        })
 825                    }))
 826                }
 827            });
 828
 829        v_flex()
 830            .id(item_id.clone())
 831            .child(
 832                h_flex()
 833                    .justify_between()
 834                    .child(
 835                        h_flex()
 836                            .flex_1()
 837                            .min_w_0()
 838                            .child(
 839                                h_flex()
 840                                    .id(format!("tooltip-{}", item_id))
 841                                    .h_full()
 842                                    .w_3()
 843                                    .mr_2()
 844                                    .justify_center()
 845                                    .tooltip(Tooltip::text(tooltip_text))
 846                                    .child(status_indicator),
 847                            )
 848                            .child(Label::new(item_id).truncate())
 849                            .child(
 850                                div()
 851                                    .id("extension-source")
 852                                    .mt_0p5()
 853                                    .mx_1()
 854                                    .flex_none()
 855                                    .tooltip(Tooltip::text(source_tooltip))
 856                                    .child(
 857                                        Icon::new(source_icon)
 858                                            .size(IconSize::Small)
 859                                            .color(Color::Muted),
 860                                    ),
 861                            )
 862                            .when(is_running, |this| {
 863                                this.child(
 864                                    Label::new(if tool_count == 1 {
 865                                        SharedString::from("1 tool")
 866                                    } else {
 867                                        SharedString::from(format!("{} tools", tool_count))
 868                                    })
 869                                    .color(Color::Muted)
 870                                    .size(LabelSize::Small),
 871                                )
 872                            }),
 873                    )
 874                    .child(
 875                        h_flex()
 876                            .gap_0p5()
 877                            .flex_none()
 878                            .child(context_server_configuration_menu)
 879                            .child(
 880                            Switch::new("context-server-switch", is_running.into())
 881                                .on_click({
 882                                    let context_server_manager = self.context_server_store.clone();
 883                                    let fs = self.fs.clone();
 884
 885                                    move |state, _window, cx| {
 886                                        let is_enabled = match state {
 887                                            ToggleState::Unselected
 888                                            | ToggleState::Indeterminate => {
 889                                                context_server_manager.update(cx, |this, cx| {
 890                                                    this.stop_server(&context_server_id, cx)
 891                                                        .log_err();
 892                                                });
 893                                                false
 894                                            }
 895                                            ToggleState::Selected => {
 896                                                context_server_manager.update(cx, |this, cx| {
 897                                                    if let Some(server) =
 898                                                        this.get_server(&context_server_id)
 899                                                    {
 900                                                        this.start_server(server, cx);
 901                                                    }
 902                                                });
 903                                                true
 904                                            }
 905                                        };
 906                                        update_settings_file(fs.clone(), cx, {
 907                                            let context_server_id = context_server_id.clone();
 908
 909                                            move |settings, _| {
 910                                                settings
 911                                                    .project
 912                                                    .context_servers
 913                                                    .entry(context_server_id.0)
 914                                                    .or_insert_with(|| {
 915                                                        settings::ContextServerSettingsContent::Extension {
 916                                                            enabled: is_enabled,
 917                                                            remote: false,
 918                                                            settings: serde_json::json!({}),
 919                                                        }
 920                                                    })
 921                                                    .set_enabled(is_enabled);
 922                                            }
 923                                        });
 924                                    }
 925                                }),
 926                        ),
 927                    ),
 928            )
 929            .map(|parent| {
 930                if let Some(error) = error {
 931                    return parent.child(
 932                        h_flex()
 933                            .gap_2()
 934                            .pr_4()
 935                            .items_start()
 936                            .child(
 937                                h_flex()
 938                                    .flex_none()
 939                                    .h(window.line_height() / 1.6_f32)
 940                                    .justify_center()
 941                                    .child(
 942                                        Icon::new(IconName::XCircle)
 943                                            .size(IconSize::XSmall)
 944                                            .color(Color::Error),
 945                                    ),
 946                            )
 947                            .child(
 948                                div().w_full().child(
 949                                    Label::new(error)
 950                                        .buffer_font(cx)
 951                                        .color(Color::Muted)
 952                                        .size(LabelSize::Small),
 953                                ),
 954                            ),
 955                    );
 956                }
 957                parent
 958            })
 959    }
 960
 961    fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
 962        let agent_server_store = self.agent_server_store.read(cx);
 963
 964        let user_defined_agents = agent_server_store
 965            .external_agents()
 966            .filter(|name| {
 967                name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
 968            })
 969            .cloned()
 970            .collect::<Vec<_>>();
 971
 972        let user_defined_agents: Vec<_> = user_defined_agents
 973            .into_iter()
 974            .map(|name| {
 975                let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
 976                    AgentIcon::Path(icon_path)
 977                } else {
 978                    AgentIcon::Name(IconName::Sparkle)
 979                };
 980                let display_name = agent_server_store
 981                    .agent_display_name(&name)
 982                    .unwrap_or_else(|| name.0.clone());
 983                (name, icon, display_name)
 984            })
 985            .collect();
 986
 987        let add_agent_popover = PopoverMenu::new("add-agent-server-popover")
 988            .trigger(
 989                Button::new("add-agent", "Add Agent")
 990                    .style(ButtonStyle::Outlined)
 991                    .icon_position(IconPosition::Start)
 992                    .icon(IconName::Plus)
 993                    .icon_size(IconSize::Small)
 994                    .icon_color(Color::Muted)
 995                    .label_size(LabelSize::Small),
 996            )
 997            .menu({
 998                move |window, cx| {
 999                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
1000                        menu.entry("Install from Extensions", None, {
1001                            |window, cx| {
1002                                window.dispatch_action(
1003                                    zed_actions::Extensions {
1004                                        category_filter: Some(
1005                                            ExtensionCategoryFilter::AgentServers,
1006                                        ),
1007                                        id: None,
1008                                    }
1009                                    .boxed_clone(),
1010                                    cx,
1011                                )
1012                            }
1013                        })
1014                        .entry("Add Custom Agent", None, {
1015                            move |window, cx| {
1016                                if let Some(workspace) = window.root().flatten() {
1017                                    let workspace = workspace.downgrade();
1018                                    window
1019                                        .spawn(cx, async |cx| {
1020                                            open_new_agent_servers_entry_in_settings_editor(
1021                                                workspace, cx,
1022                                            )
1023                                            .await
1024                                        })
1025                                        .detach_and_log_err(cx);
1026                                }
1027                            }
1028                        })
1029                        .separator()
1030                        .header("Learn More")
1031                        .item(
1032                            ContextMenuEntry::new("Agent Servers Docs")
1033                                .icon(IconName::ArrowUpRight)
1034                                .icon_color(Color::Muted)
1035                                .icon_position(IconPosition::End)
1036                                .handler({
1037                                    move |window, cx| {
1038                                        window.dispatch_action(
1039                                            Box::new(OpenBrowser {
1040                                                url: zed_urls::agent_server_docs(cx),
1041                                            }),
1042                                            cx,
1043                                        );
1044                                    }
1045                                }),
1046                        )
1047                        .item(
1048                            ContextMenuEntry::new("ACP Docs")
1049                                .icon(IconName::ArrowUpRight)
1050                                .icon_color(Color::Muted)
1051                                .icon_position(IconPosition::End)
1052                                .handler({
1053                                    move |window, cx| {
1054                                        window.dispatch_action(
1055                                            Box::new(OpenBrowser {
1056                                                url: "https://agentclientprotocol.com/".into(),
1057                                            }),
1058                                            cx,
1059                                        );
1060                                    }
1061                                }),
1062                        )
1063                    }))
1064                }
1065            })
1066            .anchor(gpui::Corner::TopRight)
1067            .offset(gpui::Point {
1068                x: px(0.0),
1069                y: px(2.0),
1070            });
1071
1072        v_flex()
1073            .border_b_1()
1074            .border_color(cx.theme().colors().border)
1075            .child(
1076                v_flex()
1077                    .child(self.render_section_title(
1078                        "External Agents",
1079                        "All agents connected through the Agent Client Protocol.",
1080                        add_agent_popover.into_any_element(),
1081                    ))
1082                    .child(
1083                        v_flex()
1084                            .p_4()
1085                            .pt_0()
1086                            .gap_2()
1087                            .child(self.render_agent_server(
1088                                AgentIcon::Name(IconName::AiClaude),
1089                                "Claude Code",
1090                                "Claude Code",
1091                                false,
1092                                cx,
1093                            ))
1094                            .child(Divider::horizontal().color(DividerColor::BorderFaded))
1095                            .child(self.render_agent_server(
1096                                AgentIcon::Name(IconName::AiOpenAi),
1097                                "Codex CLI",
1098                                "Codex CLI",
1099                                false,
1100                                cx,
1101                            ))
1102                            .child(Divider::horizontal().color(DividerColor::BorderFaded))
1103                            .child(self.render_agent_server(
1104                                AgentIcon::Name(IconName::AiGemini),
1105                                "Gemini CLI",
1106                                "Gemini CLI",
1107                                false,
1108                                cx,
1109                            ))
1110                            .map(|mut parent| {
1111                                for (name, icon, display_name) in user_defined_agents {
1112                                    parent = parent
1113                                        .child(
1114                                            Divider::horizontal().color(DividerColor::BorderFaded),
1115                                        )
1116                                        .child(self.render_agent_server(
1117                                            icon,
1118                                            name,
1119                                            display_name,
1120                                            true,
1121                                            cx,
1122                                        ));
1123                                }
1124                                parent
1125                            }),
1126                    ),
1127            )
1128    }
1129
1130    fn render_agent_server(
1131        &self,
1132        icon: AgentIcon,
1133        id: impl Into<SharedString>,
1134        display_name: impl Into<SharedString>,
1135        external: bool,
1136        cx: &mut Context<Self>,
1137    ) -> impl IntoElement {
1138        let id = id.into();
1139        let display_name = display_name.into();
1140
1141        let icon = match icon {
1142            AgentIcon::Name(icon_name) => Icon::new(icon_name)
1143                .size(IconSize::Small)
1144                .color(Color::Muted),
1145            AgentIcon::Path(icon_path) => Icon::from_external_svg(icon_path)
1146                .size(IconSize::Small)
1147                .color(Color::Muted),
1148        };
1149
1150        let tooltip_id = SharedString::new(format!("agent-source-{}", id));
1151        let tooltip_message = format!(
1152            "The {} agent was installed from an extension.",
1153            display_name
1154        );
1155
1156        let agent_server_name = ExternalAgentServerName(id.clone());
1157
1158        let uninstall_btn_id = SharedString::from(format!("uninstall-{}", id));
1159        let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash)
1160            .icon_color(Color::Muted)
1161            .icon_size(IconSize::Small)
1162            .tooltip(Tooltip::text("Uninstall Agent Extension"))
1163            .on_click(cx.listener(move |this, _, _window, cx| {
1164                let agent_name = agent_server_name.clone();
1165
1166                if let Some(ext_id) = this.agent_server_store.update(cx, |store, _cx| {
1167                    store.get_extension_id_for_agent(&agent_name)
1168                }) {
1169                    ExtensionStore::global(cx)
1170                        .update(cx, |store, cx| store.uninstall_extension(ext_id, cx))
1171                        .detach_and_log_err(cx);
1172                }
1173            }));
1174
1175        h_flex()
1176            .gap_1()
1177            .justify_between()
1178            .child(
1179                h_flex()
1180                    .gap_1p5()
1181                    .child(icon)
1182                    .child(Label::new(display_name))
1183                    .when(external, |this| {
1184                        this.child(
1185                            div()
1186                                .id(tooltip_id)
1187                                .flex_none()
1188                                .tooltip(Tooltip::text(tooltip_message))
1189                                .child(
1190                                    Icon::new(IconName::ZedSrcExtension)
1191                                        .size(IconSize::Small)
1192                                        .color(Color::Muted),
1193                                ),
1194                        )
1195                    })
1196                    .child(
1197                        Icon::new(IconName::Check)
1198                            .color(Color::Success)
1199                            .size(IconSize::Small),
1200                    ),
1201            )
1202            .when(external, |this| this.child(uninstall_button))
1203    }
1204}
1205
1206impl Render for AgentConfiguration {
1207    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1208        v_flex()
1209            .id("assistant-configuration")
1210            .key_context("AgentConfiguration")
1211            .track_focus(&self.focus_handle(cx))
1212            .relative()
1213            .size_full()
1214            .pb_8()
1215            .bg(cx.theme().colors().panel_background)
1216            .child(
1217                div()
1218                    .size_full()
1219                    .child(
1220                        v_flex()
1221                            .id("assistant-configuration-content")
1222                            .track_scroll(&self.scroll_handle)
1223                            .size_full()
1224                            .overflow_y_scroll()
1225                            .child(self.render_agent_servers_section(cx))
1226                            .child(self.render_context_servers_section(window, cx))
1227                            .child(self.render_provider_configuration_section(cx)),
1228                    )
1229                    .vertical_scrollbar_for(&self.scroll_handle, window, cx),
1230            )
1231    }
1232}
1233
1234fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool {
1235    manifest.context_servers.len() == 1
1236        && manifest.themes.is_empty()
1237        && manifest.icon_themes.is_empty()
1238        && manifest.languages.is_empty()
1239        && manifest.grammars.is_empty()
1240        && manifest.language_servers.is_empty()
1241        && manifest.slash_commands.is_empty()
1242        && manifest.snippets.is_none()
1243        && manifest.debug_locators.is_empty()
1244}
1245
1246pub(crate) fn resolve_extension_for_context_server(
1247    id: &ContextServerId,
1248    cx: &App,
1249) -> Option<(Arc<str>, Arc<ExtensionManifest>)> {
1250    ExtensionStore::global(cx)
1251        .read(cx)
1252        .installed_extensions()
1253        .iter()
1254        .find(|(_, entry)| entry.manifest.context_servers.contains_key(&id.0))
1255        .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
1256}
1257
1258// This notification appears when trying to delete
1259// an MCP server extension that not only provides
1260// the server, but other things, too, like language servers and more.
1261fn show_unable_to_uninstall_extension_with_context_server(
1262    workspace: &mut Workspace,
1263    id: ContextServerId,
1264    cx: &mut App,
1265) {
1266    let workspace_handle = workspace.weak_handle();
1267    let context_server_id = id.clone();
1268
1269    let status_toast = StatusToast::new(
1270        format!(
1271            "The {} extension provides more than just the MCP server. Proceed to uninstall anyway?",
1272            id.0
1273        ),
1274        cx,
1275        move |this, _cx| {
1276            let workspace_handle = workspace_handle.clone();
1277
1278            this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
1279                .dismiss_button(true)
1280                .action("Uninstall", move |_, _cx| {
1281                    if let Some((extension_id, _)) =
1282                        resolve_extension_for_context_server(&context_server_id, _cx)
1283                    {
1284                        ExtensionStore::global(_cx).update(_cx, |store, cx| {
1285                            store
1286                                .uninstall_extension(extension_id, cx)
1287                                .detach_and_log_err(cx);
1288                        });
1289
1290                        workspace_handle
1291                            .update(_cx, |workspace, cx| {
1292                                let fs = workspace.app_state().fs.clone();
1293                                cx.spawn({
1294                                    let context_server_id = context_server_id.clone();
1295                                    async move |_workspace_handle, cx| {
1296                                        cx.update(|cx| {
1297                                            update_settings_file(fs, cx, move |settings, _| {
1298                                                settings
1299                                                    .project
1300                                                    .context_servers
1301                                                    .remove(&context_server_id.0);
1302                                            });
1303                                        });
1304                                        anyhow::Ok(())
1305                                    }
1306                                })
1307                                .detach_and_log_err(cx);
1308                            })
1309                            .log_err();
1310                    }
1311                })
1312        },
1313    );
1314
1315    workspace.toggle_status_toast(status_toast, cx);
1316}
1317
1318async fn open_new_agent_servers_entry_in_settings_editor(
1319    workspace: WeakEntity<Workspace>,
1320    cx: &mut AsyncWindowContext,
1321) -> Result<()> {
1322    let settings_editor = workspace
1323        .update_in(cx, |_, window, cx| {
1324            create_and_open_local_file(paths::settings_file(), window, cx, || {
1325                settings::initial_user_settings_content().as_ref().into()
1326            })
1327        })?
1328        .await?
1329        .downcast::<Editor>()
1330        .unwrap();
1331
1332    settings_editor
1333        .downgrade()
1334        .update_in(cx, |item, window, cx| {
1335            let text = item.buffer().read(cx).snapshot(cx).text();
1336
1337            let settings = cx.global::<SettingsStore>();
1338
1339            let mut unique_server_name = None;
1340            let edits = settings.edits_for_update(&text, |settings| {
1341                let server_name: Option<String> = (0..u8::MAX)
1342                    .map(|i| {
1343                        if i == 0 {
1344                            "your_agent".to_string()
1345                        } else {
1346                            format!("your_agent_{}", i)
1347                        }
1348                    })
1349                    .find(|name| {
1350                        !settings
1351                            .agent_servers
1352                            .as_ref()
1353                            .is_some_and(|agent_servers| {
1354                                agent_servers.custom.contains_key(name.as_str())
1355                            })
1356                    });
1357                if let Some(server_name) = server_name {
1358                    unique_server_name = Some(SharedString::from(server_name.clone()));
1359                    settings
1360                        .agent_servers
1361                        .get_or_insert_default()
1362                        .custom
1363                        .insert(
1364                            server_name,
1365                            settings::CustomAgentServerSettings::Custom {
1366                                path: "path_to_executable".into(),
1367                                args: vec![],
1368                                env: Some(HashMap::default()),
1369                                default_mode: None,
1370                                default_model: None,
1371                                favorite_models: vec![],
1372                                default_config_options: Default::default(),
1373                                favorite_config_option_values: Default::default(),
1374                            },
1375                        );
1376                }
1377            });
1378
1379            if edits.is_empty() {
1380                return;
1381            }
1382
1383            let ranges = edits
1384                .iter()
1385                .map(|(range, _)| range.clone())
1386                .collect::<Vec<_>>();
1387
1388            item.edit(
1389                edits.into_iter().map(|(range, s)| {
1390                    (
1391                        MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
1392                        s,
1393                    )
1394                }),
1395                cx,
1396            );
1397            if let Some((unique_server_name, buffer)) =
1398                unique_server_name.zip(item.buffer().read(cx).as_singleton())
1399            {
1400                let snapshot = buffer.read(cx).snapshot();
1401                if let Some(range) =
1402                    find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
1403                {
1404                    item.change_selections(
1405                        SelectionEffects::scroll(Autoscroll::newest()),
1406                        window,
1407                        cx,
1408                        |selections| {
1409                            selections.select_ranges(vec![
1410                                MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
1411                            ]);
1412                        },
1413                    );
1414                }
1415            }
1416        })
1417}
1418
1419fn find_text_in_buffer(
1420    text: &str,
1421    start: usize,
1422    snapshot: &language::BufferSnapshot,
1423) -> Option<Range<usize>> {
1424    let chars = text.chars().collect::<Vec<char>>();
1425
1426    let mut offset = start;
1427    let mut char_offset = 0;
1428    for c in snapshot.chars_at(start) {
1429        if char_offset >= chars.len() {
1430            break;
1431        }
1432        offset += 1;
1433
1434        if c == chars[char_offset] {
1435            char_offset += 1;
1436        } else {
1437            char_offset = 0;
1438        }
1439    }
1440
1441    if char_offset == chars.len() {
1442        Some(offset.saturating_sub(chars.len())..offset)
1443    } else {
1444        None
1445    }
1446}
1447
1448// OpenAI-compatible providers are user-configured and can be removed,
1449// whereas built-in providers (like Anthropic, OpenAI, Google, etc.) can't.
1450//
1451// If in the future we have more "API-compatible-type" of providers,
1452// they should be included here as removable providers.
1453fn is_removable_provider(provider_id: &LanguageModelProviderId, cx: &App) -> bool {
1454    AllLanguageModelSettings::get_global(cx)
1455        .openai_compatible
1456        .contains_key(provider_id.0.as_ref())
1457}