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