Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:43:39.651Z 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

callout.rs

365 lines · 13.8 KB · rust
1use gpui::AnyElement;
2
3use crate::prelude::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum CalloutBorderPosition {
7    Top,
8    Bottom,
9}
10
11/// A callout component for displaying important information that requires user attention.
12///
13/// # Usage Example
14///
15/// ```
16/// use ui::prelude::*;
17/// use ui::{Button, Callout, IconName, Label, Severity};
18///
19/// let callout = Callout::new()
20///     .severity(Severity::Warning)
21///     .icon(IconName::Warning)
22///     .title("Be aware of your subscription!")
23///     .description("Your subscription is about to expire. Renew now!")
24///     .actions_slot(Button::new("renew", "Renew Now"));
25/// ```
26///
27#[derive(IntoElement, RegisterComponent)]
28pub struct Callout {
29    severity: Severity,
30    icon: Option<IconName>,
31    title: Option<SharedString>,
32    description: Option<SharedString>,
33    description_slot: Option<AnyElement>,
34    actions_slot: Option<AnyElement>,
35    dismiss_action: Option<AnyElement>,
36    line_height: Option<Pixels>,
37    border_position: CalloutBorderPosition,
38}
39
40impl Callout {
41    /// Creates a new `Callout` component with default styling.
42    pub fn new() -> Self {
43        Self {
44            severity: Severity::Info,
45            icon: None,
46            title: None,
47            description: None,
48            description_slot: None,
49            actions_slot: None,
50            dismiss_action: None,
51            line_height: None,
52            border_position: CalloutBorderPosition::Top,
53        }
54    }
55
56    /// Sets the severity of the callout.
57    pub fn severity(mut self, severity: Severity) -> Self {
58        self.severity = severity;
59        self
60    }
61
62    /// Sets the icon to display in the callout.
63    pub fn icon(mut self, icon: IconName) -> Self {
64        self.icon = Some(icon);
65        self
66    }
67
68    /// Sets the title of the callout.
69    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
70        self.title = Some(title.into());
71        self
72    }
73
74    /// Sets the description of the callout.
75    /// The description can be single or multi-line text.
76    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
77        self.description = Some(description.into());
78        self
79    }
80
81    /// Allows for any element—like markdown elements—to fill the description slot of the callout.
82    /// This method wins over `description` if both happen to be set.
83    pub fn description_slot(mut self, description: impl IntoElement) -> Self {
84        self.description_slot = Some(description.into_any_element());
85        self
86    }
87
88    /// Sets the primary call-to-action button.
89    pub fn actions_slot(mut self, action: impl IntoElement) -> Self {
90        self.actions_slot = Some(action.into_any_element());
91        self
92    }
93
94    /// Sets an optional dismiss button, which is usually an icon button with a close icon.
95    /// This button is always rendered as the last one to the far right.
96    pub fn dismiss_action(mut self, action: impl IntoElement) -> Self {
97        self.dismiss_action = Some(action.into_any_element());
98        self
99    }
100
101    /// Sets a custom line height for the callout content.
102    pub fn line_height(mut self, line_height: Pixels) -> Self {
103        self.line_height = Some(line_height);
104        self
105    }
106
107    /// Sets the border position in the callout.
108    pub fn border_position(mut self, border_position: CalloutBorderPosition) -> Self {
109        self.border_position = border_position;
110        self
111    }
112}
113
114impl RenderOnce for Callout {
115    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
116        let line_height = self.line_height.unwrap_or(window.line_height());
117
118        let has_actions = self.actions_slot.is_some() || self.dismiss_action.is_some();
119
120        let (icon, icon_color, bg_color) = match self.severity {
121            Severity::Info => (
122                IconName::Info,
123                Color::Muted,
124                cx.theme().status().info_background.opacity(0.1),
125            ),
126            Severity::Success => (
127                IconName::Check,
128                Color::Success,
129                cx.theme().status().success.opacity(0.1),
130            ),
131            Severity::Warning => (
132                IconName::Warning,
133                Color::Warning,
134                cx.theme().status().warning_background.opacity(0.2),
135            ),
136            Severity::Error => (
137                IconName::XCircle,
138                Color::Error,
139                cx.theme().status().error.opacity(0.08),
140            ),
141        };
142
143        h_flex()
144            .min_w_0()
145            .w_full()
146            .p_2()
147            .gap_2()
148            .items_start()
149            .map(|this| match self.border_position {
150                CalloutBorderPosition::Top => this.border_t_1(),
151                CalloutBorderPosition::Bottom => this.border_b_1(),
152            })
153            .border_color(cx.theme().colors().border)
154            .bg(bg_color)
155            .overflow_x_hidden()
156            .when(self.icon.is_some(), |this| {
157                this.child(
158                    h_flex()
159                        .h(line_height)
160                        .justify_center()
161                        .child(Icon::new(icon).size(IconSize::Small).color(icon_color)),
162                )
163            })
164            .child(
165                v_flex()
166                    .min_w_0()
167                    .min_h_0()
168                    .w_full()
169                    .child(
170                        h_flex()
171                            .min_h(line_height)
172                            .w_full()
173                            .gap_1()
174                            .justify_between()
175                            .flex_wrap()
176                            .when_some(self.title, |this, title| {
177                                this.child(
178                                    div()
179                                        .min_w_0()
180                                        .flex_1()
181                                        .child(Label::new(title).size(LabelSize::Small)),
182                                )
183                            })
184                            .when(has_actions, |this| {
185                                this.child(
186                                    h_flex()
187                                        .gap_0p5()
188                                        .when_some(self.actions_slot, |this, action| {
189                                            this.child(action)
190                                        })
191                                        .when_some(self.dismiss_action, |this, action| {
192                                            this.child(action)
193                                        }),
194                                )
195                            }),
196                    )
197                    .map(|this| {
198                        let base_desc_container = div()
199                            .id("callout-description-slot")
200                            .w_full()
201                            .max_h_32()
202                            .flex_1()
203                            .overflow_y_scroll()
204                            .text_ui_sm(cx);
205
206                        if let Some(description_slot) = self.description_slot {
207                            this.child(base_desc_container.child(description_slot))
208                        } else if let Some(description) = self.description {
209                            this.child(
210                                base_desc_container
211                                    .text_color(cx.theme().colors().text_muted)
212                                    .child(description),
213                            )
214                        } else {
215                            this
216                        }
217                    }),
218            )
219    }
220}
221
222impl Component for Callout {
223    fn scope() -> ComponentScope {
224        ComponentScope::DataDisplay
225    }
226
227    fn description() -> &'static str {
228        "Used to display a callout for situations where the user \
229        needs to know some information, and likely make a decision. \
230        This might be a thread running out of tokens, \
231        or running out of prompts on a plan and needing to upgrade."
232    }
233
234    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
235        let single_action = || Button::new("got-it", "Got it").label_size(LabelSize::Small);
236        let multiple_actions = || {
237            h_flex()
238                .gap_0p5()
239                .child(Button::new("update", "Backup & Update").label_size(LabelSize::Small))
240                .child(Button::new("dismiss", "Dismiss").label_size(LabelSize::Small))
241        };
242
243        let basic_examples = vec![
244            single_example(
245                "Simple with Title Only",
246                Callout::new()
247                    .icon(IconName::Info)
248                    .title("System maintenance scheduled for tonight")
249                    .actions_slot(single_action())
250                    .into_any_element(),
251            )
252            .width(px(580.)),
253            single_example(
254                "With Title and Description",
255                Callout::new()
256                    .icon(IconName::Warning)
257                    .title("Your settings contain deprecated values")
258                    .description(
259                        "We'll backup your current settings and update them to the new format.",
260                    )
261                    .actions_slot(single_action())
262                    .into_any_element(),
263            )
264            .width(px(580.)),
265            single_example(
266                "Error with Multiple Actions",
267                Callout::new()
268                    .icon(IconName::Close)
269                    .title("Thread reached the token limit")
270                    .description("Start a new thread from a summary to continue the conversation.")
271                    .actions_slot(multiple_actions())
272                    .into_any_element(),
273            )
274            .width(px(580.)),
275            single_example(
276                "Multi-line Description",
277                Callout::new()
278                    .icon(IconName::Sparkle)
279                    .title("Upgrade to Pro")
280                    .description("• Unlimited threads\n• Priority support\n• Advanced analytics")
281                    .actions_slot(multiple_actions())
282                    .into_any_element(),
283            )
284            .width(px(580.)),
285            single_example(
286                "Scrollable Long Description",
287                Callout::new()
288                    .severity(Severity::Error)
289                    .icon(IconName::XCircle)
290                    .title("Very Long API Error Description")
291                    .description_slot(
292                        v_flex().gap_1().children(
293                            [
294                                "You exceeded your current quota.",
295                                "For more information, visit the docs.",
296                                "Error details:",
297                                "• Quota exceeded for metric",
298                                "• Limit: 0",
299                                "• Model: gemini-3.1-pro",
300                                "Please retry in 26.33s.",
301                                "Additional details:",
302                                "- Request ID: abc123def456",
303                                "- Timestamp: 2024-01-15T10:30:00Z",
304                                "- Region: us-central1",
305                                "- Service: generativelanguage.googleapis.com",
306                                "- Error Code: RESOURCE_EXHAUSTED",
307                                "- Retry After: 26s",
308                                "This error occurs when you have exceeded your API quota.",
309                            ]
310                            .into_iter()
311                            .map(|t| Label::new(t).size(LabelSize::Small).color(Color::Muted)),
312                        ),
313                    )
314                    .actions_slot(single_action())
315                    .into_any_element(),
316            )
317            .width(px(580.)),
318        ];
319
320        let severity_examples = vec![
321            single_example(
322                "Info",
323                Callout::new()
324                    .icon(IconName::Info)
325                    .title("System maintenance scheduled for tonight")
326                    .actions_slot(single_action())
327                    .into_any_element(),
328            ),
329            single_example(
330                "Warning",
331                Callout::new()
332                    .severity(Severity::Warning)
333                    .icon(IconName::Triangle)
334                    .title("System maintenance scheduled for tonight")
335                    .actions_slot(single_action())
336                    .into_any_element(),
337            ),
338            single_example(
339                "Error",
340                Callout::new()
341                    .severity(Severity::Error)
342                    .icon(IconName::XCircle)
343                    .title("System maintenance scheduled for tonight")
344                    .actions_slot(single_action())
345                    .into_any_element(),
346            ),
347            single_example(
348                "Success",
349                Callout::new()
350                    .severity(Severity::Success)
351                    .icon(IconName::Check)
352                    .title("System maintenance scheduled for tonight")
353                    .actions_slot(single_action())
354                    .into_any_element(),
355            ),
356        ];
357
358        v_flex()
359            .gap_4()
360            .child(example_group(basic_examples).vertical())
361            .child(example_group_with_title("Severity", severity_examples).vertical())
362            .into_any_element()
363    }
364}
365
Served at tenant.openagents/omega Member data and write actions are omitted.