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