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 }
495 .boxed_clone(),
496 cx,
497 )
498 }),
499 ),
500 ),
501 )
502 }
503
504 fn render_context_server(
505 &self,
506 context_server_id: ContextServerId,
507 window: &mut Window,
508 cx: &mut Context<Self>,
509 ) -> impl use<> + IntoElement {
510 let tools_by_source = self.tools.read(cx).tools_by_source(cx);
511 let server_status = self
512 .context_server_store
513 .read(cx)
514 .status_for_server(&context_server_id)
515 .unwrap_or(ContextServerStatus::Stopped);
516 let server_configuration = self
517 .context_server_store
518 .read(cx)
519 .configuration_for_server(&context_server_id);
520
521 let is_running = matches!(server_status, ContextServerStatus::Running);
522 let item_id = SharedString::from(context_server_id.0.clone());
523 let is_from_extension = server_configuration
524 .as_ref()
525 .map(|config| {
526 matches!(
527 config.as_ref(),
528 ContextServerConfiguration::Extension { .. }
529 )
530 })
531 .unwrap_or(false);
532
533 let error = if let ContextServerStatus::Error(error) = server_status.clone() {
534 Some(error)
535 } else {
536 None
537 };
538
539 let are_tools_expanded = self
540 .expanded_context_server_tools
541 .get(&context_server_id)
542 .copied()
543 .unwrap_or_default();
544 let tools = tools_by_source
545 .get(&ToolSource::ContextServer {
546 id: context_server_id.0.clone().into(),
547 })
548 .map_or([].as_slice(), |tools| tools.as_slice());
549 let tool_count = tools.len();
550
551 let border_color = cx.theme().colors().border.opacity(0.6);
552
553 let (source_icon, source_tooltip) = if is_from_extension {
554 (
555 IconName::ZedMcpExtension,
556 "This MCP server was installed from an extension.",
557 )
558 } else {
559 (
560 IconName::ZedMcpCustom,
561 "This custom MCP server was installed directly.",
562 )
563 };
564
565 let (status_indicator, tooltip_text) = match server_status {
566 ContextServerStatus::Starting => (
567 Icon::new(IconName::LoadCircle)
568 .size(IconSize::XSmall)
569 .color(Color::Accent)
570 .with_animation(
571 SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
572 Animation::new(Duration::from_secs(3)).repeat(),
573 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
574 )
575 .into_any_element(),
576 "Server is starting.",
577 ),
578 ContextServerStatus::Running => (
579 Indicator::dot().color(Color::Success).into_any_element(),
580 "Server is active.",
581 ),
582 ContextServerStatus::Error(_) => (
583 Indicator::dot().color(Color::Error).into_any_element(),
584 "Server has an error.",
585 ),
586 ContextServerStatus::Stopped => (
587 Indicator::dot().color(Color::Muted).into_any_element(),
588 "Server is stopped.",
589 ),
590 };
591
592 let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu")
593 .trigger_with_tooltip(
594 IconButton::new("context-server-config-menu", IconName::Settings)
595 .icon_color(Color::Muted)
596 .icon_size(IconSize::Small),
597 Tooltip::text("Open MCP server options"),
598 )
599 .anchor(Corner::TopRight)
600 .menu({
601 let fs = self.fs.clone();
602 let context_server_id = context_server_id.clone();
603 let language_registry = self.language_registry.clone();
604 let context_server_store = self.context_server_store.clone();
605 let workspace = self.workspace.clone();
606 move |window, cx| {
607 Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
608 menu.entry("Configure Server", None, {
609 let context_server_id = context_server_id.clone();
610 let language_registry = language_registry.clone();
611 let workspace = workspace.clone();
612 move |window, cx| {
613 ConfigureContextServerModal::show_modal_for_existing_server(
614 context_server_id.clone(),
615 language_registry.clone(),
616 workspace.clone(),
617 window,
618 cx,
619 )
620 .detach_and_log_err(cx);
621 }
622 })
623 .separator()
624 .entry("Uninstall", None, {
625 let fs = fs.clone();
626 let context_server_id = context_server_id.clone();
627 let context_server_store = context_server_store.clone();
628 let workspace = workspace.clone();
629 move |_, cx| {
630 let is_provided_by_extension = context_server_store
631 .read(cx)
632 .configuration_for_server(&context_server_id)
633 .as_ref()
634 .map(|config| {
635 matches!(
636 config.as_ref(),
637 ContextServerConfiguration::Extension { .. }
638 )
639 })
640 .unwrap_or(false);
641
642 let uninstall_extension_task = match (
643 is_provided_by_extension,
644 resolve_extension_for_context_server(&context_server_id, cx),
645 ) {
646 (true, Some((id, manifest))) => {
647 if extension_only_provides_context_server(manifest.as_ref())
648 {
649 ExtensionStore::global(cx).update(cx, |store, cx| {
650 store.uninstall_extension(id, cx)
651 })
652 } else {
653 workspace.update(cx, |workspace, cx| {
654 show_unable_to_uninstall_extension_with_context_server(workspace, context_server_id.clone(), cx);
655 }).log_err();
656 Task::ready(Ok(()))
657 }
658 }
659 _ => Task::ready(Ok(())),
660 };
661
662 cx.spawn({
663 let fs = fs.clone();
664 let context_server_id = context_server_id.clone();
665 async move |cx| {
666 uninstall_extension_task.await?;
667 cx.update(|cx| {
668 update_settings_file::<ProjectSettings>(
669 fs.clone(),
670 cx,
671 {
672 let context_server_id =
673 context_server_id.clone();
674 move |settings, _| {
675 settings
676 .context_servers
677 .remove(&context_server_id.0);
678 }
679 },
680 )
681 })
682 }
683 })
684 .detach_and_log_err(cx);
685 }
686 })
687 }))
688 }
689 });
690
691 v_flex()
692 .id(item_id.clone())
693 .border_1()
694 .rounded_md()
695 .border_color(border_color)
696 .bg(cx.theme().colors().background.opacity(0.2))
697 .overflow_hidden()
698 .child(
699 h_flex()
700 .p_1()
701 .justify_between()
702 .when(
703 error.is_some() || are_tools_expanded && tool_count >= 1,
704 |element| element.border_b_1().border_color(border_color),
705 )
706 .child(
707 h_flex()
708 .child(
709 Disclosure::new(
710 "tool-list-disclosure",
711 are_tools_expanded || error.is_some(),
712 )
713 .disabled(tool_count == 0)
714 .on_click(cx.listener({
715 let context_server_id = context_server_id.clone();
716 move |this, _event, _window, _cx| {
717 let is_open = this
718 .expanded_context_server_tools
719 .entry(context_server_id.clone())
720 .or_insert(false);
721
722 *is_open = !*is_open;
723 }
724 })),
725 )
726 .child(
727 h_flex()
728 .id(SharedString::from(format!("tooltip-{}", item_id)))
729 .h_full()
730 .w_3()
731 .mx_1()
732 .justify_center()
733 .tooltip(Tooltip::text(tooltip_text))
734 .child(status_indicator),
735 )
736 .child(Label::new(item_id).ml_0p5())
737 .child(
738 div()
739 .id("extension-source")
740 .mt_0p5()
741 .mx_1()
742 .tooltip(Tooltip::text(source_tooltip))
743 .child(
744 Icon::new(source_icon)
745 .size(IconSize::Small)
746 .color(Color::Muted),
747 ),
748 )
749 .when(is_running, |this| {
750 this.child(
751 Label::new(if tool_count == 1 {
752 SharedString::from("1 tool")
753 } else {
754 SharedString::from(format!("{} tools", tool_count))
755 })
756 .color(Color::Muted)
757 .size(LabelSize::Small),
758 )
759 }),
760 )
761 .child(
762 h_flex()
763 .gap_1()
764 .child(context_server_configuration_menu)
765 .child(
766 Switch::new("context-server-switch", is_running.into())
767 .color(SwitchColor::Accent)
768 .on_click({
769 let context_server_manager =
770 self.context_server_store.clone();
771 let context_server_id = context_server_id.clone();
772 let fs = self.fs.clone();
773
774 move |state, _window, cx| {
775 let is_enabled = match state {
776 ToggleState::Unselected
777 | ToggleState::Indeterminate => {
778 context_server_manager.update(
779 cx,
780 |this, cx| {
781 this.stop_server(
782 &context_server_id,
783 cx,
784 )
785 .log_err();
786 },
787 );
788 false
789 }
790 ToggleState::Selected => {
791 context_server_manager.update(
792 cx,
793 |this, cx| {
794 if let Some(server) =
795 this.get_server(&context_server_id)
796 {
797 this.start_server(server, cx);
798 }
799 },
800 );
801 true
802 }
803 };
804 update_settings_file::<ProjectSettings>(
805 fs.clone(),
806 cx,
807 {
808 let context_server_id =
809 context_server_id.clone();
810
811 move |settings, _| {
812 settings
813 .context_servers
814 .entry(context_server_id.0)
815 .or_insert_with(|| {
816 ContextServerSettings::Extension {
817 enabled: is_enabled,
818 settings: serde_json::json!({}),
819 }
820 })
821 .set_enabled(is_enabled);
822 }
823 },
824 );
825 }
826 }),
827 ),
828 ),
829 )
830 .map(|parent| {
831 if let Some(error) = error {
832 return parent.child(
833 h_flex()
834 .p_2()
835 .gap_2()
836 .items_start()
837 .child(
838 h_flex()
839 .flex_none()
840 .h(window.line_height() / 1.6_f32)
841 .justify_center()
842 .child(
843 Icon::new(IconName::XCircle)
844 .size(IconSize::XSmall)
845 .color(Color::Error),
846 ),
847 )
848 .child(
849 div().w_full().child(
850 Label::new(error)
851 .buffer_font(cx)
852 .color(Color::Muted)
853 .size(LabelSize::Small),
854 ),
855 ),
856 );
857 }
858
859 if !are_tools_expanded || tools.is_empty() {
860 return parent;
861 }
862
863 parent.child(v_flex().py_1p5().px_1().gap_1().children(
864 tools.into_iter().enumerate().map(|(ix, tool)| {
865 h_flex()
866 .id(("tool-item", ix))
867 .px_1()
868 .gap_2()
869 .justify_between()
870 .hover(|style| style.bg(cx.theme().colors().element_hover))
871 .rounded_sm()
872 .child(
873 Label::new(tool.name())
874 .buffer_font(cx)
875 .size(LabelSize::Small),
876 )
877 .child(
878 Icon::new(IconName::Info)
879 .size(IconSize::Small)
880 .color(Color::Ignored),
881 )
882 .tooltip(Tooltip::text(tool.description()))
883 }),
884 ))
885 })
886 }
887}
888
889impl Render for AgentConfiguration {
890 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
891 v_flex()
892 .id("assistant-configuration")
893 .key_context("AgentConfiguration")
894 .track_focus(&self.focus_handle(cx))
895 .relative()
896 .size_full()
897 .pb_8()
898 .bg(cx.theme().colors().panel_background)
899 .child(
900 v_flex()
901 .id("assistant-configuration-content")
902 .track_scroll(&self.scroll_handle)
903 .size_full()
904 .overflow_y_scroll()
905 .child(self.render_general_settings_section(cx))
906 .child(self.render_context_servers_section(window, cx))
907 .child(self.render_provider_configuration_section(cx)),
908 )
909 .child(
910 div()
911 .id("assistant-configuration-scrollbar")
912 .occlude()
913 .absolute()
914 .right(px(3.))
915 .top_0()
916 .bottom_0()
917 .pb_6()
918 .w(px(12.))
919 .cursor_default()
920 .on_mouse_move(cx.listener(|_, _, _window, cx| {
921 cx.notify();
922 cx.stop_propagation()
923 }))
924 .on_hover(|_, _window, cx| {
925 cx.stop_propagation();
926 })
927 .on_any_mouse_down(|_, _window, cx| {
928 cx.stop_propagation();
929 })
930 .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
931 cx.notify();
932 }))
933 .children(Scrollbar::vertical(self.scrollbar_state.clone())),
934 )
935 }
936}
937
938fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool {
939 manifest.context_servers.len() == 1
940 && manifest.themes.is_empty()
941 && manifest.icon_themes.is_empty()
942 && manifest.languages.is_empty()
943 && manifest.grammars.is_empty()
944 && manifest.language_servers.is_empty()
945 && manifest.slash_commands.is_empty()
946 && manifest.indexed_docs_providers.is_empty()
947 && manifest.snippets.is_none()
948 && manifest.debug_locators.is_empty()
949}
950
951pub(crate) fn resolve_extension_for_context_server(
952 id: &ContextServerId,
953 cx: &App,
954) -> Option<(Arc<str>, Arc<ExtensionManifest>)> {
955 ExtensionStore::global(cx)
956 .read(cx)
957 .installed_extensions()
958 .iter()
959 .find(|(_, entry)| entry.manifest.context_servers.contains_key(&id.0))
960 .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
961}
962
963// This notification appears when trying to delete
964// an MCP server extension that not only provides
965// the server, but other things, too, like language servers and more.
966fn show_unable_to_uninstall_extension_with_context_server(
967 workspace: &mut Workspace,
968 id: ContextServerId,
969 cx: &mut App,
970) {
971 let workspace_handle = workspace.weak_handle();
972 let context_server_id = id.clone();
973
974 let status_toast = StatusToast::new(
975 format!(
976 "The {} extension provides more than just the MCP server. Proceed to uninstall anyway?",
977 id.0
978 ),
979 cx,
980 move |this, _cx| {
981 let workspace_handle = workspace_handle.clone();
982 let context_server_id = context_server_id.clone();
983
984 this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
985 .dismiss_button(true)
986 .action("Uninstall", move |_, _cx| {
987 if let Some((extension_id, _)) =
988 resolve_extension_for_context_server(&context_server_id, _cx)
989 {
990 ExtensionStore::global(_cx).update(_cx, |store, cx| {
991 store
992 .uninstall_extension(extension_id, cx)
993 .detach_and_log_err(cx);
994 });
995
996 workspace_handle
997 .update(_cx, |workspace, cx| {
998 let fs = workspace.app_state().fs.clone();
999 cx.spawn({
1000 let context_server_id = context_server_id.clone();
1001 async move |_workspace_handle, cx| {
1002 cx.update(|cx| {
1003 update_settings_file::<ProjectSettings>(
1004 fs,
1005 cx,
1006 move |settings, _| {
1007 settings
1008 .context_servers
1009 .remove(&context_server_id.0);
1010 },
1011 );
1012 })?;
1013 anyhow::Ok(())
1014 }
1015 })
1016 .detach_and_log_err(cx);
1017 })
1018 .log_err();
1019 }
1020 })
1021 },
1022 );
1023
1024 workspace.toggle_status_toast(status_toast, cx);
1025}