1mod add_llm_provider_modal;
2mod configure_context_server_modal;
3mod configure_context_server_tools_modal;
4mod manage_profiles_modal;
5mod tool_picker;
6
7use std::{ops::Range, sync::Arc};
8
9use agent::ContextServerRegistry;
10use anyhow::Result;
11use cloud_llm_client::{Plan, PlanV1, PlanV2};
12use collections::HashMap;
13use context_server::ContextServerId;
14use editor::{Editor, SelectionEffects, scroll::Autoscroll};
15use extension::ExtensionManifest;
16use extension_host::ExtensionStore;
17use fs::Fs;
18use gpui::{
19 Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
20 ScrollHandle, Subscription, Task, WeakEntity,
21};
22use language::LanguageRegistry;
23use language_model::{
24 LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
25};
26use language_models::AllLanguageModelSettings;
27use notifications::status_toast::{StatusToast, ToastIcon};
28use project::{
29 agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
30 context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
31};
32use rope::Rope;
33use settings::{Settings, SettingsStore, update_settings_file};
34use ui::{
35 Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor,
36 ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, PopoverMenu, Switch,
37 SwitchColor, Tooltip, WithScrollbar, prelude::*,
38};
39use util::ResultExt as _;
40use workspace::{Workspace, create_and_open_local_file};
41use zed_actions::ExtensionCategoryFilter;
42
43pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
44pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
45pub(crate) use manage_profiles_modal::ManageProfilesModal;
46
47use crate::{
48 AddContextServer,
49 agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
50};
51
52pub struct AgentConfiguration {
53 fs: Arc<dyn Fs>,
54 language_registry: Arc<LanguageRegistry>,
55 agent_server_store: Entity<AgentServerStore>,
56 workspace: WeakEntity<Workspace>,
57 focus_handle: FocusHandle,
58 configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
59 context_server_store: Entity<ContextServerStore>,
60 expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>,
61 context_server_registry: Entity<ContextServerRegistry>,
62 _registry_subscription: Subscription,
63 scroll_handle: ScrollHandle,
64 _check_for_gemini: Task<()>,
65}
66
67impl AgentConfiguration {
68 pub fn new(
69 fs: Arc<dyn Fs>,
70 agent_server_store: Entity<AgentServerStore>,
71 context_server_store: Entity<ContextServerStore>,
72 context_server_registry: Entity<ContextServerRegistry>,
73 language_registry: Arc<LanguageRegistry>,
74 workspace: WeakEntity<Workspace>,
75 window: &mut Window,
76 cx: &mut Context<Self>,
77 ) -> Self {
78 let focus_handle = cx.focus_handle();
79
80 let registry_subscription = cx.subscribe_in(
81 &LanguageModelRegistry::global(cx),
82 window,
83 |this, _, event: &language_model::Event, window, cx| match event {
84 language_model::Event::AddedProvider(provider_id) => {
85 let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
86 if let Some(provider) = provider {
87 this.add_provider_configuration_view(&provider, window, cx);
88 }
89 }
90 language_model::Event::RemovedProvider(provider_id) => {
91 this.remove_provider_configuration_view(provider_id);
92 }
93 _ => {}
94 },
95 );
96
97 cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
98 .detach();
99
100 let mut this = Self {
101 fs,
102 language_registry,
103 workspace,
104 focus_handle,
105 configuration_views_by_provider: HashMap::default(),
106 agent_server_store,
107 context_server_store,
108 expanded_provider_configurations: HashMap::default(),
109 context_server_registry,
110 _registry_subscription: registry_subscription,
111 scroll_handle: ScrollHandle::new(),
112 _check_for_gemini: Task::ready(()),
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(
137 language_model::ConfigurationViewTargetAgent::ZedAgent,
138 window,
139 cx,
140 );
141 self.configuration_views_by_provider
142 .insert(provider.id(), configuration_view);
143 }
144}
145
146impl Focusable for AgentConfiguration {
147 fn focus_handle(&self, _: &App) -> FocusHandle {
148 self.focus_handle.clone()
149 }
150}
151
152pub enum AssistantConfigurationEvent {
153 NewThread(Arc<dyn LanguageModelProvider>),
154}
155
156impl EventEmitter<AssistantConfigurationEvent> for AgentConfiguration {}
157
158impl AgentConfiguration {
159 fn render_provider_configuration_block(
160 &mut self,
161 provider: &Arc<dyn LanguageModelProvider>,
162 cx: &mut Context<Self>,
163 ) -> impl IntoElement + use<> {
164 let provider_id = provider.id().0;
165 let provider_name = provider.name().0;
166 let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}"));
167
168 let configuration_view = self
169 .configuration_views_by_provider
170 .get(&provider.id())
171 .cloned();
172
173 let is_expanded = self
174 .expanded_provider_configurations
175 .get(&provider.id())
176 .copied()
177 .unwrap_or(false);
178
179 let is_zed_provider = provider.id() == ZED_CLOUD_PROVIDER_ID;
180 let current_plan = if is_zed_provider {
181 self.workspace
182 .upgrade()
183 .and_then(|workspace| workspace.read(cx).user_store().read(cx).plan())
184 } else {
185 None
186 };
187
188 let is_signed_in = self
189 .workspace
190 .read_with(cx, |workspace, _| {
191 !workspace.client().status().borrow().is_signed_out()
192 })
193 .unwrap_or(false);
194
195 v_flex()
196 .w_full()
197 .when(is_expanded, |this| this.mb_2())
198 .child(
199 div()
200 .px_2()
201 .child(Divider::horizontal().color(DividerColor::BorderFaded)),
202 )
203 .child(
204 h_flex()
205 .map(|this| {
206 if is_expanded {
207 this.mt_2().mb_1()
208 } else {
209 this.my_2()
210 }
211 })
212 .w_full()
213 .justify_between()
214 .child(
215 h_flex()
216 .id(provider_id_string.clone())
217 .px_2()
218 .py_0p5()
219 .w_full()
220 .justify_between()
221 .rounded_sm()
222 .hover(|hover| hover.bg(cx.theme().colors().element_hover))
223 .child(
224 h_flex()
225 .w_full()
226 .gap_1p5()
227 .child(
228 Icon::new(provider.icon())
229 .size(IconSize::Small)
230 .color(Color::Muted),
231 )
232 .child(
233 h_flex()
234 .w_full()
235 .gap_1()
236 .child(Label::new(provider_name.clone()))
237 .map(|this| {
238 if is_zed_provider && is_signed_in {
239 this.child(
240 self.render_zed_plan_info(current_plan, cx),
241 )
242 } else {
243 this.when(
244 provider.is_authenticated(cx)
245 && !is_expanded,
246 |parent| {
247 parent.child(
248 Icon::new(IconName::Check)
249 .color(Color::Success),
250 )
251 },
252 )
253 }
254 }),
255 ),
256 )
257 .child(
258 Disclosure::new(provider_id_string, is_expanded)
259 .opened_icon(IconName::ChevronUp)
260 .closed_icon(IconName::ChevronDown),
261 )
262 .on_click(cx.listener({
263 let provider_id = provider.id();
264 move |this, _event, _window, _cx| {
265 let is_expanded = this
266 .expanded_provider_configurations
267 .entry(provider_id.clone())
268 .or_insert(false);
269
270 *is_expanded = !*is_expanded;
271 }
272 })),
273 ),
274 )
275 .child(
276 v_flex()
277 .w_full()
278 .px_2()
279 .gap_1()
280 .when(is_expanded, |parent| match configuration_view {
281 Some(configuration_view) => parent.child(configuration_view),
282 None => parent.child(Label::new(format!(
283 "No configuration view for {provider_name}",
284 ))),
285 })
286 .when(is_expanded && provider.is_authenticated(cx), |parent| {
287 parent.child(
288 Button::new(
289 SharedString::from(format!("new-thread-{provider_id}")),
290 "Start New Thread",
291 )
292 .full_width()
293 .style(ButtonStyle::Filled)
294 .layer(ElevationIndex::ModalSurface)
295 .icon_position(IconPosition::Start)
296 .icon(IconName::Thread)
297 .icon_size(IconSize::Small)
298 .icon_color(Color::Muted)
299 .label_size(LabelSize::Small)
300 .on_click(cx.listener({
301 let provider = provider.clone();
302 move |_this, _event, _window, cx| {
303 cx.emit(AssistantConfigurationEvent::NewThread(
304 provider.clone(),
305 ))
306 }
307 })),
308 )
309 })
310 .when(
311 is_expanded && is_removable_provider(&provider.id(), cx),
312 |this| {
313 this.child(
314 Button::new(
315 SharedString::from(format!("delete-provider-{provider_id}")),
316 "Remove Provider",
317 )
318 .full_width()
319 .style(ButtonStyle::Outlined)
320 .icon_position(IconPosition::Start)
321 .icon(IconName::Trash)
322 .icon_size(IconSize::Small)
323 .icon_color(Color::Muted)
324 .label_size(LabelSize::Small)
325 .on_click(cx.listener({
326 let provider = provider.clone();
327 move |this, _event, window, cx| {
328 this.delete_provider(provider.clone(), window, cx);
329 }
330 })),
331 )
332 },
333 ),
334 )
335 }
336
337 fn delete_provider(
338 &mut self,
339 provider: Arc<dyn LanguageModelProvider>,
340 window: &mut Window,
341 cx: &mut Context<Self>,
342 ) {
343 let fs = self.fs.clone();
344 let provider_id = provider.id();
345
346 cx.spawn_in(window, async move |_, cx| {
347 cx.update(|_window, cx| {
348 update_settings_file(fs.clone(), cx, {
349 let provider_id = provider_id.clone();
350 move |settings, _| {
351 if let Some(ref mut openai_compatible) = settings
352 .language_models
353 .as_mut()
354 .and_then(|lm| lm.openai_compatible.as_mut())
355 {
356 let key_to_remove: Arc<str> = Arc::from(provider_id.0.as_ref());
357 openai_compatible.remove(&key_to_remove);
358 }
359 }
360 });
361 })
362 .log_err();
363
364 cx.update(|_window, cx| {
365 LanguageModelRegistry::global(cx).update(cx, {
366 let provider_id = provider_id.clone();
367 move |registry, cx| {
368 registry.unregister_provider(provider_id, cx);
369 }
370 })
371 })
372 .log_err();
373
374 anyhow::Ok(())
375 })
376 .detach_and_log_err(cx);
377 }
378
379 fn render_provider_configuration_section(
380 &mut self,
381 cx: &mut Context<Self>,
382 ) -> impl IntoElement {
383 let providers = LanguageModelRegistry::read_global(cx).providers();
384
385 v_flex()
386 .w_full()
387 .child(
388 h_flex()
389 .p(DynamicSpacing::Base16.rems(cx))
390 .pr(DynamicSpacing::Base20.rems(cx))
391 .pb_0()
392 .mb_2p5()
393 .items_start()
394 .justify_between()
395 .child(
396 v_flex()
397 .w_full()
398 .gap_0p5()
399 .child(
400 h_flex()
401 .pr_1()
402 .w_full()
403 .gap_2()
404 .justify_between()
405 .child(Headline::new("LLM Providers"))
406 .child(
407 PopoverMenu::new("add-provider-popover")
408 .trigger(
409 Button::new("add-provider", "Add Provider")
410 .style(ButtonStyle::Filled)
411 .layer(ElevationIndex::ModalSurface)
412 .icon_position(IconPosition::Start)
413 .icon(IconName::Plus)
414 .icon_size(IconSize::Small)
415 .icon_color(Color::Muted)
416 .label_size(LabelSize::Small),
417 )
418 .anchor(gpui::Corner::TopRight)
419 .menu({
420 let workspace = self.workspace.clone();
421 move |window, cx| {
422 Some(ContextMenu::build(
423 window,
424 cx,
425 |menu, _window, _cx| {
426 menu.header("Compatible APIs").entry(
427 "OpenAI",
428 None,
429 {
430 let workspace =
431 workspace.clone();
432 move |window, cx| {
433 workspace
434 .update(cx, |workspace, cx| {
435 AddLlmProviderModal::toggle(
436 LlmCompatibleProvider::OpenAi,
437 workspace,
438 window,
439 cx,
440 );
441 })
442 .log_err();
443 }
444 },
445 )
446 },
447 ))
448 }
449 }),
450 ),
451 )
452 .child(
453 Label::new("Add at least one provider to use AI-powered features with Zed's native agent.")
454 .color(Color::Muted),
455 ),
456 ),
457 )
458 .child(
459 div()
460 .w_full()
461 .pl(DynamicSpacing::Base08.rems(cx))
462 .pr(DynamicSpacing::Base20.rems(cx))
463 .children(
464 providers.into_iter().map(|provider| {
465 self.render_provider_configuration_block(&provider, cx)
466 }),
467 ),
468 )
469 }
470
471 fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement {
472 if let Some(plan) = plan {
473 let free_chip_bg = cx
474 .theme()
475 .colors()
476 .editor_background
477 .opacity(0.5)
478 .blend(cx.theme().colors().text_accent.opacity(0.05));
479
480 let pro_chip_bg = cx
481 .theme()
482 .colors()
483 .editor_background
484 .opacity(0.5)
485 .blend(cx.theme().colors().text_accent.opacity(0.2));
486
487 let (plan_name, label_color, bg_color) = match plan {
488 Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree) => {
489 ("Free", Color::Default, free_chip_bg)
490 }
491 Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial) => {
492 ("Pro Trial", Color::Accent, pro_chip_bg)
493 }
494 Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro) => {
495 ("Pro", Color::Accent, pro_chip_bg)
496 }
497 };
498
499 Chip::new(plan_name.to_string())
500 .bg_color(bg_color)
501 .label_color(label_color)
502 .into_any_element()
503 } else {
504 div().into_any_element()
505 }
506 }
507
508 fn render_context_servers_section(
509 &mut self,
510 window: &mut Window,
511 cx: &mut Context<Self>,
512 ) -> impl IntoElement {
513 let mut context_server_ids = self
514 .context_server_store
515 .read(cx)
516 .server_ids(cx)
517 .into_iter()
518 .collect::<Vec<_>>();
519
520 // Sort context servers: ones without mcp-server- prefix first, then prefixed ones
521 context_server_ids.sort_by(|a, b| {
522 const MCP_PREFIX: &str = "mcp-server-";
523 match (a.0.strip_prefix(MCP_PREFIX), b.0.strip_prefix(MCP_PREFIX)) {
524 // If one has mcp-server- prefix and other doesn't, non-mcp comes first
525 (Some(_), None) => std::cmp::Ordering::Greater,
526 (None, Some(_)) => std::cmp::Ordering::Less,
527 // If both have same prefix status, sort by appropriate key
528 (Some(a), Some(b)) => a.cmp(b),
529 (None, None) => a.0.cmp(&b.0),
530 }
531 });
532
533 let add_server_popover = PopoverMenu::new("add-server-popover")
534 .trigger(
535 Button::new("add-server", "Add Server")
536 .style(ButtonStyle::Filled)
537 .layer(ElevationIndex::ModalSurface)
538 .icon_position(IconPosition::Start)
539 .icon(IconName::Plus)
540 .icon_size(IconSize::Small)
541 .icon_color(Color::Muted)
542 .label_size(LabelSize::Small),
543 )
544 .anchor(gpui::Corner::TopRight)
545 .menu({
546 move |window, cx| {
547 Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
548 menu.entry("Add Custom Server", None, {
549 |window, cx| window.dispatch_action(AddContextServer.boxed_clone(), cx)
550 })
551 .entry("Install from Extensions", None, {
552 |window, cx| {
553 window.dispatch_action(
554 zed_actions::Extensions {
555 category_filter: Some(
556 ExtensionCategoryFilter::ContextServers,
557 ),
558 id: None,
559 }
560 .boxed_clone(),
561 cx,
562 )
563 }
564 })
565 }))
566 }
567 });
568
569 v_flex()
570 .p(DynamicSpacing::Base16.rems(cx))
571 .pr(DynamicSpacing::Base20.rems(cx))
572 .gap_2()
573 .border_b_1()
574 .border_color(cx.theme().colors().border)
575 .child(
576 h_flex()
577 .w_full()
578 .items_start()
579 .justify_between()
580 .gap_1()
581 .child(
582 v_flex()
583 .gap_0p5()
584 .child(Headline::new("Model Context Protocol (MCP) Servers"))
585 .child(
586 Label::new(
587 "All MCP servers connected directly or via a Zed extension.",
588 )
589 .color(Color::Muted),
590 ),
591 )
592 .child(add_server_popover),
593 )
594 .child(v_flex().w_full().gap_1().map(|mut parent| {
595 if context_server_ids.is_empty() {
596 parent.child(
597 h_flex()
598 .p_4()
599 .justify_center()
600 .border_1()
601 .border_dashed()
602 .border_color(cx.theme().colors().border.opacity(0.6))
603 .rounded_sm()
604 .child(
605 Label::new("No MCP servers added yet.")
606 .color(Color::Muted)
607 .size(LabelSize::Small),
608 ),
609 )
610 } else {
611 for (index, context_server_id) in context_server_ids.into_iter().enumerate() {
612 if index > 0 {
613 parent = parent.child(
614 Divider::horizontal()
615 .color(DividerColor::BorderFaded)
616 .into_any_element(),
617 );
618 }
619 parent =
620 parent.child(self.render_context_server(context_server_id, window, cx));
621 }
622 parent
623 }
624 }))
625 }
626
627 fn render_context_server(
628 &self,
629 context_server_id: ContextServerId,
630 window: &mut Window,
631 cx: &mut Context<Self>,
632 ) -> impl use<> + IntoElement {
633 let server_status = self
634 .context_server_store
635 .read(cx)
636 .status_for_server(&context_server_id)
637 .unwrap_or(ContextServerStatus::Stopped);
638 let server_configuration = self
639 .context_server_store
640 .read(cx)
641 .configuration_for_server(&context_server_id);
642
643 let is_running = matches!(server_status, ContextServerStatus::Running);
644 let item_id = SharedString::from(context_server_id.0.clone());
645 let is_from_extension = server_configuration
646 .as_ref()
647 .map(|config| {
648 matches!(
649 config.as_ref(),
650 ContextServerConfiguration::Extension { .. }
651 )
652 })
653 .unwrap_or(false);
654
655 let error = if let ContextServerStatus::Error(error) = server_status.clone() {
656 Some(error)
657 } else {
658 None
659 };
660
661 let tool_count = self
662 .context_server_registry
663 .read(cx)
664 .tools_for_server(&context_server_id)
665 .count();
666
667 let (source_icon, source_tooltip) = if is_from_extension {
668 (
669 IconName::ZedMcpExtension,
670 "This MCP server was installed from an extension.",
671 )
672 } else {
673 (
674 IconName::ZedMcpCustom,
675 "This custom MCP server was installed directly.",
676 )
677 };
678
679 let (status_indicator, tooltip_text) = match server_status {
680 ContextServerStatus::Starting => (
681 Icon::new(IconName::LoadCircle)
682 .size(IconSize::XSmall)
683 .color(Color::Accent)
684 .with_keyed_rotate_animation(
685 SharedString::from(format!("{}-starting", context_server_id.0)),
686 3,
687 )
688 .into_any_element(),
689 "Server is starting.",
690 ),
691 ContextServerStatus::Running => (
692 Indicator::dot().color(Color::Success).into_any_element(),
693 "Server is active.",
694 ),
695 ContextServerStatus::Error(_) => (
696 Indicator::dot().color(Color::Error).into_any_element(),
697 "Server has an error.",
698 ),
699 ContextServerStatus::Stopped => (
700 Indicator::dot().color(Color::Muted).into_any_element(),
701 "Server is stopped.",
702 ),
703 };
704
705 let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu")
706 .trigger_with_tooltip(
707 IconButton::new("context-server-config-menu", IconName::Settings)
708 .icon_color(Color::Muted)
709 .icon_size(IconSize::Small),
710 Tooltip::text("Configure MCP Server"),
711 )
712 .anchor(Corner::TopRight)
713 .menu({
714 let fs = self.fs.clone();
715 let context_server_id = context_server_id.clone();
716 let language_registry = self.language_registry.clone();
717 let context_server_store = self.context_server_store.clone();
718 let workspace = self.workspace.clone();
719 let context_server_registry = self.context_server_registry.clone();
720
721 move |window, cx| {
722 Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
723 menu.entry("Configure Server", None, {
724 let context_server_id = context_server_id.clone();
725 let language_registry = language_registry.clone();
726 let workspace = workspace.clone();
727 move |window, cx| {
728 ConfigureContextServerModal::show_modal_for_existing_server(
729 context_server_id.clone(),
730 language_registry.clone(),
731 workspace.clone(),
732 window,
733 cx,
734 )
735 .detach_and_log_err(cx);
736 }
737 }).when(tool_count > 0, |this| this.entry("View Tools", None, {
738 let context_server_id = context_server_id.clone();
739 let context_server_registry = context_server_registry.clone();
740 let workspace = workspace.clone();
741 move |window, cx| {
742 let context_server_id = context_server_id.clone();
743 workspace.update(cx, |workspace, cx| {
744 ConfigureContextServerToolsModal::toggle(
745 context_server_id,
746 context_server_registry.clone(),
747 workspace,
748 window,
749 cx,
750 );
751 })
752 .ok();
753 }
754 }))
755 .separator()
756 .entry("Uninstall", None, {
757 let fs = fs.clone();
758 let context_server_id = context_server_id.clone();
759 let context_server_store = context_server_store.clone();
760 let workspace = workspace.clone();
761 move |_, cx| {
762 let is_provided_by_extension = context_server_store
763 .read(cx)
764 .configuration_for_server(&context_server_id)
765 .as_ref()
766 .map(|config| {
767 matches!(
768 config.as_ref(),
769 ContextServerConfiguration::Extension { .. }
770 )
771 })
772 .unwrap_or(false);
773
774 let uninstall_extension_task = match (
775 is_provided_by_extension,
776 resolve_extension_for_context_server(&context_server_id, cx),
777 ) {
778 (true, Some((id, manifest))) => {
779 if extension_only_provides_context_server(manifest.as_ref())
780 {
781 ExtensionStore::global(cx).update(cx, |store, cx| {
782 store.uninstall_extension(id, cx)
783 })
784 } else {
785 workspace.update(cx, |workspace, cx| {
786 show_unable_to_uninstall_extension_with_context_server(workspace, context_server_id.clone(), cx);
787 }).log_err();
788 Task::ready(Ok(()))
789 }
790 }
791 _ => Task::ready(Ok(())),
792 };
793
794 cx.spawn({
795 let fs = fs.clone();
796 let context_server_id = context_server_id.clone();
797 async move |cx| {
798 uninstall_extension_task.await?;
799 cx.update(|cx| {
800 update_settings_file(
801 fs.clone(),
802 cx,
803 {
804 let context_server_id =
805 context_server_id.clone();
806 move |settings, _| {
807 settings.project
808 .context_servers
809 .remove(&context_server_id.0);
810 }
811 },
812 )
813 })
814 }
815 })
816 .detach_and_log_err(cx);
817 }
818 })
819 }))
820 }
821 });
822
823 v_flex()
824 .id(item_id.clone())
825 .child(
826 h_flex()
827 .justify_between()
828 .child(
829 h_flex()
830 .flex_1()
831 .min_w_0()
832 .child(
833 h_flex()
834 .id(SharedString::from(format!("tooltip-{}", item_id)))
835 .h_full()
836 .w_3()
837 .mr_2()
838 .justify_center()
839 .tooltip(Tooltip::text(tooltip_text))
840 .child(status_indicator),
841 )
842 .child(Label::new(item_id).truncate())
843 .child(
844 div()
845 .id("extension-source")
846 .mt_0p5()
847 .mx_1()
848 .flex_none()
849 .tooltip(Tooltip::text(source_tooltip))
850 .child(
851 Icon::new(source_icon)
852 .size(IconSize::Small)
853 .color(Color::Muted),
854 ),
855 )
856 .when(is_running, |this| {
857 this.child(
858 Label::new(if tool_count == 1 {
859 SharedString::from("1 tool")
860 } else {
861 SharedString::from(format!("{} tools", tool_count))
862 })
863 .color(Color::Muted)
864 .size(LabelSize::Small),
865 )
866 }),
867 )
868 .child(
869 h_flex()
870 .gap_0p5()
871 .flex_none()
872 .child(context_server_configuration_menu)
873 .child(
874 Switch::new("context-server-switch", is_running.into())
875 .color(SwitchColor::Accent)
876 .on_click({
877 let context_server_manager = self.context_server_store.clone();
878 let fs = self.fs.clone();
879
880 move |state, _window, cx| {
881 let is_enabled = match state {
882 ToggleState::Unselected
883 | ToggleState::Indeterminate => {
884 context_server_manager.update(cx, |this, cx| {
885 this.stop_server(&context_server_id, cx)
886 .log_err();
887 });
888 false
889 }
890 ToggleState::Selected => {
891 context_server_manager.update(cx, |this, cx| {
892 if let Some(server) =
893 this.get_server(&context_server_id)
894 {
895 this.start_server(server, cx);
896 }
897 });
898 true
899 }
900 };
901 update_settings_file(fs.clone(), cx, {
902 let context_server_id = context_server_id.clone();
903
904 move |settings, _| {
905 settings
906 .project
907 .context_servers
908 .entry(context_server_id.0)
909 .or_insert_with(|| {
910 settings::ContextServerSettingsContent::Extension {
911 enabled: is_enabled,
912 settings: serde_json::json!({}),
913 }
914 })
915 .set_enabled(is_enabled);
916 }
917 });
918 }
919 }),
920 ),
921 ),
922 )
923 .map(|parent| {
924 if let Some(error) = error {
925 return parent.child(
926 h_flex()
927 .gap_2()
928 .pr_4()
929 .items_start()
930 .child(
931 h_flex()
932 .flex_none()
933 .h(window.line_height() / 1.6_f32)
934 .justify_center()
935 .child(
936 Icon::new(IconName::XCircle)
937 .size(IconSize::XSmall)
938 .color(Color::Error),
939 ),
940 )
941 .child(
942 div().w_full().child(
943 Label::new(error)
944 .buffer_font(cx)
945 .color(Color::Muted)
946 .size(LabelSize::Small),
947 ),
948 ),
949 );
950 }
951 parent
952 })
953 }
954
955 fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
956 let user_defined_agents = self
957 .agent_server_store
958 .read(cx)
959 .external_agents()
960 .filter(|name| {
961 name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
962 })
963 .cloned()
964 .collect::<Vec<_>>();
965
966 let user_defined_agents = user_defined_agents
967 .into_iter()
968 .map(|name| {
969 self.render_agent_server(IconName::Ai, name)
970 .into_any_element()
971 })
972 .collect::<Vec<_>>();
973
974 v_flex()
975 .border_b_1()
976 .border_color(cx.theme().colors().border)
977 .child(
978 v_flex()
979 .p(DynamicSpacing::Base16.rems(cx))
980 .pr(DynamicSpacing::Base20.rems(cx))
981 .gap_2()
982 .child(
983 v_flex()
984 .gap_0p5()
985 .child(
986 h_flex()
987 .pr_1()
988 .w_full()
989 .gap_2()
990 .justify_between()
991 .child(Headline::new("External Agents"))
992 .child(
993 Button::new("add-agent", "Add Agent")
994 .style(ButtonStyle::Filled)
995 .layer(ElevationIndex::ModalSurface)
996 .icon_position(IconPosition::Start)
997 .icon(IconName::Plus)
998 .icon_size(IconSize::Small)
999 .icon_color(Color::Muted)
1000 .label_size(LabelSize::Small)
1001 .on_click(
1002 move |_, window, cx| {
1003 if let Some(workspace) = window.root().flatten() {
1004 let workspace = workspace.downgrade();
1005 window
1006 .spawn(cx, async |cx| {
1007 open_new_agent_servers_entry_in_settings_editor(
1008 workspace,
1009 cx,
1010 ).await
1011 })
1012 .detach_and_log_err(cx);
1013 }
1014 }
1015 ),
1016 )
1017 )
1018 .child(
1019 Label::new(
1020 "All agents connected through the Agent Client Protocol.",
1021 )
1022 .color(Color::Muted),
1023 ),
1024 )
1025 .child(self.render_agent_server(
1026 IconName::AiClaude,
1027 "Claude Code",
1028 ))
1029 .child(Divider::horizontal().color(DividerColor::BorderFaded))
1030 .child(self.render_agent_server(
1031 IconName::AiOpenAi,
1032 "Codex",
1033 ))
1034 .child(Divider::horizontal().color(DividerColor::BorderFaded))
1035 .child(self.render_agent_server(
1036 IconName::AiGemini,
1037 "Gemini CLI",
1038 ))
1039 .map(|mut parent| {
1040 for agent in user_defined_agents {
1041 parent = parent.child(Divider::horizontal().color(DividerColor::BorderFaded))
1042 .child(agent);
1043 }
1044 parent
1045 })
1046 )
1047 }
1048
1049 fn render_agent_server(
1050 &self,
1051 icon: IconName,
1052 name: impl Into<SharedString>,
1053 ) -> impl IntoElement {
1054 h_flex().gap_1p5().justify_between().child(
1055 h_flex()
1056 .gap_1p5()
1057 .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
1058 .child(Label::new(name.into()))
1059 .child(
1060 Icon::new(IconName::Check)
1061 .size(IconSize::Small)
1062 .color(Color::Success),
1063 ),
1064 )
1065 }
1066}
1067
1068impl Render for AgentConfiguration {
1069 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1070 v_flex()
1071 .id("assistant-configuration")
1072 .key_context("AgentConfiguration")
1073 .track_focus(&self.focus_handle(cx))
1074 .relative()
1075 .size_full()
1076 .pb_8()
1077 .bg(cx.theme().colors().panel_background)
1078 .child(
1079 div()
1080 .size_full()
1081 .child(
1082 v_flex()
1083 .id("assistant-configuration-content")
1084 .track_scroll(&self.scroll_handle)
1085 .size_full()
1086 .overflow_y_scroll()
1087 .child(self.render_agent_servers_section(cx))
1088 .child(self.render_context_servers_section(window, cx))
1089 .child(self.render_provider_configuration_section(cx)),
1090 )
1091 .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
1092 )
1093 }
1094}
1095
1096fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool {
1097 manifest.context_servers.len() == 1
1098 && manifest.themes.is_empty()
1099 && manifest.icon_themes.is_empty()
1100 && manifest.languages.is_empty()
1101 && manifest.grammars.is_empty()
1102 && manifest.language_servers.is_empty()
1103 && manifest.slash_commands.is_empty()
1104 && manifest.snippets.is_none()
1105 && manifest.debug_locators.is_empty()
1106}
1107
1108pub(crate) fn resolve_extension_for_context_server(
1109 id: &ContextServerId,
1110 cx: &App,
1111) -> Option<(Arc<str>, Arc<ExtensionManifest>)> {
1112 ExtensionStore::global(cx)
1113 .read(cx)
1114 .installed_extensions()
1115 .iter()
1116 .find(|(_, entry)| entry.manifest.context_servers.contains_key(&id.0))
1117 .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
1118}
1119
1120// This notification appears when trying to delete
1121// an MCP server extension that not only provides
1122// the server, but other things, too, like language servers and more.
1123fn show_unable_to_uninstall_extension_with_context_server(
1124 workspace: &mut Workspace,
1125 id: ContextServerId,
1126 cx: &mut App,
1127) {
1128 let workspace_handle = workspace.weak_handle();
1129 let context_server_id = id.clone();
1130
1131 let status_toast = StatusToast::new(
1132 format!(
1133 "The {} extension provides more than just the MCP server. Proceed to uninstall anyway?",
1134 id.0
1135 ),
1136 cx,
1137 move |this, _cx| {
1138 let workspace_handle = workspace_handle.clone();
1139
1140 this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
1141 .dismiss_button(true)
1142 .action("Uninstall", move |_, _cx| {
1143 if let Some((extension_id, _)) =
1144 resolve_extension_for_context_server(&context_server_id, _cx)
1145 {
1146 ExtensionStore::global(_cx).update(_cx, |store, cx| {
1147 store
1148 .uninstall_extension(extension_id, cx)
1149 .detach_and_log_err(cx);
1150 });
1151
1152 workspace_handle
1153 .update(_cx, |workspace, cx| {
1154 let fs = workspace.app_state().fs.clone();
1155 cx.spawn({
1156 let context_server_id = context_server_id.clone();
1157 async move |_workspace_handle, cx| {
1158 cx.update(|cx| {
1159 update_settings_file(fs, cx, move |settings, _| {
1160 settings
1161 .project
1162 .context_servers
1163 .remove(&context_server_id.0);
1164 });
1165 })?;
1166 anyhow::Ok(())
1167 }
1168 })
1169 .detach_and_log_err(cx);
1170 })
1171 .log_err();
1172 }
1173 })
1174 },
1175 );
1176
1177 workspace.toggle_status_toast(status_toast, cx);
1178}
1179
1180async fn open_new_agent_servers_entry_in_settings_editor(
1181 workspace: WeakEntity<Workspace>,
1182 cx: &mut AsyncWindowContext,
1183) -> Result<()> {
1184 let settings_editor = workspace
1185 .update_in(cx, |_, window, cx| {
1186 create_and_open_local_file(paths::settings_file(), window, cx, |cx| {
1187 Rope::from_str(
1188 &settings::initial_user_settings_content(),
1189 cx.background_executor(),
1190 )
1191 })
1192 })?
1193 .await?
1194 .downcast::<Editor>()
1195 .unwrap();
1196
1197 settings_editor
1198 .downgrade()
1199 .update_in(cx, |item, window, cx| {
1200 let text = item.buffer().read(cx).snapshot(cx).text();
1201
1202 let settings = cx.global::<SettingsStore>();
1203
1204 let mut unique_server_name = None;
1205 let edits = settings.edits_for_update(&text, |settings| {
1206 let server_name: Option<SharedString> = (0..u8::MAX)
1207 .map(|i| {
1208 if i == 0 {
1209 "your_agent".into()
1210 } else {
1211 format!("your_agent_{}", i).into()
1212 }
1213 })
1214 .find(|name| {
1215 !settings
1216 .agent_servers
1217 .as_ref()
1218 .is_some_and(|agent_servers| agent_servers.custom.contains_key(name))
1219 });
1220 if let Some(server_name) = server_name {
1221 unique_server_name = Some(server_name.clone());
1222 settings
1223 .agent_servers
1224 .get_or_insert_default()
1225 .custom
1226 .insert(
1227 server_name,
1228 settings::CustomAgentServerSettings {
1229 path: "path_to_executable".into(),
1230 args: vec![],
1231 env: Some(HashMap::default()),
1232 default_mode: None,
1233 },
1234 );
1235 }
1236 });
1237
1238 if edits.is_empty() {
1239 return;
1240 }
1241
1242 let ranges = edits
1243 .iter()
1244 .map(|(range, _)| range.clone())
1245 .collect::<Vec<_>>();
1246
1247 item.edit(edits, cx);
1248 if let Some((unique_server_name, buffer)) =
1249 unique_server_name.zip(item.buffer().read(cx).as_singleton())
1250 {
1251 let snapshot = buffer.read(cx).snapshot();
1252 if let Some(range) =
1253 find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
1254 {
1255 item.change_selections(
1256 SelectionEffects::scroll(Autoscroll::newest()),
1257 window,
1258 cx,
1259 |selections| {
1260 selections.select_ranges(vec![range]);
1261 },
1262 );
1263 }
1264 }
1265 })
1266}
1267
1268fn find_text_in_buffer(
1269 text: &str,
1270 start: usize,
1271 snapshot: &language::BufferSnapshot,
1272) -> Option<Range<usize>> {
1273 let chars = text.chars().collect::<Vec<char>>();
1274
1275 let mut offset = start;
1276 let mut char_offset = 0;
1277 for c in snapshot.chars_at(start) {
1278 if char_offset >= chars.len() {
1279 break;
1280 }
1281 offset += 1;
1282
1283 if c == chars[char_offset] {
1284 char_offset += 1;
1285 } else {
1286 char_offset = 0;
1287 }
1288 }
1289
1290 if char_offset == chars.len() {
1291 Some(offset.saturating_sub(chars.len())..offset)
1292 } else {
1293 None
1294 }
1295}
1296
1297// OpenAI-compatible providers are user-configured and can be removed,
1298// whereas built-in providers (like Anthropic, OpenAI, Google, etc.) can't.
1299//
1300// If in the future we have more "API-compatible-type" of providers,
1301// they should be included here as removable providers.
1302fn is_removable_provider(provider_id: &LanguageModelProviderId, cx: &App) -> bool {
1303 AllLanguageModelSettings::get_global(cx)
1304 .openai_compatible
1305 .contains_key(provider_id.0.as_ref())
1306}