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