agent_registry_ui.rs

  1use std::collections::{BTreeMap, BTreeSet};
  2use std::ops::Range;
  3use std::sync::OnceLock;
  4
  5use client::zed_urls;
  6use collections::HashMap;
  7use editor::{Editor, EditorElement, EditorStyle};
  8use fs::Fs;
  9use gpui::{
 10    AnyElement, App, Context, Entity, EventEmitter, Focusable, KeyContext, ParentElement, Render,
 11    RenderOnce, SharedString, Styled, TextStyle, UniformListScrollHandle, Window, point,
 12    uniform_list,
 13};
 14use project::agent_server_store::{AllAgentServersSettings, CustomAgentServerSettings};
 15use project::{AgentRegistryStore, RegistryAgent};
 16use settings::{Settings, SettingsStore, update_settings_file};
 17use theme::ThemeSettings;
 18use ui::{
 19    Banner, ButtonStyle, ScrollableHandle, Severity, ToggleButtonGroup, ToggleButtonGroupSize,
 20    ToggleButtonGroupStyle, ToggleButtonSimple, Tooltip, WithScrollbar, prelude::*,
 21};
 22use workspace::{
 23    Workspace,
 24    item::{Item, ItemEvent},
 25};
 26
 27/// Registry IDs for built-in agents that Zed already provides first-class support for.
 28/// These are filtered out of the ACP Agent Registry UI to avoid showing duplicates.
 29const BUILT_IN_REGISTRY_IDS: [&str; 4] = ["claude-acp", "claude-code-acp", "codex-acp", "gemini"];
 30
 31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 32enum RegistryFilter {
 33    All,
 34    Installed,
 35    NotInstalled,
 36}
 37
 38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 39enum RegistryInstallStatus {
 40    NotInstalled,
 41    InstalledRegistry,
 42    InstalledCustom,
 43    InstalledExtension,
 44}
 45
 46#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
 47enum BuiltInAgent {
 48    Claude,
 49    Codex,
 50    Gemini,
 51}
 52
 53fn keywords_by_agent_feature() -> &'static BTreeMap<BuiltInAgent, Vec<&'static str>> {
 54    static KEYWORDS_BY_FEATURE: OnceLock<BTreeMap<BuiltInAgent, Vec<&'static str>>> =
 55        OnceLock::new();
 56    KEYWORDS_BY_FEATURE.get_or_init(|| {
 57        BTreeMap::from_iter([
 58            (BuiltInAgent::Claude, vec!["claude", "claude code"]),
 59            (BuiltInAgent::Codex, vec!["codex", "codex cli"]),
 60            (BuiltInAgent::Gemini, vec!["gemini", "gemini cli"]),
 61        ])
 62    })
 63}
 64
 65#[derive(IntoElement)]
 66struct AgentRegistryCard {
 67    children: Vec<AnyElement>,
 68}
 69
 70impl AgentRegistryCard {
 71    fn new() -> Self {
 72        Self {
 73            children: Vec::new(),
 74        }
 75    }
 76}
 77
 78impl ParentElement for AgentRegistryCard {
 79    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
 80        self.children.extend(elements)
 81    }
 82}
 83
 84impl RenderOnce for AgentRegistryCard {
 85    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 86        div().w_full().child(
 87            v_flex()
 88                .p_3()
 89                .mt_4()
 90                .w_full()
 91                .min_h(rems_from_px(86.))
 92                .gap_2()
 93                .bg(cx.theme().colors().elevated_surface_background.opacity(0.5))
 94                .border_1()
 95                .border_color(cx.theme().colors().border_variant)
 96                .rounded_md()
 97                .children(self.children),
 98        )
 99    }
