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}