Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:55:17.055Z 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

ai_setting_item.rs

415 lines · 14.4 KB · rust
1use crate::{IconDecoration, IconDecorationKind, Tooltip, prelude::*};
2use gpui::{Animation, AnimationExt, SharedString, pulsating_between};
3use std::time::Duration;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
6pub enum AiSettingItemStatus {
7    #[default]
8    Stopped,
9    Starting,
10    Running,
11    Error,
12    AuthRequired,
13    ClientSecretRequired,
14    Authenticating,
15}
16
17impl AiSettingItemStatus {
18    fn tooltip_text(&self) -> &'static str {
19        match self {
20            Self::Stopped => "Server is stopped.",
21            Self::Starting => "Server is starting.",
22            Self::Running => "Server is active.",
23            Self::Error => "Server has an error.",
24            Self::AuthRequired => "Authentication Required.",
25            Self::ClientSecretRequired => "Client Secret Required.",
26            Self::Authenticating => "Waiting for Authorization…",
27        }
28    }
29
30    fn indicator_color(&self) -> Option<Color> {
31        match self {
32            Self::Stopped => None,
33            Self::Starting | Self::Authenticating => Some(Color::Muted),
34            Self::Running => Some(Color::Success),
35            Self::Error => Some(Color::Error),
36            Self::AuthRequired | Self::ClientSecretRequired => Some(Color::Warning),
37        }
38    }
39
40    fn is_animated(&self) -> bool {
41        matches!(self, Self::Starting | Self::Authenticating)
42    }
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum AiSettingItemSource {
47    Extension,
48    Custom,
49    Registry,
50}
51
52impl AiSettingItemSource {
53    fn icon_name(&self) -> IconName {
54        match self {
55            Self::Extension => IconName::SrcExtension,
56            Self::Custom => IconName::SrcCustom,
57            Self::Registry => IconName::AcpRegistry,
58        }
59    }
60
61    fn tooltip_text(&self, label: &str) -> String {
62        match self {
63            Self::Extension => format!("{label} was installed from an extension."),
64            Self::Registry => format!("{label} was installed from the ACP registry."),
65            Self::Custom => format!("{label} was configured manually."),
66        }
67    }
68}
69
70/// A reusable setting item row for AI-related configuration lists.
71#[derive(IntoElement, RegisterComponent)]
72pub struct AiSettingItem {
73    id: ElementId,
74    status: AiSettingItemStatus,
75    source: AiSettingItemSource,
76    icon: Option<AnyElement>,
77    label: SharedString,
78    detail_label: Option<SharedString>,
79    actions: Vec<AnyElement>,
80    details: Option<AnyElement>,
81}
82
83impl AiSettingItem {
84    pub fn new(
85        id: impl Into<ElementId>,
86        label: impl Into<SharedString>,
87        status: AiSettingItemStatus,
88        source: AiSettingItemSource,
89    ) -> Self {
90        Self {
91            id: id.into(),
92            status,
93            source,
94            icon: None,
95            label: label.into(),
96            detail_label: None,
97            actions: Vec::new(),
98            details: None,
99        }
100    }
101
102    pub fn icon(mut self, element: impl IntoElement) -> Self {
103        self.icon = Some(element.into_any_element());
104        self
105    }
106
107    pub fn detail_label(mut self, detail: impl Into<SharedString>) -> Self {
108        self.detail_label = Some(detail.into());
109        self
110    }
111
112    pub fn action(mut self, element: impl IntoElement) -> Self {
113        self.actions.push(element.into_any_element());
114        self
115    }
116
117    pub fn details(mut self, element: impl IntoElement) -> Self {
118        self.details = Some(element.into_any_element());
119        self
120    }
121}
122
123impl RenderOnce for AiSettingItem {
124    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
125        let Self {
126            id,
127            status,
128            source,
129            icon,
130            label,
131            detail_label,
132            actions,
133            details,
134        } = self;
135
136        let source_id = format!("source-{}", id);
137        let icon_id = format!("icon-{}", id);
138        let status_tooltip = status.tooltip_text();
139        let source_tooltip = source.tooltip_text(&label);
140
141        let icon_element = icon.unwrap_or_else(|| {
142            let letter = label.chars().next().unwrap_or('?').to_ascii_uppercase();
143
144            h_flex()
145                .size_5()
146                .flex_none()
147                .justify_center()
148                .rounded_sm()
149                .border_1()
150                .border_color(cx.theme().colors().border_variant)
151                .bg(cx.theme().colors().element_active.opacity(0.2))
152                .child(
153                    Label::new(SharedString::from(letter.to_string()))
154                        .size(LabelSize::Small)
155                        .color(Color::Muted)
156                        .buffer_font(cx),
157                )
158                .into_any_element()
159        });
160
161        let icon_child = if status.is_animated() {
162            div()
163                .child(icon_element)
164                .with_animation(
165                    format!("icon-pulse-{}", id),
166                    Animation::new(Duration::from_secs(2))
167                        .repeat()
168                        .with_easing(pulsating_between(0.4, 0.8)),
169                    |element, delta| element.opacity(delta),
170                )
171                .into_any_element()
172        } else {
173            icon_element.into_any_element()
174        };
175
176        let icon_container = div()
177            .id(icon_id)
178            .relative()
179            .flex_none()
180            .tooltip(Tooltip::text(status_tooltip))
181            .child(icon_child)
182            .when_some(status.indicator_color(), |this, color| {
183                this.child(
184                    IconDecoration::new(
185                        IconDecorationKind::Dot,
186                        cx.theme().colors().panel_background,
187                        cx,
188                    )
189                    .size(px(12.))
190                    .color(color.color(cx))
191                    .position(gpui::Point {
192                        x: px(-3.),
193                        y: px(-3.),
194                    }),
195                )
196            });
197
198        v_flex()
199            .id(id)
200            .min_w_0()
201            .py_2()
202            .child(
203                h_flex()
204                    .min_w_0()
205                    .w_full()
206                    .gap_1p5()
207                    .justify_between()
208                    .child(
209                        h_flex()
210                            .flex_1()
211                            .min_w_0()
212                            .gap_1p5()
213                            .child(icon_container)
214                            .child(Label::new(label).flex_shrink_0().truncate())
215                            .child(
216                                div()
217                                    .id(source_id)
218                                    .min_w_0()
219                                    .flex_none()
220                                    .tooltip(Tooltip::text(source_tooltip))
221                                    .child(
222                                        Icon::new(source.icon_name())
223                                            .size(IconSize::Small)
224                                            .color(Color::Muted),
225                                    ),
226                            )
227                            .when_some(detail_label, |this, detail| {
228                                this.child(
229                                    Label::new(detail)
230                                        .color(Color::Muted)
231                                        .size(LabelSize::Small),
232                                )
233                            }),
234                    )
235                    .when(!actions.is_empty(), |this| {
236                        this.child(h_flex().gap_0p5().flex_none().children(actions))
237                    }),
238            )
239            .children(details)
240    }
241}
242
243impl Component for AiSettingItem {
244    fn scope() -> ComponentScope {
245        ComponentScope::Agent
246    }
247
248    fn description() -> &'static str {
249        "A reusable row used in AI-related configuration lists to display a \
250        server or provider's name, source, current status, and associated actions."
251    }
252
253    fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
254        let container = || {
255            v_flex()
256                .w_80()
257                .p_2()
258                .gap_2()
259                .border_1()
260                .border_color(cx.theme().colors().border_variant)
261                .bg(cx.theme().colors().panel_background)
262        };
263
264        let details_row = |icon_name: IconName, icon_color: Color, message: &str| {
265            h_flex()
266                .py_1()
267                .min_w_0()
268                .w_full()
269                .gap_2()
270                .justify_between()
271                .child(
272                    h_flex()
273                        .pr_4()
274                        .min_w_0()
275                        .w_full()
276                        .gap_2()
277                        .child(
278                            Icon::new(icon_name)
279                                .size(IconSize::XSmall)
280                                .color(icon_color),
281                        )
282                        .child(
283                            div().min_w_0().flex_1().child(
284                                Label::new(SharedString::from(message.to_string()))
285                                    .color(Color::Muted)
286                                    .size(LabelSize::Small),
287                            ),
288                        ),
289                )
290        };
291
292        let examples = vec![
293            single_example(
294                "MCP server with letter avatar (running)",
295                container()
296                    .child(
297                        AiSettingItem::new(
298                            "ext-mcp",
299                            "Postgres",
300                            AiSettingItemStatus::Running,
301                            AiSettingItemSource::Extension,
302                        )
303                        .detail_label("3 tools")
304                        .action(
305                            IconButton::new("menu", IconName::Settings)
306                                .icon_size(IconSize::Small)
307                                .icon_color(Color::Muted),
308                        )
309                        .action(
310                            IconButton::new("toggle", IconName::Check)
311                                .icon_size(IconSize::Small)
312                                .icon_color(Color::Muted),
313                        ),
314                    )
315                    .into_any_element(),
316            ),
317            single_example(
318                "MCP server (stopped)",
319                container()
320                    .child(AiSettingItem::new(
321                        "custom-mcp",
322                        "my-local-server",
323                        AiSettingItemStatus::Stopped,
324                        AiSettingItemSource::Custom,
325                    ))
326                    .into_any_element(),
327            ),
328            single_example(
329                "MCP server (starting, animated)",
330                container()
331                    .child(AiSettingItem::new(
332                        "starting-mcp",
333                        "Context7",
334                        AiSettingItemStatus::Starting,
335                        AiSettingItemSource::Extension,
336                    ))
337                    .into_any_element(),
338            ),
339            single_example(
340                "Agent with icon (running)",
341                container()
342                    .child(
343                        AiSettingItem::new(
344                            "ext-agent",
345                            "Claude Agent",
346                            AiSettingItemStatus::Running,
347                            AiSettingItemSource::Extension,
348                        )
349                        .icon(
350                            Icon::new(IconName::AiClaude)
351                                .size(IconSize::Small)
352                                .color(Color::Muted),
353                        )
354                        .action(
355                            IconButton::new("restart", IconName::RotateCw)
356                                .icon_size(IconSize::Small)
357                                .icon_color(Color::Muted),
358                        )
359                        .action(
360                            IconButton::new("delete", IconName::Trash)
361                                .icon_size(IconSize::Small)
362                                .icon_color(Color::Muted),
363                        ),
364                    )
365                    .into_any_element(),
366            ),
367            single_example(
368                "Registry agent (starting, animated)",
369                container()
370                    .child(
371                        AiSettingItem::new(
372                            "reg-agent",
373                            "Devin Agent",
374                            AiSettingItemStatus::Starting,
375                            AiSettingItemSource::Registry,
376                        )
377                        .icon(
378                            Icon::new(IconName::OmegaAssistant)
379                                .size(IconSize::Small)
380                                .color(Color::Muted),
381                        ),
382                    )
383                    .into_any_element(),
384            ),
385            single_example(
386                "Error with details",
387                container()
388                    .child(
389                        AiSettingItem::new(
390                            "error-mcp",
391                            "Amplitude",
392                            AiSettingItemStatus::Error,
393                            AiSettingItemSource::Extension,
394                        )
395                        .details(
396                            details_row(
397                                IconName::XCircle,
398                                Color::Error,
399                                "Failed to connect: connection refused",
400                            )
401                            .child(
402                                Button::new("logout", "Log Out")
403                                    .style(ButtonStyle::Outlined)
404                                    .label_size(LabelSize::Small),
405                            ),
406                        ),
407                    )
408                    .into_any_element(),
409            ),
410        ];
411
412        example_group(examples).vertical().into_any_element()
413    }
414}
415
Served at tenant.openagents/omega Member data and write actions are omitted.