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    Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure,
  38    Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize,
  39    PopoverMenu, Switch, SwitchColor, Tooltip, 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(SharedString::from(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                                .color(SwitchColor::Accent)
 883                                .on_click({
 884                                    let context_server_manager = self.context_server_store.clone();
 885                                    let fs = self.fs.clone();
 886
 887                                    move |state, _window, cx| {
 888                                        let is_enabled = match state {
 889                                            ToggleState::Unselected
 890                                            | ToggleState::Indeterminate => {
 891                                                context_server_manager.update(cx, |this, cx| {
 892                                                    this.stop_server(&context_server_id, cx)
 893                                                        .log_err();
 894                                                });
 895                                                false
 896                                            }
 897                                            ToggleState::Selected => {
 898                                                context_server_manager.update(cx, |this, cx| {
 899                                                    if let Some(server) =
 900                                                        this.get_server(&context_server_id)
 901                                                    {
 902                                                        this.start_server(server, cx);
 903                                                    }
 904                                                });
 905                                                true
 906                                            }
 907                                        };
 908                                        update_settings_file(fs.clone(), cx, {
 909                                            let context_server_id = context_server_id.clone();
 910
 911                                            move |settings, _| {
 912                                                settings
 913                                                    .project
 914                                                    .context_servers
 915                                                    .entry(context_server_id.0)
 916                                                    .or_insert_with(|| {
 917                                                        settings::ContextServerSettingsContent::Extension {
 918                                                            enabled: is_enabled,
 919                                                            settings: serde_json::json!({}),
 920                                                        }
 921                                                    })
 922                                                    .set_enabled(is_enabled);
 923                                            }
 924                                        });
 925                                    }
 926                                }),
 927                        ),
 928                    ),
 929            )
 930            .map(|parent| {
 931                if let Some(error) = error {
 932                    return parent.child(
 933                        h_flex()
 934                            .gap_2()
 935                            .pr_4()
 936                            .items_start()
 937                            .child(
 938                                h_flex()
 939                                    .flex_none()
 940                                    .h(window.line_height() / 1.6_f32)
 941                                    .justify_center()
 942                                    .child(
 943                                        Icon::new(IconName::XCircle)
 944                                            .size(IconSize::XSmall)
 945                                            .color(Color::Error),
 946                                    ),
 947                            )
 948                            .child(
 949                                div().w_full().child(
 950                                    Label::new(error)
 951                                        .buffer_font(cx)
 952                                        .color(Color::Muted)
 953                                        .size(LabelSize::Small),
 954                                ),
 955                            ),
 956                    );
 957                }
 958                parent
 959            })
 960    }
 961
 962    fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
 963        let agent_server_store = self.agent_server_store.read(cx);
 964
 965        let user_defined_agents = agent_server_store
 966            .external_agents()
 967            .filter(|name| {
 968                name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
 969            })
 970            .cloned()
 971            .collect::<Vec<_>>();
 972
 973        let user_defined_agents: Vec<_> = user_defined_agents
 974            .into_iter()
 975            .map(|name| {
 976                let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
 977                    AgentIcon::Path(icon_path)
 978                } else {
 979                    AgentIcon::Name(IconName::Ai)
 980                };
 981                (name, icon)
 982            })
 983            .collect();
 984
 985        let add_agent_popover = PopoverMenu::new("add-agent-server-popover")
 986            .trigger(
 987                Button::new("add-agent", "Add Agent")
 988                    .style(ButtonStyle::Outlined)
 989                    .icon_position(IconPosition::Start)
 990                    .icon(IconName::Plus)
 991                    .icon_size(IconSize::Small)
 992                    .icon_color(Color::Muted)
 993                    .label_size(LabelSize::Small),
 994            )
 995            .menu({
 996                move |window, cx| {
 997                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
 998                        menu.entry("Install from Extensions", None, {
 999                            |window, cx| {
1000                                window.dispatch_action(
1001                                    zed_actions::Extensions {
1002                                        category_filter: Some(
1003                                            ExtensionCategoryFilter::AgentServers,
1004                                        ),
1005                                        id: None,
1006                                    }
1007                                    .boxed_clone(),
1008                                    cx,
1009                                )
1010                            }
1011                        })
1012                        .entry("Add Custom Agent", None, {
1013                            move |window, cx| {
1014                                if let Some(workspace) = window.root().flatten() {
1015                                    let workspace = workspace.downgrade();
1016                                    window
1017                                        .spawn(cx, async |cx| {
1018                                            open_new_agent_servers_entry_in_settings_editor(
1019                                                workspace, cx,
1020                                            )
1021                                            .await
1022                                        })
1023                                        .detach_and_log_err(cx);
1024                                }
1025                            }
1026                        })
1027                        .separator()
1028                        .header("Learn More")
1029                        .item(
1030                            ContextMenuEntry::new("Agent Servers Docs")
1031                                .icon(IconName::ArrowUpRight)
1032                                .icon_color(Color::Muted)
1033                                .icon_position(IconPosition::End)
1034                                .handler({
1035                                    move |window, cx| {
1036                                        window.dispatch_action(
1037                                            Box::new(OpenBrowser {
1038                                                url: zed_urls::agent_server_docs(cx),
1039                                            }),
1040                                            cx,
1041                                        );
1042                                    }
1043                                }),
1044                        )
1045                        .item(
1046                            ContextMenuEntry::new("ACP Docs")
1047                                .icon(IconName::ArrowUpRight)
1048                                .icon_color(Color::Muted)
1049                                .icon_position(IconPosition::End)
1050                                .handler({
1051                                    move |window, cx| {
1052                                        window.dispatch_action(
1053                                            Box::new(OpenBrowser {
1054                                                url: "https://agentclientprotocol.com/".into(),
1055                                            }),
1056                                            cx,
1057                                        );
1058                                    }
1059                                }),
1060                        )
1061                    }))
1062                }
1063            })
1064            .anchor(gpui::Corner::TopRight)
1065            .offset(gpui::Point {
1066                x: px(0.0),
1067                y: px(2.0),
1068            });
1069
1070        v_flex()
1071            .border_b_1()
1072            .border_color(cx.theme().colors().border)
1073            .child(
1074                v_flex()
1075                    .child(self.render_section_title(
1076                        "External Agents",
1077                        "All agents connected through the Agent Client Protocol.",
1078                        add_agent_popover.into_any_element(),
1079                    ))
1080                    .child(
1081                        v_flex()
1082                            .p_4()
1083                            .pt_0()
1084                            .gap_2()
1085                            .child(self.render_agent_server(
1086                                AgentIcon::Name(IconName::AiClaude),
1087                                "Claude Code",
1088                                false,
1089                                cx,
1090                            ))
1091                            .child(Divider::horizontal().color(DividerColor::BorderFaded))
1092                            .child(self.render_agent_server(
1093                                AgentIcon::Name(IconName::AiOpenAi),
1094                                "Codex CLI",
1095                                false,
1096                                cx,
1097                            ))
1098                            .child(Divider::horizontal().color(DividerColor::BorderFaded))
1099                            .child(self.render_agent_server(
1100                                AgentIcon::Name(IconName::AiGemini),
1101                                "Gemini CLI",
1102                                false,
1103                                cx,
1104                            ))
1105                            .map(|mut parent| {
1106                                for (name, icon) in user_defined_agents {
1107                                    parent = parent
1108                                        .child(
1109                                            Divider::horizontal().color(DividerColor::BorderFaded),
1110                                        )
1111                                        .child(self.render_agent_server(icon, name, true, cx));
1112                                }
1113                                parent
1114                            }),
1115                    ),
1116            )
1117    }
1118
1119    fn render_agent_server(
1120        &self,
1121        icon: AgentIcon,
1122        name: impl Into<SharedString>,
1123        external: bool,
1124        cx: &mut Context<Self>,
1125    ) -> impl IntoElement {
1126        let name = name.into();
1127        let icon = match icon {
1128            AgentIcon::Name(icon_name) => Icon::new(icon_name)
1129                .size(IconSize::Small)
1130                .color(Color::Muted),
1131            AgentIcon::Path(icon_path) => Icon::from_external_svg(icon_path)
1132                .size(IconSize::Small)
1133                .color(Color::Muted),
1134        };
1135
1136        let tooltip_id = SharedString::new(format!("agent-source-{}", name));
1137        let tooltip_message = format!("The {} agent was installed from an extension.", name);
1138
1139        let agent_server_name = ExternalAgentServerName(name.clone());
1140
1141        let uninstall_btn_id = SharedString::from(format!("uninstall-{}", name));
1142        let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash)
1143            .icon_color(Color::Muted)
1144            .icon_size(IconSize::Small)
1145            .tooltip(Tooltip::text("Uninstall Agent Extension"))
1146            .on_click(cx.listener(move |this, _, _window, cx| {
1147                let agent_name = agent_server_name.clone();
1148
1149                if let Some(ext_id) = this.agent_server_store.update(cx, |store, _cx| {
1150                    store.get_extension_id_for_agent(&agent_name)
1151                }) {
1152                    ExtensionStore::global(cx)
1153                        .update(cx, |store, cx| store.uninstall_extension(ext_id, cx))
1154                        .detach_and_log_err(cx);
1155                }
1156            }));
1157
1158        h_flex()
1159            .gap_1()
1160            .justify_between()
1161            .child(
1162                h_flex()
1163                    .gap_1p5()
1164                    .child(icon)
1165                    .child(Label::new(name))
1166                    .when(external, |this| {
1167                        this.child(
1168                            div()
1169                                .id(tooltip_id)
1170                                .flex_none()
1171                                .tooltip(Tooltip::text(tooltip_message))
1172                                .child(
1173                                    Icon::new(IconName::ZedSrcExtension)
1174                                        .size(IconSize::Small)
1175                                        .color(Color::Muted),
1176                                ),
1177                        )
1178                    })
1179                    .child(
1180                        Icon::new(IconName::Check)
1181                            .color(Color::Success)
1182                            .size(IconSize::Small),
1183                    ),
1184            )
1185            .when(external, |this| this.child(uninstall_button))
1186    }
1187}
1188
1189impl Render for AgentConfiguration {
1190    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1191        v_flex()
1192            .id("assistant-configuration")
1193            .key_context("AgentConfiguration")
1194            .track_focus(&self.focus_handle(cx))
1195            .relative()
1196            .size_full()
1197            .pb_8()
1198            .bg(cx.theme().colors().panel_background)
1199            .child(
1200                div()
1201                    .size_full()
1202                    .child(
1203                        v_flex()
1204                            .id("assistant-configuration-content")
1205                            .track_scroll(&self.scroll_handle)
1206                            .size_full()
1207                            .overflow_y_scroll()
1208                            .child(self.render_agent_servers_section(cx))
1209                            .child(self.render_context_servers_section(window, cx))
1210                            .child(self.render_provider_configuration_section(cx)),
1211                    )
1212                    .vertical_scrollbar_for(&self.scroll_handle, window, cx),
1213            )
1214    }
1215}
1216
1217fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool {
1218    manifest.context_servers.len() == 1
1219        && manifest.themes.is_empty()
1220        && manifest.icon_themes.is_empty()
1221        && manifest.languages.is_empty()
1222        && manifest.grammars.is_empty()
1223        && manifest.language_servers.is_empty()
1224        && manifest.slash_commands.is_empty()
1225        && manifest.snippets.is_none()
1226        && manifest.debug_locators.is_empty()
1227}
1228
1229pub(crate) fn resolve_extension_for_context_server(
1230    id: &ContextServerId,
1231    cx: &App,
1232) -> Option<(Arc<str>, Arc<ExtensionManifest>)> {
1233    ExtensionStore::global(cx)
1234        .read(cx)
1235        .installed_extensions()
1236        .iter()
1237        .find(|(_, entry)| entry.manifest.context_servers.contains_key(&id.0))
1238        .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
1239}
1240
1241// This notification appears when trying to delete
1242// an MCP server extension that not only provides
1243// the server, but other things, too, like language servers and more.
1244fn show_unable_to_uninstall_extension_with_context_server(
1245    workspace: &mut Workspace,
1246    id: ContextServerId,
1247    cx: &mut App,
1248) {
1249    let workspace_handle = workspace.weak_handle();
1250    let context_server_id = id.clone();
1251
1252    let status_toast = StatusToast::new(
1253        format!(
1254            "The {} extension provides more than just the MCP server. Proceed to uninstall anyway?",
1255            id.0
1256        ),
1257        cx,
1258        move |this, _cx| {
1259            let workspace_handle = workspace_handle.clone();
1260
1261            this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
1262                .dismiss_button(true)
1263                .action("Uninstall", move |_, _cx| {
1264                    if let Some((extension_id, _)) =
1265                        resolve_extension_for_context_server(&context_server_id, _cx)
1266                    {
1267                        ExtensionStore::global(_cx).update(_cx, |store, cx| {
1268                            store
1269                                .uninstall_extension(extension_id, cx)
1270                                .detach_and_log_err(cx);
1271                        });
1272
1273                        workspace_handle
1274                            .update(_cx, |workspace, cx| {
1275                                let fs = workspace.app_state().fs.clone();
1276                                cx.spawn({
1277                                    let context_server_id = context_server_id.clone();
1278                                    async move |_workspace_handle, cx| {
1279                                        cx.update(|cx| {
1280                                            update_settings_file(fs, cx, move |settings, _| {
1281                                                settings
1282                                                    .project
1283                                                    .context_servers
1284                                                    .remove(&context_server_id.0);
1285                                            });
1286                                        })?;
1287                                        anyhow::Ok(())
1288                                    }
1289                                })
1290                                .detach_and_log_err(cx);
1291                            })
1292                            .log_err();
1293                    }
1294                })
1295        },
1296    );
1297
1298    workspace.toggle_status_toast(status_toast, cx);
1299}
1300
1301async fn open_new_agent_servers_entry_in_settings_editor(
1302    workspace: WeakEntity<Workspace>,
1303    cx: &mut AsyncWindowContext,
1304) -> Result<()> {
1305    let settings_editor = workspace
1306        .update_in(cx, |_, window, cx| {
1307            create_and_open_local_file(paths::settings_file(), window, cx, || {
1308                settings::initial_user_settings_content().as_ref().into()
1309            })
1310        })?
1311        .await?
1312        .downcast::<Editor>()
1313        .unwrap();
1314
1315    settings_editor
1316        .downgrade()
1317        .update_in(cx, |item, window, cx| {
1318            let text = item.buffer().read(cx).snapshot(cx).text();
1319
1320            let settings = cx.global::<SettingsStore>();
1321
1322            let mut unique_server_name = None;
1323            let edits = settings.edits_for_update(&text, |settings| {
1324                let server_name: Option<SharedString> = (0..u8::MAX)
1325                    .map(|i| {
1326                        if i == 0 {
1327                            "your_agent".into()
1328                        } else {
1329                            format!("your_agent_{}", i).into()
1330                        }
1331                    })
1332                    .find(|name| {
1333                        !settings
1334                            .agent_servers
1335                            .as_ref()
1336                            .is_some_and(|agent_servers| agent_servers.custom.contains_key(name))
1337                    });
1338                if let Some(server_name) = server_name {
1339                    unique_server_name = Some(server_name.clone());
1340                    settings
1341                        .agent_servers
1342                        .get_or_insert_default()
1343                        .custom
1344                        .insert(
1345                            server_name,
1346                            settings::CustomAgentServerSettings::Custom {
1347                                path: "path_to_executable".into(),
1348                                args: vec![],
1349                                env: Some(HashMap::default()),
1350                                default_mode: None,
1351                                default_model: None,
1352                            },
1353                        );
1354                }
1355            });
1356
1357            if edits.is_empty() {
1358                return;
1359            }
1360
1361            let ranges = edits
1362                .iter()
1363                .map(|(range, _)| range.clone())
1364                .collect::<Vec<_>>();
1365
1366            item.edit(
1367                edits.into_iter().map(|(range, s)| {
1368                    (
1369                        MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
1370                        s,
1371                    )
1372                }),
1373                cx,
1374            );
1375            if let Some((unique_server_name, buffer)) =
1376                unique_server_name.zip(item.buffer().read(cx).as_singleton())
1377            {
1378                let snapshot = buffer.read(cx).snapshot();
1379                if let Some(range) =
1380                    find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
1381                {
1382                    item.change_selections(
1383                        SelectionEffects::scroll(Autoscroll::newest()),
1384                        window,
1385                        cx,
1386                        |selections| {
1387                            selections.select_ranges(vec![
1388                                MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
1389                            ]);
1390                        },
1391                    );
1392                }
1393            }
1394        })
1395}
1396
1397fn find_text_in_buffer(
1398    text: &str,
1399    start: usize,
1400    snapshot: &language::BufferSnapshot,
1401) -> Option<Range<usize>> {
1402    let chars = text.chars().collect::<Vec<char>>();
1403
1404    let mut offset = start;
1405    let mut char_offset = 0;
1406    for c in snapshot.chars_at(start) {
1407        if char_offset >= chars.len() {
1408            break;
1409        }
1410        offset += 1;
1411
1412        if c == chars[char_offset] {
1413            char_offset += 1;
1414        } else {
1415            char_offset = 0;
1416        }
1417    }
1418
1419    if char_offset == chars.len() {
1420        Some(offset.saturating_sub(chars.len())..offset)
1421    } else {
1422        None
1423    }
1424}
1425
1426// OpenAI-compatible providers are user-configured and can be removed,
1427// whereas built-in providers (like Anthropic, OpenAI, Google, etc.) can't.
1428//
1429// If in the future we have more "API-compatible-type" of providers,
1430// they should be included here as removable providers.
1431fn is_removable_provider(provider_id: &LanguageModelProviderId, cx: &App) -> bool {
1432    AllLanguageModelSettings::get_global(cx)
1433        .openai_compatible
1434        .contains_key(provider_id.0.as_ref())
1435}