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