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