agent_configuration.rs

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