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