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    LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
  26};
  27use language_models::AllLanguageModelSettings;
  28use notifications::status_toast::{StatusToast, ToastIcon};
  29use project::{
  30    agent_server_store::{
  31        AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME,
  32    },
  33    context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
  34};
  35use settings::{Settings, SettingsStore, update_settings_file};
  36use ui::{
  37    ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, Divider,
  38    DividerColor, ElevationIndex, Indicator, LabelSize, PopoverMenu, Switch, Tooltip,
  39    WithScrollbar, prelude::*,
  40};
  41use util::ResultExt as _;
  42use workspace::{Workspace, create_and_open_local_file};
  43use zed_actions::{ExtensionCategoryFilter, OpenBrowser};
  44
  45pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
  46pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
  47pub(crate) use manage_profiles_modal::ManageProfilesModal;
  48
  49use crate::agent_configuration::add_llm_provider_modal::{
  50    AddLlmProviderModal, LlmCompatibleProvider,
  51};
  52
  53pub struct AgentConfiguration {
  54    fs: Arc<dyn Fs>,
  55    language_registry: Arc<LanguageRegistry>,
  56    agent_server_store: Entity<AgentServerStore>,
  57    workspace: WeakEntity<Workspace>,
  58    focus_handle: FocusHandle,
  59    configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
  60    context_server_store: Entity<ContextServerStore>,
  61    expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>,
  62    context_server_registry: Entity<ContextServerRegistry>,
  63    _registry_subscription: Subscription,
  64    scroll_handle: ScrollHandle,
  65    _check_for_gemini: Task<()>,
  66}
  67
  68impl AgentConfiguration {
  69    pub fn new(
  70        fs: Arc<dyn Fs>,
  71        agent_server_store: Entity<AgentServerStore>,
  72        context_server_store: Entity<ContextServerStore>,
  73        context_server_registry: Entity<ContextServerRegistry>,
  74        language_registry: Arc<LanguageRegistry>,
  75        workspace: WeakEntity<Workspace>,
  76        window: &mut Window,
  77        cx: &mut Context<Self>,
  78    ) -> Self {
  79        let focus_handle = cx.focus_handle();
  80
  81        let registry_subscription = cx.subscribe_in(
  82            &LanguageModelRegistry::global(cx),
  83            window,
  84            |this, _, event: &language_model::Event, window, cx| match event {
  85                language_model::Event::AddedProvider(provider_id) => {
  86                    let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
  87                    if let Some(provider) = provider {
  88                        this.add_provider_configuration_view(&provider, window, cx);
  89                    }
  90                }
  91                language_model::Event::RemovedProvider(provider_id) => {
  92                    this.remove_provider_configuration_view(provider_id);
  93                }
  94                _ => {}
  95            },
  96        );
  97
  98        cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
  99            .detach();
 100
 101        let mut this = Self {
 102            fs,
 103            language_registry,
 104            workspace,
 105            focus_handle,
 106            configuration_views_by_provider: HashMap::default(),
 107            agent_server_store,
 108            context_server_store,
 109            expanded_provider_configurations: HashMap::default(),
 110            context_server_registry,
 111            _registry_subscription: registry_subscription,
 112            scroll_handle: ScrollHandle::new(),
 113            _check_for_gemini: Task::ready(()),
 114        };
 115        this.build_provider_configuration_views(window, cx);
 116        this
 117    }
 118
 119    fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 120        let providers = LanguageModelRegistry::read_global(cx).providers();
 121        for provider in providers {
 122            self.add_provider_configuration_view(&provider, window, cx);
 123        }
 124    }
 125
 126    fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
 127        self.configuration_views_by_provider.remove(provider_id);
 128        self.expanded_provider_configurations.remove(provider_id);
 129    }
 130
 131    fn add_provider_configuration_view(
 132        &mut self,
 133        provider: &Arc<dyn LanguageModelProvider>,
 134        window: &mut Window,
 135        cx: &mut Context<Self>,
 136    ) {
 137        let configuration_view = provider.configuration_view(
 138            language_model::ConfigurationViewTargetAgent::ZedAgent,
 139            window,
 140            cx,
 141        );
 142        self.configuration_views_by_provider
 143            .insert(provider.id(), configuration_view);
 144    }
 145}
 146
 147impl Focusable for AgentConfiguration {
 148    fn focus_handle(&self, _: &App) -> FocusHandle {
 149        self.focus_handle.clone()
 150    }
 151}
 152
 153pub enum AssistantConfigurationEvent {
 154    NewThread(Arc<dyn LanguageModelProvider>),
 155}
 156
 157impl EventEmitter<AssistantConfigurationEvent> for AgentConfiguration {}
 158
 159enum AgentIcon {
 160    Name(IconName),
 161    Path(SharedString),
 162}
 163
 164impl AgentConfiguration {
 165    fn render_section_title(
 166        &mut self,
 167        title: impl Into<SharedString>,
 168        description: impl Into<SharedString>,
 169        menu: AnyElement,
 170    ) -> impl IntoElement {
 171        h_flex()
 172            .p_4()
 173            .pb_0()
 174            .mb_2p5()
 175            .items_start()
 176            .justify_between()
 177            .child(
 178                v_flex()
 179                    .w_full()
 180                    .gap_0p5()
 181                    .child(
 182                        h_flex()
 183                            .pr_1()
 184                            .w_full()
 185                            .gap_2()
 186                            .justify_between()
 187                            .flex_wrap()
 188                            .child(Headline::new(title.into()))
 189                            .child(menu),
 190                    )
 191                    .child(Label::new(description.into()).color(Color::Muted)),
 192            )
 193    }
 194
 195    fn render_provider_configuration_block(
 196        &mut self,
 197        provider: &Arc<dyn LanguageModelProvider>,
 198        cx: &mut Context<Self>,
 199    ) -> impl IntoElement + use<> {
 200        let provider_id = provider.id().0;
 201        let provider_name = provider.name().0;
 202        let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}"));
 203
 204        let configuration_view = self
 205            .configuration_views_by_provider
 206            .get(&provider.id())
 207            .cloned();
 208
 209        let is_expanded = self
 210            .expanded_provider_configurations
 211            .get(&provider.id())
 212            .copied()
 213            .unwrap_or(false);
 214
 215        let is_zed_provider = provider.id() == ZED_CLOUD_PROVIDER_ID;
 216        let current_plan = if is_zed_provider {
 217            self.workspace
 218                .upgrade()
 219                .and_then(|workspace| workspace.read(cx).user_store().read(cx).plan())
 220        } else {
 221            None
 222        };
 223
 224        let is_signed_in = self
 225            .workspace
 226            .read_with(cx, |workspace, _| {
 227                !workspace.client().status().borrow().is_signed_out()
 228            })
 229            .unwrap_or(false);
 230
 231        v_flex()
 232            .w_full()
 233            .when(is_expanded, |this| this.mb_2())
 234            .child(
 235                div()
 236                    .px_2()
 237                    .child(Divider::horizontal().color(DividerColor::BorderFaded)),
 238            )
 239            .child(
 240                h_flex()
 241                    .map(|this| {
 242                        if is_expanded {
 243                            this.mt_2().mb_1()
 244                        } else {
 245                            this.my_2()
 246                        }
 247                    })
 248                    .w_full()
 249                    .justify_between()
 250                    .child(
 251                        h_flex()
 252                            .id(provider_id_string.clone())
 253                            .px_2()
 254                            .py_0p5()
 255                            .w_full()
 256                            .justify_between()
 257                            .rounded_sm()
 258                            .hover(|hover| hover.bg(cx.theme().colors().element_hover))
 259                            .child(
 260                                h_flex()
 261                                    .w_full()
 262                                    .gap_1p5()
 263                                    .child(
 264                                        Icon::new(provider.icon())
 265                                            .size(IconSize::Small)
 266                                            .color(Color::Muted),
 267                                    )
 268                                    .child(
 269                                        h_flex()
 270                                            .w_full()
 271                                            .gap_1()
 272                                            .child(Label::new(provider_name.clone()))
 273                                            .map(|this| {
 274                                                if is_zed_provider && is_signed_in {
 275                                                    this.child(
 276                                                        self.render_zed_plan_info(current_plan, cx),
 277                                                    )
 278                                                } else {
 279                                                    this.when(
 280                                                        provider.is_authenticated(cx)
 281                                                            && !is_expanded,
 282                                                        |parent| {
 283                                                            parent.child(
 284                                                                Icon::new(IconName::Check)
 285                                                                    .color(Color::Success),
 286                                                            )
 287                                                        },
 288                                                    )
 289                                                }
 290                                            }),
 291                                    ),
 292                            )
 293                            .child(
 294                                Disclosure::new(provider_id_string, is_expanded)
 295                                    .opened_icon(IconName::ChevronUp)
 296                                    .closed_icon(IconName::ChevronDown),
 297                            )
 298                            .on_click(cx.listener({
 299                                let provider_id = provider.id();
 300                                move |this, _event, _window, _cx| {
 301                                    let is_expanded = this
 302                                        .expanded_provider_configurations
 303                                        .entry(provider_id.clone())
 304                                        .or_insert(false);
 305
 306                                    *is_expanded = !*is_expanded;
 307                                }
 308                            })),
 309                    ),
 310            )
 311            .child(
 312                v_flex()
 313                    .w_full()
 314                    .px_2()
 315                    .gap_1()
 316                    .when(is_expanded, |parent| match configuration_view {
 317                        Some(configuration_view) => parent.child(configuration_view),
 318                        None => parent.child(Label::new(format!(
 319                            "No configuration view for {provider_name}",
 320                        ))),
 321                    })
 322                    .when(is_expanded && provider.is_authenticated(cx), |parent| {
 323                        parent.child(
 324                            Button::new(
 325                                SharedString::from(format!("new-thread-{provider_id}")),
 326                                "Start New Thread",
 327                            )
 328                            .full_width()
 329                            .style(ButtonStyle::Outlined)
 330                            .layer(ElevationIndex::ModalSurface)
 331                            .icon_position(IconPosition::Start)
 332                            .icon(IconName::Thread)
 333                            .icon_size(IconSize::Small)
 334                            .icon_color(Color::Muted)
 335                            .label_size(LabelSize::Small)
 336                            .on_click(cx.listener({
 337                                let provider = provider.clone();
 338                                move |_this, _event, _window, cx| {
 339                                    cx.emit(AssistantConfigurationEvent::NewThread(
 340                                        provider.clone(),
 341                                    ))
 342                                }
 343                            })),
 344                        )
 345                    })
 346                    .when(
 347                        is_expanded && is_removable_provider(&provider.id(), cx),
 348                        |this| {
 349                            this.child(
 350                                Button::new(
 351                                    SharedString::from(format!("delete-provider-{provider_id}")),
 352                                    "Remove Provider",
 353                                )
 354                                .full_width()
 355                                .style(ButtonStyle::Outlined)
 356                                .icon_position(IconPosition::Start)
 357                                .icon(IconName::Trash)
 358                                .icon_size(IconSize::Small)
 359                                .icon_color(Color::Muted)
 360                                .label_size(LabelSize::Small)
 361                                .on_click(cx.listener({
 362                                    let provider = provider.clone();
 363                                    move |this, _event, window, cx| {
 364                                        this.delete_provider(provider.clone(), window, cx);
 365                                    }
 366                                })),
 367                            )
 368                        },
 369                    ),
 370            )
 371    }
 372
 373    fn delete_provider(
 374        &mut self,
 375        provider: Arc<dyn LanguageModelProvider>,
 376        window: &mut Window,
 377        cx: &mut Context<Self>,
 378    ) {
 379        let fs = self.fs.clone();
 380        let provider_id = provider.id();
 381
 382        cx.spawn_in(window, async move |_, cx| {
 383            cx.update(|_window, cx| {
 384                update_settings_file(fs.clone(), cx, {
 385                    let provider_id = provider_id.clone();
 386                    move |settings, _| {
 387                        if let Some(ref mut openai_compatible) = settings
 388                            .language_models
 389                            .as_mut()
 390                            .and_then(|lm| lm.openai_compatible.as_mut())
 391                        {
 392                            let key_to_remove: Arc<str> = Arc::from(provider_id.0.as_ref());
 393                            openai_compatible.remove(&key_to_remove);
 394                        }
 395                    }
 396                });
 397            })
 398            .log_err();
 399
 400            cx.update(|_window, cx| {
 401                LanguageModelRegistry::global(cx).update(cx, {
 402                    let provider_id = provider_id.clone();
 403                    move |registry, cx| {
 404                        registry.unregister_provider(provider_id, cx);
 405                    }
 406                })
 407            })
 408            .log_err();
 409
 410            anyhow::Ok(())
 411        })
 412        .detach_and_log_err(cx);
 413    }
 414
 415    fn render_provider_configuration_section(
 416        &mut self,
 417        cx: &mut Context<Self>,
 418    ) -> impl IntoElement {
 419        let providers = LanguageModelRegistry::read_global(cx).providers();
 420
 421        let popover_menu = PopoverMenu::new("add-provider-popover")
 422            .trigger(
 423                Button::new("add-provider", "Add Provider")
 424                    .style(ButtonStyle::Outlined)
 425                    .icon_position(IconPosition::Start)
 426                    .icon(IconName::Plus)
 427                    .icon_size(IconSize::Small)
 428                    .icon_color(Color::Muted)
 429                    .label_size(LabelSize::Small),
 430            )
 431            .menu({
 432                let workspace = self.workspace.clone();
 433                move |window, cx| {
 434                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
 435                        menu.header("Compatible APIs").entry("OpenAI", None, {
 436                            let workspace = workspace.clone();
 437                            move |window, cx| {
 438                                workspace
 439                                    .update(cx, |workspace, cx| {
 440                                        AddLlmProviderModal::toggle(
 441                                            LlmCompatibleProvider::OpenAi,
 442                                            workspace,
 443                                            window,
 444                                            cx,
 445                                        );
 446                                    })
 447                                    .log_err();
 448                            }
 449                        })
 450                    }))
 451                }
 452            })
 453            .anchor(gpui::Corner::TopRight)
 454            .offset(gpui::Point {
 455                x: px(0.0),
 456                y: px(2.0),
 457            });
 458
 459        v_flex()
 460            .w_full()
 461            .child(self.render_section_title(
 462                "LLM Providers",
 463                "Add at least one provider to use AI-powered features with Zed's native agent.",
 464                popover_menu.into_any_element(),
 465            ))
 466            .child(
 467                div()
 468                    .w_full()
 469                    .pl(DynamicSpacing::Base08.rems(cx))
 470                    .pr(DynamicSpacing::Base20.rems(cx))
 471                    .children(
 472                        providers.into_iter().map(|provider| {
 473                            self.render_provider_configuration_block(&provider, cx)
 474                        }),
 475                    ),
 476            )
 477    }
 478
 479    fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement {
 480        if let Some(plan) = plan {
 481            let free_chip_bg = cx
 482                .theme()
 483                .colors()
 484                .editor_background
 485                .opacity(0.5)
 486                .blend(cx.theme().colors().text_accent.opacity(0.05));
 487
 488            let pro_chip_bg = cx
 489                .theme()
 490                .colors()
 491                .editor_background
 492                .opacity(0.5)
 493                .blend(cx.theme().colors().text_accent.opacity(0.2));
 494
 495            let (plan_name, label_color, bg_color) = match plan {
 496                Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree) => {
 497                    ("Free", Color::Default, free_chip_bg)
 498                }
 499                Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial) => {
 500                    ("Pro Trial", Color::Accent, pro_chip_bg)
 501                }
 502                Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro) => {
 503                    ("Pro", Color::Accent, pro_chip_bg)
 504                }
 505            };
 506
 507            Chip::new(plan_name.to_string())
 508                .bg_color(bg_color)
 509                .label_color(label_color)
 510                .into_any_element()
 511        } else {
 512            div().into_any_element()
 513        }
 514    }
 515
 516    fn render_context_servers_section(
 517        &mut self,
 518        window: &mut Window,
 519        cx: &mut Context<Self>,
 520    ) -> impl IntoElement {
 521        let mut context_server_ids = self
 522            .context_server_store
 523            .read(cx)
 524            .server_ids(cx)
 525            .into_iter()
 526            .collect::<Vec<_>>();
 527
 528        // Sort context servers: ones without mcp-server- prefix first, then prefixed ones
 529        context_server_ids.sort_by(|a, b| {
 530            const MCP_PREFIX: &str = "mcp-server-";
 531            match (a.0.strip_prefix(MCP_PREFIX), b.0.strip_prefix(MCP_PREFIX)) {
 532                // If one has mcp-server- prefix and other doesn't, non-mcp comes first
 533                (Some(_), None) => std::cmp::Ordering::Greater,
 534                (None, Some(_)) => std::cmp::Ordering::Less,
 535                // If both have same prefix status, sort by appropriate key
 536                (Some(a), Some(b)) => a.cmp(b),
 537                (None, None) => a.0.cmp(&b.0),
 538            }
 539        });
 540
 541        let add_server_popover = PopoverMenu::new("add-server-popover")
 542            .trigger(
 543                Button::new("add-server", "Add Server")
 544                    .style(ButtonStyle::Outlined)
 545                    .icon_position(IconPosition::Start)
 546                    .icon(IconName::Plus)
 547                    .icon_size(IconSize::Small)
 548                    .icon_color(Color::Muted)
 549                    .label_size(LabelSize::Small),
 550            )
 551            .menu({
 552                move |window, cx| {
 553                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
 554                        menu.entry("Add Custom Server", None, {
 555                            |window, cx| {
 556                                window.dispatch_action(crate::AddContextServer.boxed_clone(), cx)
 557                            }
 558                        })
 559                        .entry("Install from Extensions", None, {
 560                            |window, cx| {
 561                                window.dispatch_action(
 562                                    zed_actions::Extensions {
 563                                        category_filter: Some(
 564                                            ExtensionCategoryFilter::ContextServers,
 565                                        ),
 566                                        id: None,
 567                                    }
 568                                    .boxed_clone(),
 569                                    cx,
 570                                )
 571                            }
 572                        })
 573                    }))
 574                }
 575            })
 576            .anchor(gpui::Corner::TopRight)
 577            .offset(gpui::Point {
 578                x: px(0.0),
 579                y: px(2.0),
 580            });
 581
 582        v_flex()
 583            .border_b_1()
 584            .border_color(cx.theme().colors().border)
 585            .child(self.render_section_title(
 586                "Model Context Protocol (MCP) Servers",
 587                "All MCP servers connected directly or via a Zed extension.",
 588                add_server_popover.into_any_element(),
 589            ))
 590            .child(
 591                v_flex()
 592                    .pl_4()
 593                    .pb_4()
 594                    .pr_5()
 595                    .w_full()
 596                    .gap_1()
 597                    .map(|mut parent| {
 598                        if context_server_ids.is_empty() {
 599                            parent.child(
 600                                h_flex()
 601                                    .p_4()
 602                                    .justify_center()
 603                                    .border_1()
 604                                    .border_dashed()
 605                                    .border_color(cx.theme().colors().border.opacity(0.6))
 606                                    .rounded_sm()
 607                                    .child(
 608                                        Label::new("No MCP servers added yet.")
 609                                            .color(Color::Muted)
 610                                            .size(LabelSize::Small),
 611                                    ),
 612                            )
 613                        } else {
 614                            for (index, context_server_id) in
 615                                context_server_ids.into_iter().enumerate()
 616                            {
 617                                if index > 0 {
 618                                    parent = parent.child(
 619                                        Divider::horizontal()
 620                                            .color(DividerColor::BorderFaded)
 621                                            .into_any_element(),
 622                                    );
 623                                }
 624                                parent = parent.child(self.render_context_server(
 625                                    context_server_id,
 626                                    window,
 627                                    cx,
 628                                ));
 629                            }
 630                            parent
 631                        }
 632                    }),
 633            )
 634    }
 635
 636    fn render_context_server(
 637        &self,
 638        context_server_id: ContextServerId,
 639        window: &mut Window,
 640        cx: &mut Context<Self>,
 641    ) -> impl use<> + IntoElement {
 642        let server_status = self
 643            .context_server_store
 644            .read(cx)
 645            .status_for_server(&context_server_id)
 646            .unwrap_or(ContextServerStatus::Stopped);
 647        let server_configuration = self
 648            .context_server_store
 649            .read(cx)
 650            .configuration_for_server(&context_server_id);
 651
 652        let is_running = matches!(server_status, ContextServerStatus::Running);
 653        let item_id = SharedString::from(context_server_id.0.clone());
 654        // Servers without a configuration can only be provided by extensions.
 655        let provided_by_extension = server_configuration.as_ref().is_none_or(|config| {
 656            matches!(
 657                config.as_ref(),
 658                ContextServerConfiguration::Extension { .. }
 659            )
 660        });
 661
 662        let error = if let ContextServerStatus::Error(error) = server_status.clone() {
 663            Some(error)
 664        } else {
 665            None
 666        };
 667
 668        let tool_count = self
 669            .context_server_registry
 670            .read(cx)
 671            .tools_for_server(&context_server_id)
 672            .count();
 673
 674        let (source_icon, source_tooltip) = if provided_by_extension {
 675            (
 676                IconName::ZedSrcExtension,
 677                "This MCP server was installed from an extension.",
 678            )
 679        } else {
 680            (
 681                IconName::ZedSrcCustom,
 682                "This custom MCP server was installed directly.",
 683            )
 684        };
 685
 686        let (status_indicator, tooltip_text) = match server_status {
 687            ContextServerStatus::Starting => (
 688                Icon::new(IconName::LoadCircle)
 689                    .size(IconSize::XSmall)
 690                    .color(Color::Accent)
 691                    .with_keyed_rotate_animation(
 692                        SharedString::from(format!("{}-starting", context_server_id.0)),
 693                        3,
 694                    )
 695                    .into_any_element(),
 696                "Server is starting.",
 697            ),
 698            ContextServerStatus::Running => (
 699                Indicator::dot().color(Color::Success).into_any_element(),
 700                "Server is active.",
 701            ),
 702            ContextServerStatus::Error(_) => (
 703                Indicator::dot().color(Color::Error).into_any_element(),
 704                "Server has an error.",
 705            ),
 706            ContextServerStatus::Stopped => (
 707                Indicator::dot().color(Color::Muted).into_any_element(),
 708                "Server is stopped.",
 709            ),
 710        };
 711        let is_remote = server_configuration
 712            .as_ref()
 713            .map(|config| matches!(config.as_ref(), ContextServerConfiguration::Http { .. }))
 714            .unwrap_or(false);
 715        let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu")
 716            .trigger_with_tooltip(
 717                IconButton::new("context-server-config-menu", IconName::Settings)
 718                    .icon_color(Color::Muted)
 719                    .icon_size(IconSize::Small),
 720                Tooltip::text("Configure MCP Server"),
 721            )
 722            .anchor(Corner::TopRight)
 723            .menu({
 724                let fs = self.fs.clone();
 725                let context_server_id = context_server_id.clone();
 726                let language_registry = self.language_registry.clone();
 727                let workspace = self.workspace.clone();
 728                let context_server_registry = self.context_server_registry.clone();
 729
 730                move |window, cx| {
 731                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
 732                        menu.entry("Configure Server", None, {
 733                            let context_server_id = context_server_id.clone();
 734                            let language_registry = language_registry.clone();
 735                            let workspace = workspace.clone();
 736                            move |window, cx| {
 737                                if is_remote {
 738                                    crate::agent_configuration::configure_context_server_modal::ConfigureContextServerModal::show_modal_for_existing_server(
 739                                        context_server_id.clone(),
 740                                        language_registry.clone(),
 741                                        workspace.clone(),
 742                                        window,
 743                                        cx,
 744                                    )
 745                                    .detach();
 746                                } else {
 747                                    ConfigureContextServerModal::show_modal_for_existing_server(
 748                                        context_server_id.clone(),
 749                                        language_registry.clone(),
 750                                        workspace.clone(),
 751                                        window,
 752                                        cx,
 753                                    )
 754                                    .detach();
 755                                }
 756                            }
 757                        }).when(tool_count > 0, |this| this.entry("View Tools", None, {
 758                            let context_server_id = context_server_id.clone();
 759                            let context_server_registry = context_server_registry.clone();
 760                            let workspace = workspace.clone();
 761                            move |window, cx| {
 762                                let context_server_id = context_server_id.clone();
 763                                workspace.update(cx, |workspace, cx| {
 764                                    ConfigureContextServerToolsModal::toggle(
 765                                        context_server_id,
 766                                        context_server_registry.clone(),
 767                                        workspace,
 768                                        window,
 769                                        cx,
 770                                    );
 771                                })
 772                                .ok();
 773                            }
 774                        }))
 775                        .separator()
 776                        .entry("Uninstall", None, {
 777                            let fs = fs.clone();
 778                            let context_server_id = context_server_id.clone();
 779                            let workspace = workspace.clone();
 780                            move |_, cx| {
 781                                let uninstall_extension_task = match (
 782                                    provided_by_extension,
 783                                    resolve_extension_for_context_server(&context_server_id, cx),
 784                                ) {
 785                                    (true, Some((id, manifest))) => {
 786                                        if extension_only_provides_context_server(manifest.as_ref())
 787                                        {
 788                                            ExtensionStore::global(cx).update(cx, |store, cx| {
 789                                                store.uninstall_extension(id, cx)
 790                                            })
 791                                        } else {
 792                                            workspace.update(cx, |workspace, cx| {
 793                                                show_unable_to_uninstall_extension_with_context_server(workspace, context_server_id.clone(), cx);
 794                                            }).log_err();
 795                                            Task::ready(Ok(()))
 796                                        }
 797                                    }
 798                                    _ => Task::ready(Ok(())),
 799                                };
 800
 801                                cx.spawn({
 802                                    let fs = fs.clone();
 803                                    let context_server_id = context_server_id.clone();
 804                                    async move |cx| {
 805                                        uninstall_extension_task.await?;
 806                                        cx.update(|cx| {
 807                                            update_settings_file(
 808                                                fs.clone(),
 809                                                cx,
 810                                                {
 811                                                    let context_server_id =
 812                                                        context_server_id.clone();
 813                                                    move |settings, _| {
 814                                                        settings.project
 815                                                            .context_servers
 816                                                            .remove(&context_server_id.0);
 817                                                    }
 818                                                },
 819                                            )
 820                                        })
 821                                    }
 822                                })
 823                                .detach_and_log_err(cx);
 824                            }
 825                        })
 826                    }))
 827                }
 828            });
 829
 830        v_flex()
 831            .id(item_id.clone())
 832            .child(
 833                h_flex()
 834                    .justify_between()
 835                    .child(
 836                        h_flex()
 837                            .flex_1()
 838                            .min_w_0()
 839                            .child(
 840                                h_flex()
 841                                    .id(format!("tooltip-{}", item_id))
 842                                    .h_full()
 843                                    .w_3()
 844                                    .mr_2()
 845                                    .justify_center()
 846                                    .tooltip(Tooltip::text(tooltip_text))
 847                                    .child(status_indicator),
 848                            )
 849                            .child(Label::new(item_id).truncate())
 850                            .child(
 851                                div()
 852                                    .id("extension-source")
 853                                    .mt_0p5()
 854                                    .mx_1()
 855                                    .flex_none()
 856                                    .tooltip(Tooltip::text(source_tooltip))
 857                                    .child(
 858                                        Icon::new(source_icon)
 859                                            .size(IconSize::Small)
 860                                            .color(Color::Muted),
 861                                    ),
 862                            )
 863                            .when(is_running, |this| {
 864                                this.child(
 865                                    Label::new(if tool_count == 1 {
 866                                        SharedString::from("1 tool")
 867                                    } else {
 868                                        SharedString::from(format!("{} tools", tool_count))
 869                                    })
 870                                    .color(Color::Muted)
 871                                    .size(LabelSize::Small),
 872                                )
 873                            }),
 874                    )
 875                    .child(
 876                        h_flex()
 877                            .gap_0p5()
 878                            .flex_none()
 879                            .child(context_server_configuration_menu)
 880                            .child(
 881                            Switch::new("context-server-switch", is_running.into())
 882                                .on_click({
 883                                    let context_server_manager = self.context_server_store.clone();
 884                                    let fs = self.fs.clone();
 885
 886                                    move |state, _window, cx| {
 887                                        let is_enabled = match state {
 888                                            ToggleState::Unselected
 889                                            | ToggleState::Indeterminate => {
 890                                                context_server_manager.update(cx, |this, cx| {
 891                                                    this.stop_server(&context_server_id, cx)
 892                                                        .log_err();
 893                                                });
 894                                                false
 895                                            }
 896                                            ToggleState::Selected => {
 897                                                context_server_manager.update(cx, |this, cx| {
 898                                                    if let Some(server) =
 899                                                        this.get_server(&context_server_id)
 900                                                    {
 901                                                        this.start_server(server, cx);
 902                                                    }
 903                                                });
 904                                                true
 905                                            }
 906                                        };
 907                                        update_settings_file(fs.clone(), cx, {
 908                                            let context_server_id = context_server_id.clone();
 909
 910                                            move |settings, _| {
 911                                                settings
 912                                                    .project
 913                                                    .context_servers
 914                                                    .entry(context_server_id.0)
 915                                                    .or_insert_with(|| {
 916                                                        settings::ContextServerSettingsContent::Extension {
 917                                                            enabled: is_enabled,
 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<SharedString> = (0..u8::MAX)
1342                    .map(|i| {
1343                        if i == 0 {
1344                            "your_agent".into()
1345                        } else {
1346                            format!("your_agent_{}", i).into()
1347                        }
1348                    })
1349                    .find(|name| {
1350                        !settings
1351                            .agent_servers
1352                            .as_ref()
1353                            .is_some_and(|agent_servers| agent_servers.custom.contains_key(name))
1354                    });
1355                if let Some(server_name) = server_name {
1356                    unique_server_name = Some(server_name.clone());
1357                    settings
1358                        .agent_servers
1359                        .get_or_insert_default()
1360                        .custom
1361                        .insert(
1362                            server_name,
1363                            settings::CustomAgentServerSettings::Custom {
1364                                path: "path_to_executable".into(),
1365                                args: vec![],
1366                                env: Some(HashMap::default()),
1367                                default_mode: None,
1368                                default_model: None,
1369                            },
1370                        );
1371                }
1372            });
1373
1374            if edits.is_empty() {
1375                return;
1376            }
1377
1378            let ranges = edits
1379                .iter()
1380                .map(|(range, _)| range.clone())
1381                .collect::<Vec<_>>();
1382
1383            item.edit(
1384                edits.into_iter().map(|(range, s)| {
1385                    (
1386                        MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
1387                        s,
1388                    )
1389                }),
1390                cx,
1391            );
1392            if let Some((unique_server_name, buffer)) =
1393                unique_server_name.zip(item.buffer().read(cx).as_singleton())
1394            {
1395                let snapshot = buffer.read(cx).snapshot();
1396                if let Some(range) =
1397                    find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
1398                {
1399                    item.change_selections(
1400                        SelectionEffects::scroll(Autoscroll::newest()),
1401                        window,
1402                        cx,
1403                        |selections| {
1404                            selections.select_ranges(vec![
1405                                MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
1406                            ]);
1407                        },
1408                    );
1409                }
1410            }
1411        })
1412}
1413
1414fn find_text_in_buffer(
1415    text: &str,
1416    start: usize,
1417    snapshot: &language::BufferSnapshot,
1418) -> Option<Range<usize>> {
1419    let chars = text.chars().collect::<Vec<char>>();
1420
1421    let mut offset = start;
1422    let mut char_offset = 0;
1423    for c in snapshot.chars_at(start) {
1424        if char_offset >= chars.len() {
1425            break;
1426        }
1427        offset += 1;
1428
1429        if c == chars[char_offset] {
1430            char_offset += 1;
1431        } else {
1432            char_offset = 0;
1433        }
1434    }
1435
1436    if char_offset == chars.len() {
1437        Some(offset.saturating_sub(chars.len())..offset)
1438    } else {
1439        None
1440    }
1441}
1442
1443// OpenAI-compatible providers are user-configured and can be removed,
1444// whereas built-in providers (like Anthropic, OpenAI, Google, etc.) can't.
1445//
1446// If in the future we have more "API-compatible-type" of providers,
1447// they should be included here as removable providers.
1448fn is_removable_provider(provider_id: &LanguageModelProviderId, cx: &App) -> bool {
1449    AllLanguageModelSettings::get_global(cx)
1450        .openai_compatible
1451        .contains_key(provider_id.0.as_ref())
1452}