Skip to repository content697 lines · 26.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:04:37.552Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
agent_registry_ui.rs
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(®istry_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