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