100}
101
102pub struct AgentRegistryPage {
103    registry_store: Entity<AgentRegistryStore>,
104    list: UniformListScrollHandle,
105    registry_agents: Vec<RegistryAgent>,
106    filtered_registry_indices: Vec<usize>,
107    installed_statuses: HashMap<String, RegistryInstallStatus>,
108    query_editor: Entity<Editor>,
109    filter: RegistryFilter,
110    upsells: BTreeSet<BuiltInAgent>,
111    _subscriptions: Vec<gpui::Subscription>,
112}
113
114impl AgentRegistryPage {
115    pub fn new(
116        _workspace: &Workspace,
117        window: &mut Window,
118        cx: &mut Context<Workspace>,
119    ) -> Entity<Self> {
120        cx.new(|cx| {
121            let registry_store = AgentRegistryStore::global(cx);
122            let query_editor = cx.new(|cx| {
123                let mut input = Editor::single_line(window, cx);
124                input.set_placeholder_text("Search agents...", window, cx);
125                input
126            });
127            cx.subscribe(&query_editor, Self::on_query_change).detach();
128
129            let mut subscriptions = Vec::new();
130            subscriptions.push(cx.observe(&registry_store, |this, _, cx| {
131                this.reload_registry_agents(cx);
132            }));
133            subscriptions.push(cx.observe_global::<SettingsStore>(|this, cx| {
134                this.filter_registry_agents(cx);
135            }));
136
137            let mut this = Self {
138                registry_store,
139                list: UniformListScrollHandle::new(),
140                registry_agents: Vec::new(),
141                filtered_registry_indices: Vec::new(),
142                installed_statuses: HashMap::default(),
143                query_editor,
144                filter: RegistryFilter::All,
145                upsells: BTreeSet::new(),
146                _subscriptions: subscriptions,
147            };
148
149            this.reload_registry_agents(cx);
150            this.registry_store
151                .update(cx, |store, cx| store.refresh(cx));
152
153            this
154        })
155    }
156
157    fn reload_registry_agents(&mut self, cx: &mut Context<Self>) {
158        self.registry_agents = self.registry_store.read(cx).agents().to_vec();
159        self.registry_agents.sort_by(|left, right| {
160            left.name()
161                .as_ref()
162                .cmp(right.name().as_ref())
163                .then_with(|| left.id().as_ref().cmp(right.id().as_ref()))
164        });
165        self.filter_registry_agents(cx);
166    }
167
168    fn refresh_installed_statuses(&mut self, cx: &mut Context<Self>) {
169        let settings = cx
170            .global::<SettingsStore>()
171            .get::<AllAgentServersSettings>(None);
172        self.installed_statuses.clear();
173        for (id, settings) in &settings.custom {
174            let status = match settings {
175                CustomAgentServerSettings::Registry { .. } => {
176                    RegistryInstallStatus::InstalledRegistry
177                }
178                CustomAgentServerSettings::Custom { .. } => RegistryInstallStatus::InstalledCustom,
179                CustomAgentServerSettings::Extension { .. } => {
180                    RegistryInstallStatus::InstalledExtension
181                }
182            };
183            self.installed_statuses.insert(id.clone(), status);
184        }
185    }
186
187    fn install_status(&self, id: &str) -> RegistryInstallStatus {
188        self.installed_statuses
189            .get(id)
190            .copied()
191            .unwrap_or(RegistryInstallStatus::NotInstalled)
192    }
193
194    fn search_query(&self, cx: &mut App) -> Option<String> {
195        let search = self.query_editor.read(cx).text(cx);
196        if search.trim().is_empty() {
197            None
198        } else {
199            Some(search)
200        }
201    }
202
203    fn filter_registry_agents(&mut self, cx: &mut Context<Self>) {
204        self.refresh_installed_statuses(cx);
205        self.refresh_feature_upsells(cx);
206        let search = self.search_query(cx).map(|search| search.to_lowercase());
207        let filter = self.filter;
208        let installed_statuses = self.installed_statuses.clone();
209
210        let filtered_indices = self
211            .registry_agents
212            .iter()
213            .enumerate()
214            .filter(|(_, agent)| {
215                // Filter out built-in agents since they already appear in the main
216                // agent configuration UI and don't need to be installed from the registry.
217                if BUILT_IN_REGISTRY_IDS.contains(&agent.id().as_ref()) {
218                    return false;
219                }
220
221                let matches_search = search.as_ref().is_none_or(|query| {
222                    let query = query.as_str();
223                    agent.id().as_ref().to_lowercase().contains(query)
224                        || agent.name().as_ref().to_lowercase().contains(query)
225                        || agent.description().as_ref().to_lowercase().contains(query)
226                });
227
228                let install_status = installed_statuses
229                    .get(agent.id().as_ref())
230                    .copied()
231                    .unwrap_or(RegistryInstallStatus::NotInstalled);
232                let matches_filter = match filter {
233                    RegistryFilter::All => true,
234                    RegistryFilter::Installed => {
235                        install_status != RegistryInstallStatus::NotInstalled
236                    }
237                    RegistryFilter::NotInstalled => {
238                        install_status == RegistryInstallStatus::NotInstalled
239                    }
240                };
241
242                matches_search && matches_filter
243            })
244            .map(|(index, _)| index)
245            .collect();
246
247        self.filtered_registry_indices = filtered_indices;
248
249        cx.notify();
250    }
251
252    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
253        self.list.set_offset(point(px(0.), px(0.)));
254        cx.notify();
255    }
256
257    fn on_query_change(
258        &mut self,
259        _: Entity<Editor>,
260        event: &editor::EditorEvent,
261        cx: &mut Context<Self>,
262    ) {
263        if let editor::EditorEvent::Edited { .. } = event {
264            self.filter_registry_agents(cx);
265            self.scroll_to_top(cx);
266        }
267    }
268
269    fn refresh_feature_upsells(&mut self, cx: &mut Context<Self>) {
270        let Some(search) = self.search_query(cx) else {
271            self.upsells.clear();
272            return;
273        };
274
275        let search = search.to_lowercase();
276        let search_terms = search
277            .split_whitespace()
278            .map(|term| term.trim())
279            .collect::<Vec<_>>();
280
281        for (feature, keywords) in keywords_by_agent_feature() {
282            if keywords
283                .iter()
284                .any(|keyword| search_terms.contains(keyword))
285            {
286                self.upsells.insert(*feature);
287            } else {
288                self.upsells.remove(feature);
289            }
290        }
291    }
292
293    fn render_feature_upsell_banner(
294        &self,
295        label: SharedString,
296        docs_url: SharedString,
297    ) -> impl IntoElement {
298        let docs_url_button = Button::new("open_docs", "View Documentation")
299            .icon(IconName::ArrowUpRight)
300            .icon_size(IconSize::Small)
301            .icon_position(IconPosition::End)
302            .icon_color(Color::Muted)
303            .on_click({
304                move |_event, _window, cx| {
305                    telemetry::event!(
306                        "Documentation Viewed",
307                        source = "Agent Registry Feature Upsell",
308                        url = docs_url,
309                    );
310                    cx.open_url(&docs_url)
311                }
312            });
313
314        div().pt_4().px_4().child(
315            Banner::new()
316                .severity(Severity::Success)
317                .child(Label::new(label).mt_0p5())
318                .action_slot(docs_url_button),
319        )
320    }
321
322    fn render_feature_upsells(&self) -> impl IntoElement {
323        let mut container = v_flex();
324
325        for feature in &self.upsells {
326            let banner = match feature {
327                BuiltInAgent::Claude => self.render_feature_upsell_banner(
328                    "Claude Code support is built-in to Zed!".into(),
329                    "https://zed.dev/docs/ai/external-agents#claude-code".into(),
330                ),
331                BuiltInAgent::Codex => self.render_feature_upsell_banner(
332                    "Codex CLI support is built-in to Zed!".into(),
333                    "https://zed.dev/docs/ai/external-agents#codex-cli".into(),
334                ),
335                BuiltInAgent::Gemini => self.render_feature_upsell_banner(
336                    "Gemini CLI support is built-in to Zed!".into(),
337                    "https://zed.dev/docs/ai/external-agents#gemini-cli".into(),
338                ),
339            };
340            container = container.child(banner);
341        }
342
343        container
344    }
345
346    fn render_search(&self, cx: &mut Context<Self>) -> Div {
347        let mut key_context = KeyContext::new_with_defaults();
348        key_context.add("BufferSearchBar");
349
350        h_flex()
351            .key_context(key_context)
352            .h_8()
353            .min_w(rems_from_px(384.))
354            .flex_1()
355            .pl_1p5()
356            .pr_2()
357            .gap_2()
358            .border_1()
359            .border_color(cx.theme().colors().border)
360            .rounded_md()
361            .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
362            .child(self.render_text_input(&self.query_editor, cx))
363    }
364
365    fn render_text_input(
366        &self,
367        editor: &Entity<Editor>,
368        cx: &mut Context<Self>,
369    ) -> impl IntoElement {
370        let settings = ThemeSettings::get_global(cx);
371        let text_style = TextStyle {
372            color: if editor.read(cx).read_only(cx) {
373                cx.theme().colors().text_disabled
374            } else {
375                cx.theme().colors().text
376            },
377            font_family: settings.ui_font.family.clone(),
378            font_features: settings.ui_font.features.clone(),
379            font_fallbacks: settings.ui_font.fallbacks.clone(),
380            font_size: rems(0.875).into(),
381            font_weight: settings.ui_font.weight,
382            line_height: relative(1.3),
383            ..Default::default()
384        };
385
386        EditorElement::new(
387            editor,
388            EditorStyle {
389                background: cx.theme().colors().editor_background,
390                local_player: cx.theme().players().local(),
391                text: text_style,
392                ..Default::default()
393            },
394        )
395    }
396
397    fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
398        let has_search = self.search_query(cx).is_some();
399        let registry_store = self.registry_store.read(cx);
400
401        let message = if registry_store.is_fetching() {
402            "Loading registry..."
403        } else if registry_store.fetch_error().is_some() {
404            "Failed to load the agent registry. Please check your connection and try again."
405        } else {
406            match self.filter {
407                RegistryFilter::All => {
408                    if has_search {
409                        "No agents match your search."
410                    } else {
411                        "No agents available."
412                    }
413                }
414                RegistryFilter::Installed => {
415                    if has_search {
416                        "No installed agents match your search."
417                    } else {
418                        "No installed agents."
419                    }
420                }
421                RegistryFilter::NotInstalled => {
422                    if has_search {
423                        "No uninstalled agents match your search."
424                    } else {
425                        "No uninstalled agents."
426                    }
427                }
428            }
429        };
430
431        h_flex()
432            .py_4()
433            .gap_1p5()
434            .when(registry_store.fetch_error().is_some(), |this| {
435                this.child(
436                    Icon::new(IconName::Warning)
437                        .size(IconSize::Small)
438                        .color(Color::Warning),
439                )
440            })
441            .child(Label::new(message))
442    }
443
444    fn render_agents(
445        &mut self,
446        range: Range<usize>,
447        _: &mut Window,
448        cx: &mut Context<Self>,
449    ) -> Vec<AgentRegistryCard> {
450        range
451            .map(|index| {
452                let Some(agent_index) = self.filtered_registry_indices.get(index).copied() else {
453                    return self.render_missing_agent();
454                };
455                let Some(agent) = self.registry_agents.get(agent_index) else {
456                    return self.render_missing_agent();
457                };
458                self.render_registry_agent(agent, cx)
459            })
460            .collect()
461    }
462
463    fn render_missing_agent(&self) -> AgentRegistryCard {
464        AgentRegistryCard::new().child(
465            Label::new("Missing registry entry.")
466                .size(LabelSize::Small)
467                .color(Color::Muted),
468        )
469    }
470
471    fn render_registry_agent(
472        &self,
473        agent: &RegistryAgent,
474        cx: &mut Context<Self>,
475    ) -> AgentRegistryCard {
476        let install_status = self.install_status(agent.id().as_ref());
477        let supports_current_platform = agent.supports_current_platform();
478
479        let icon = match agent.icon_path() {
480            Some(icon_path) => Icon::from_external_svg(icon_path.clone()),
481            None => Icon::new(IconName::Sparkle),
482        }
483        .size(IconSize::Medium)
484        .color(Color::Muted);
485
486        let install_button =
487            self.install_button(agent, install_status, supports_current_platform, cx);
488
489        let repository_button = agent.repository().map(|repository| {
490            let repository_for_tooltip: SharedString = repository.to_string().into();
491            let repository_for_click = repository.to_string();
492
493            IconButton::new(
494                SharedString::from(format!("agent-repo-{}", agent.id())),
495                IconName::Github,
496            )
497            .icon_size(IconSize::Small)
498            .tooltip(move |_, cx| {
499                Tooltip::with_meta(
500                    "Visit Agent Repository",
501                    None,
502                    repository_for_tooltip.clone(),
503                    cx,
504                )
505            })
506            .on_click(move |_, _, cx| {
507                cx.open_url(&repository_for_click);
508            })
509        });
510
511        AgentRegistryCard::new()
512            .child(
513                h_flex()
514                    .justify_between()
515                    .child(
516                        h_flex()
517                            .gap_2()
518                            .child(icon)
519                            .child(Headline::new(agent.name().clone()).size(HeadlineSize::Small))
520                            .child(Label::new(format!("v{}", agent.version())).color(Color::Muted))
521                            .when(!supports_current_platform, |this| {
522                                this.child(
523                                    Label::new("Not supported on this platform")
524                                        .size(LabelSize::Small)
525                                        .color(Color::Warning),
526                                )
527                            }),
528                    )
529                    .child(install_button),
530            )
531            .child(
532                h_flex()
533                    .gap_2()
534                    .justify_between()
535                    .child(
536                        Label::new(agent.description().clone())
537                            .size(LabelSize::Small)
538                            .truncate(),
539                    )
540                    .child(
541                        h_flex()
542                            .gap_1()
543                            .child(
544                                Label::new(format!("ID: {}", agent.id()))
545                                    .size(LabelSize::Small)
546                                    .color(Color::Muted)
547                                    .truncate(),
548                            )
549                            .when_some(repository_button, |this, button| this.child(button)),
550                    ),
551            )
552    }
553
554    fn install_button(
555        &self,
556        agent: &RegistryAgent,
557        install_status: RegistryInstallStatus,
558        supports_current_platform: bool,
559        cx: &mut Context<Self>,
560    ) -> Button {
561        let button_id = SharedString::from(format!("install-agent-{}", agent.id()));
562
563        if !supports_current_platform {
564            return Button::new(button_id, "Unavailable")
565                .style(ButtonStyle::OutlinedGhost)
566                .disabled(true);
567        }
568
569        match install_status {
570            RegistryInstallStatus::NotInstalled => {
571                let fs = <dyn Fs>::global(cx);
572                let agent_id = agent.id().to_string();
573                Button::new(button_id, "Install")
574                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
575                    .icon(IconName::Download)
576                    .icon_size(IconSize::Small)
577                    .icon_color(Color::Muted)
578                    .icon_position(IconPosition::Start)
579                    .on_click(move |_, _, cx| {
580                        let agent_id = agent_id.clone();
581                        update_settings_file(fs.clone(), cx, move |settings, _| {
582                            let agent_servers = settings.agent_servers.get_or_insert_default();
583                            agent_servers.custom.entry(agent_id).or_insert_with(|| {
584                                settings::CustomAgentServerSettings::Registry {
585                                    default_mode: None,
586                                    default_model: None,
587                                    env: Default::default(),
588                                    favorite_models: Vec::new(),
589                                    default_config_options: HashMap::default(),
590                                    favorite_config_option_values: HashMap::default(),
591                                }
592                            });
593                        });
594                    })
595            }
596            RegistryInstallStatus::InstalledRegistry => {
597                let fs = <dyn Fs>::global(cx);
598                let agent_id = agent.id().to_string();
599                Button::new(button_id, "Remove")
600                    .style(ButtonStyle::OutlinedGhost)
601                    .on_click(move |_, _, cx| {
602                        let agent_id = agent_id.clone();
603                        update_settings_file(fs.clone(), cx, move |settings, _| {
604                            let Some(agent_servers) = settings.agent_servers.as_mut() else {
605                                return;
606                            };
607                            if let Some(entry) = agent_servers.custom.get(agent_id.as_str())
608                                && matches!(
609                                    entry,
610                                    settings::CustomAgentServerSettings::Registry { .. }
611                                )
612                            {
613                                agent_servers.custom.remove(agent_id.as_str());
614                            }
615                        });
616                    })
617            }
618            RegistryInstallStatus::InstalledCustom => Button::new(button_id, "Installed")
619                .style(ButtonStyle::OutlinedGhost)
620                .disabled(true),
621            RegistryInstallStatus::InstalledExtension => Button::new(button_id, "Installed")
622                .style(ButtonStyle::OutlinedGhost)
623                .disabled(true),
624        }
625    }
626}
627
628impl Render for AgentRegistryPage {
629    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
630        v_flex()
631            .size_full()
632            .bg(cx.theme().colors().editor_background)
633            .child(
634                v_flex()
635                    .p_4()
636                    .gap_4()
637                    .border_b_1()
638                    .border_color(cx.theme().colors().border_variant)
639                    .child(
640                        h_flex()
641                            .w_full()
642                            .gap_1p5()
643                            .justify_between()
644                            .child(Headline::new("ACP Registry").size(HeadlineSize::Large))
645                            .child(
646                                Button::new("learn-more", "Learn More")
647                                    .style(ButtonStyle::Outlined)
648                                    .size(ButtonSize::Medium)
649                                    .icon(IconName::ArrowUpRight)
650                                    .icon_color(Color::Muted)
651                                    .icon_size(IconSize::Small)
652                                    .on_click(move |_, _, cx| {
653                                        cx.open_url(&zed_urls::acp_registry_blog(cx))
654                                    }),
655                            ),
656                    )
657                    .child(
658                        h_flex()
659                            .w_full()
660                            .flex_wrap()
661                            .gap_2()
662                            .child(self.render_search(cx))
663                            .child(
664                                div().child(
665                                    ToggleButtonGroup::single_row(
666                                        "registry-filter-buttons",
667                                        [
668                                            ToggleButtonSimple::new(
669                                                "All",
670                                                cx.listener(|this, _event, _, cx| {
671                                                    this.filter = RegistryFilter::All;
672                                                    this.filter_registry_agents(cx);
673                                                    this.scroll_to_top(cx);
674                                                }),
675                                            ),
676                                            ToggleButtonSimple::new(
677                                                "Installed",
678                                                cx.listener(|this, _event, _, cx| {
679                                                    this.filter = RegistryFilter::Installed;
680                                                    this.filter_registry_agents(cx);
681                                                    this.scroll_to_top(cx);
682                                                }),
683                                            ),
684                                            ToggleButtonSimple::new(
685                                                "Not Installed",
686                                                cx.listener(|this, _event, _, cx| {
687                                                    this.filter = RegistryFilter::NotInstalled;
688                                                    this.filter_registry_agents(cx);
689                                                    this.scroll_to_top(cx);
690                                                }),
691                                            ),
692                                        ],
693                                    )
694                                    .style(ToggleButtonGroupStyle::Outlined)
695                                    .size(ToggleButtonGroupSize::Custom(rems_from_px(30.)))
696                                    .label_size(LabelSize::Default)
697                                    .auto_width()
698                                    .selected_index(match self.filter {
699                                        RegistryFilter::All => 0,
700                                        RegistryFilter::Installed => 1,
701                                        RegistryFilter::NotInstalled => 2,
702                                    })
703                                    .into_any_element(),
704                                ),
705                            ),
706                    ),
707            )
708            .child(self.render_feature_upsells())
709            .child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| {
710                let count = self.filtered_registry_indices.len();
711                let has_upsells = !self.upsells.is_empty();
712                if count == 0 && !has_upsells {
713                    this.child(self.render_empty_state(cx)).into_any_element()
714                } else if count == 0 {
715                    this.into_any_element()
716                } else {
717                    let scroll_handle = &self.list;
718                    this.child(
719                        uniform_list("registry-entries", count, cx.processor(Self::render_agents))
720                            .flex_grow()
721                            .pb_4()
722                            .track_scroll(scroll_handle),
723                    )
724                    .vertical_scrollbar_for(scroll_handle, window, cx)
725                    .into_any_element()
726                }
727            }))
728    }
729}
730
731impl EventEmitter<ItemEvent> for AgentRegistryPage {}
732
733impl Focusable for AgentRegistryPage {
734    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
735        self.query_editor.read(cx).focus_handle(cx)
736    }
737}
738
739impl Item for AgentRegistryPage {
740    type Event = ItemEvent;
741
742    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
743        "ACP Registry".into()
744    }
745
746    fn telemetry_event_text(&self) -> Option<&'static str> {
747        Some("ACP Registry Page Opened")
748    }
749
750    fn show_toolbar(&self) -> bool {
751        false
752    }
753
754    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
755        f(*event)
756    }
757}