Skip to repository content415 lines · 14.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:55:17.055Z 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
ai_setting_item.rs
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