agent_configuration.rs

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