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