Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:04:17.484Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

agent_registry_ui.rs

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