Skip to repository content

tenant.openagents/omega

No repository description is available.

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

onboarding_banner.rs

179 lines · 6.1 KB · rust
1// This module provides infrastructure for showing onboarding banners in the title bar.
2// Currently used by the "Skills have replaced Rules" announcement; older usages
3// (Claude Agent, ACP) lived here previously and were removed.
4#![allow(dead_code)]
5
6use gpui::{Action, Entity, Global, Render, SharedString, TaskExt};
7use ui::{ButtonLike, Tooltip, prelude::*};
8use util::ResultExt;
9
10/// Prompts the user to try newly released Omega features
11pub struct OnboardingBanner {
12    dismissed: bool,
13    source: String,
14    details: BannerDetails,
15    visible_when: Option<Box<dyn Fn(&mut App) -> bool>>,
16}
17
18#[derive(Clone)]
19struct BannerGlobal {
20    entity: Entity<OnboardingBanner>,
21}
22impl Global for BannerGlobal {}
23
24pub struct BannerDetails {
25    pub action: Box<dyn Action>,
26    pub icon_name: IconName,
27    pub label: SharedString,
28    pub subtitle: Option<SharedString>,
29}
30
31impl OnboardingBanner {
32    pub fn new(
33        source: &str,
34        icon_name: IconName,
35        label: impl Into<SharedString>,
36        subtitle: Option<SharedString>,
37        action: Box<dyn Action>,
38        cx: &mut Context<Self>,
39    ) -> Self {
40        cx.set_global(BannerGlobal {
41            entity: cx.entity(),
42        });
43        Self {
44            source: source.to_string(),
45            details: BannerDetails {
46                action,
47                icon_name,
48                label: label.into(),
49                subtitle: subtitle.or(Some(SharedString::from("Introducing:"))),
50            },
51            visible_when: None,
52            dismissed: get_dismissed(source, cx),
53        }
54    }
55
56    pub fn visible_when(mut self, predicate: impl Fn(&mut App) -> bool + 'static) -> Self {
57        self.visible_when = Some(Box::new(predicate));
58        self
59    }
60
61    fn should_show(&self, cx: &mut App) -> bool {
62        !self.dismissed && self.visible_when.as_ref().map_or(true, |f| f(cx))
63    }
64
65    fn dismiss(&mut self, cx: &mut Context<Self>) {
66        persist_dismissed(&self.source, cx);
67        self.dismissed = true;
68        cx.notify();
69    }
70}
71
72fn dismissed_at_key(source: &str) -> String {
73    if source == "Git Onboarding" {
74        "zed_git_banner_dismissed_at".to_string()
75    } else {
76        format!(
77            "{}_banner_dismissed_at",
78            source.to_lowercase().trim().replace(" ", "_")
79        )
80    }
81}
82
83fn get_dismissed(source: &str, cx: &App) -> bool {
84    let dismissed_at = dismissed_at_key(source);
85    db::kvp::KeyValueStore::global(cx)
86        .read_kvp(&dismissed_at)
87        .log_err()
88        .is_some_and(|dismissed| dismissed.is_some())
89}
90
91fn persist_dismissed(source: &str, cx: &mut App) {
92    let dismissed_at = dismissed_at_key(source);
93    let kvp = db::kvp::KeyValueStore::global(cx);
94    cx.spawn(async move |_| {
95        let time = chrono::Utc::now().to_rfc3339();
96        kvp.write_kvp(dismissed_at, time).await
97    })
98    .detach_and_log_err(cx);
99}
100
101pub fn restore_banner(cx: &mut App) {
102    if let Some(banner_global) = cx.try_global::<BannerGlobal>() {
103        let entity = banner_global.entity.clone();
104        cx.defer(move |cx| {
105            entity.update(cx, |this, cx| {
106                this.dismissed = false;
107                cx.notify();
108            });
109        });
110
111        let source = &cx.global::<BannerGlobal>().entity.read(cx).source;
112        let dismissed_at = dismissed_at_key(source);
113        let kvp = db::kvp::KeyValueStore::global(cx);
114        cx.spawn(async move |_| kvp.delete_kvp(dismissed_at).await)
115            .detach_and_log_err(cx);
116    }
117}
118
119impl Render for OnboardingBanner {
120    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
121        if !self.should_show(cx) {
122            return div();
123        }
124
125        let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
126        let banner = h_flex()
127            .rounded_sm()
128            .border_1()
129            .border_color(border_color)
130            .occlude()
131            .child(
132                ButtonLike::new("try-a-feature")
133                    .child(
134                        h_flex()
135                            .h_full()
136                            .gap_1()
137                            .child(Icon::new(self.details.icon_name).size(IconSize::XSmall))
138                            .child(
139                                h_flex()
140                                    .gap_0p5()
141                                    .when_some(self.details.subtitle.as_ref(), |this, subtitle| {
142                                        this.child(
143                                            Label::new(subtitle)
144                                                .size(LabelSize::Small)
145                                                .color(Color::Muted),
146                                        )
147                                    })
148                                    .child(Label::new(&self.details.label).size(LabelSize::Small)),
149                            ),
150                    )
151                    .on_click(cx.listener(|this, _, window, cx| {
152                        telemetry::event!("Banner Clicked", source = this.source);
153                        this.dismiss(cx);
154                        window.dispatch_action(this.details.action.boxed_clone(), cx)
155                    })),
156            )
157            .child(
158                div().border_l_1().border_color(border_color).child(
159                    IconButton::new("close", IconName::Close)
160                        .icon_size(IconSize::Indicator)
161                        .on_click(cx.listener(|this, _, _window, cx| {
162                            telemetry::event!("Banner Dismissed", source = this.source);
163                            this.dismiss(cx)
164                        }))
165                        .tooltip(|_window, cx| {
166                            Tooltip::with_meta(
167                                "Close Announcement Banner",
168                                None,
169                                "It won't show again for this feature",
170                                cx,
171                            )
172                        }),
173                ),
174            );
175
176        div().pr_2().child(banner)
177    }
178}
179
Served at tenant.openagents/omega Member data and write actions are omitted.