agent_configuration.rs

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