Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:39:13.332Z 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

auto_update_ui.rs

400 lines · 13.9 KB · rust
1use std::sync::Arc;
2
3use agent_skills::GLOBAL_SKILLS_DIR_DISPLAY;
4use auto_update::{AutoUpdater, release_notes_url};
5use client::zed_urls;
6use db::kvp::Dismissable;
7use editor::{Editor, MultiBuffer};
8use gpui::{
9    App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, TaskExt, Window, actions,
10    prelude::*,
11};
12use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
13use prompt_store::rules_to_skills_migration;
14use release_channel::{AppVersion, ReleaseChannel};
15use semver::Version;
16use serde::Deserialize;
17use smol::io::AsyncReadExt;
18use ui::{AnnouncementToast, ListBulletItem, SkillsIllustration, prelude::*};
19use util::{ResultExt as _, maybe};
20use workspace::{
21    Workspace,
22    notifications::{
23        Notification, NotificationId, SuppressEvent, show_app_notification,
24        simple_message_notification::MessageNotification,
25    },
26    workspace_error::{ErrorAction, ErrorSeverity, WorkspaceError},
27};
28use zed_actions::ShowUpdateNotification;
29
30actions!(
31    auto_update,
32    [
33        /// Opens the release notes for the current version in a new tab.
34        ViewReleaseNotesLocally
35    ]
36);
37
38pub fn init(cx: &mut App) {
39    notify_if_app_was_updated(cx);
40    cx.observe_new(|workspace: &mut Workspace, _window, cx| {
41        workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, window, cx| {
42            view_release_notes_locally(workspace, window, cx);
43        });
44
45        if matches!(
46            ReleaseChannel::global(cx),
47            ReleaseChannel::Nightly | ReleaseChannel::Dev
48        ) {
49            workspace.register_action(|_workspace, _: &ShowUpdateNotification, _window, cx| {
50                show_update_notification(cx);
51            });
52        }
53    })
54    .detach();
55}
56
57#[derive(Deserialize)]
58struct ReleaseNotesBody {
59    title: String,
60    release_notes: String,
61}
62
63fn notify_release_notes_failed_to_show(
64    workspace: &mut Workspace,
65    _window: &mut Window,
66    cx: &mut Context<Workspace>,
67) {
68    let url = release_notes_url(cx);
69
70    struct ReleaseNotesError {
71        url: Option<String>,
72    }
73
74    impl WorkspaceError for ReleaseNotesError {
75        fn primary_message(&self) -> SharedString {
76            "Couldn't load release notes".into()
77        }
78        fn severity(&self) -> ErrorSeverity {
79            ErrorSeverity::Error
80        }
81        fn primary_action(&self) -> ErrorAction {
82            self.url
83                .clone()
84                .map(|url| ErrorAction::link("View in Browser", url))
85                .unwrap_or_else(ErrorAction::dismiss)
86        }
87    }
88
89    workspace.show_error(ReleaseNotesError { url }, cx);
90}
91
92fn view_release_notes_locally(
93    workspace: &mut Workspace,
94    window: &mut Window,
95    cx: &mut Context<Workspace>,
96) {
97    let release_channel = ReleaseChannel::global(cx);
98
99    if matches!(
100        release_channel,
101        ReleaseChannel::Nightly | ReleaseChannel::Dev
102    ) {
103        if let Some(url) = release_notes_url(cx) {
104            cx.open_url(&url);
105        }
106        return;
107    }
108
109    let version = AppVersion::global(cx).to_string();
110
111    let client = client::Client::global(cx).http_client();
112    let url = client.build_url(&format!(
113        "/api/release_notes/v2/{}/{}",
114        release_channel.dev_name(),
115        version
116    ));
117
118    let markdown = workspace
119        .app_state()
120        .languages
121        .language_for_name("Markdown");
122
123    cx.spawn_in(window, async move |workspace, cx| {
124        let markdown = markdown.await.log_err();
125        let response = client.get(&url, Default::default(), true).await;
126        let Some(mut response) = response.log_err() else {
127            workspace
128                .update_in(cx, notify_release_notes_failed_to_show)
129                .log_err();
130            return;
131        };
132
133        let mut body = Vec::new();
134        response.body_mut().read_to_end(&mut body).await.ok();
135
136        let body: serde_json::Result<ReleaseNotesBody> = serde_json::from_slice(body.as_slice());
137
138        let res: Option<()> = maybe!(async {
139            let body = body.ok()?;
140            let project = workspace
141                .read_with(cx, |workspace, _| workspace.project().clone())
142                .ok()?;
143            let (language_registry, buffer) = project.update(cx, |project, cx| {
144                (
145                    project.languages().clone(),
146                    project.create_buffer(markdown, false, cx),
147                )
148            });
149            let buffer = buffer.await.ok()?;
150            buffer.update(cx, |buffer, cx| {
151                buffer.edit([(0..0, body.release_notes)], None, cx)
152            });
153
154            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(body.title));
155
156            let ws_handle = workspace.clone();
157            workspace
158                .update_in(cx, |workspace, window, cx| {
159                    let editor =
160                        cx.new(|cx| Editor::for_multibuffer(buffer, Some(project), window, cx));
161                    let markdown_preview: Entity<MarkdownPreviewView> = MarkdownPreviewView::new(
162                        MarkdownPreviewMode::Default,
163                        editor,
164                        ws_handle,
165                        language_registry,
166                        window,
167                        cx,
168                    );
169                    workspace.add_item_to_active_pane(
170                        Box::new(markdown_preview),
171                        None,
172                        true,
173                        window,
174                        cx,
175                    );
176                    cx.notify();
177                })
178                .ok()
179        })
180        .await;
181        if res.is_none() {
182            workspace
183                .update_in(cx, notify_release_notes_failed_to_show)
184                .log_err();
185        }
186    })
187    .detach();
188}
189
190#[derive(Clone)]
191struct AnnouncementContent {
192    heading: SharedString,
193    description: SharedString,
194    bullet_items: Vec<SharedString>,
195    primary_action_label: SharedString,
196    secondary_action_label: SharedString,
197    primary_action_url: Option<SharedString>,
198    primary_action_callback: Option<Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>>,
199    secondary_action_url: Option<SharedString>,
200    on_dismiss: Option<Arc<dyn Fn(&mut App) + Send + Sync>>,
201}
202
203struct SkillsAnnouncement;
204
205impl Dismissable for SkillsAnnouncement {
206    const KEY: &'static str = "skills_announcement_dismissed";
207}
208
209fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementContent> {
210    let version_with_skills = match ReleaseChannel::global(cx) {
211        ReleaseChannel::Stable => Version::new(1, 4, 0),
212        ReleaseChannel::Dev | ReleaseChannel::Nightly | ReleaseChannel::Preview => {
213            Version::new(1, 4, 0)
214        }
215    };
216
217    if *version >= version_with_skills && !SkillsAnnouncement::dismissed(cx) {
218        // Only mention the Rules → Skills migration if the user actually
219        // had Rules that got migrated. New users (and existing users who
220        // never created a Rule) would otherwise be confused by a bullet
221        // referring to "your rules" that don't exist.
222        let migrated_anything =
223            rules_to_skills_migration::migration_result().is_some_and(|result| !result.is_empty());
224
225        let mut bullet_items: Vec<SharedString> = Vec::with_capacity(3);
226        bullet_items
227            .push(format!("Skills live in {GLOBAL_SKILLS_DIR_DISPLAY}/<name>/SKILL.md").into());
228        bullet_items.push("Type / to manually invoke a skill".into());
229        if migrated_anything {
230            bullet_items.push(
231                "The Rules Library is making way for skills: your default rules are now in a global AGENTS.md, and your other rules have been converted to skills".into(),
232            );
233        }
234
235        Some(AnnouncementContent {
236            heading: "Introducing Skills Support".into(),
237            description: "Extend the agent with focused instructions and domain knowledge.".into(),
238            bullet_items,
239            primary_action_label: "Try Now".into(),
240            secondary_action_label: "Read Documentation".into(),
241            primary_action_url: None,
242            primary_action_callback: Some(Arc::new(move |window, cx| {
243                window.dispatch_action(Box::new(zed_actions::assistant::FocusAgent), cx);
244            })),
245            on_dismiss: Some(Arc::new(|cx| SkillsAnnouncement::set_dismissed(true, cx))),
246            secondary_action_url: Some(zed_urls::skills_docs(cx).into()),
247        })
248    } else {
249        None
250    }
251}
252
253struct AnnouncementToastNotification {
254    focus_handle: FocusHandle,
255    content: AnnouncementContent,
256}
257
258impl AnnouncementToastNotification {
259    fn new(content: AnnouncementContent, cx: &mut App) -> Self {
260        Self {
261            focus_handle: cx.focus_handle(),
262            content,
263        }
264    }
265
266    fn dismiss(&mut self, cx: &mut Context<Self>) {
267        cx.emit(DismissEvent);
268        if let Some(on_dismiss) = &self.content.on_dismiss {
269            on_dismiss(cx);
270        }
271    }
272}
273
274impl Focusable for AnnouncementToastNotification {
275    fn focus_handle(&self, _cx: &App) -> FocusHandle {
276        self.focus_handle.clone()
277    }
278}
279
280impl EventEmitter<DismissEvent> for AnnouncementToastNotification {}
281impl EventEmitter<SuppressEvent> for AnnouncementToastNotification {}
282impl Notification for AnnouncementToastNotification {}
283
284impl Render for AnnouncementToastNotification {
285    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
286        AnnouncementToast::new()
287            .illustration(SkillsIllustration::new())
288            .heading(self.content.heading.clone())
289            .description(self.content.description.clone())
290            .bullet_items(
291                self.content
292                    .bullet_items
293                    .iter()
294                    .map(|item| ListBulletItem::new(item.clone())),
295            )
296            .primary_action_label(self.content.primary_action_label.clone())
297            .secondary_action_label(self.content.secondary_action_label.clone())
298            .primary_on_click(cx.listener({
299                let url = self.content.primary_action_url.clone();
300                let callback = self.content.primary_action_callback.clone();
301                move |this, _, window, cx| {
302                    telemetry::event!("Skills Announcement Main Click");
303                    if let Some(callback) = &callback {
304                        callback(window, cx);
305                    }
306                    if let Some(url) = &url {
307                        cx.open_url(url);
308                    }
309                    this.dismiss(cx);
310                }
311            }))
312            .secondary_on_click(cx.listener({
313                let url = self.content.secondary_action_url.clone();
314                move |_, _, _window, cx| {
315                    telemetry::event!("Skills Announcement Secondary Click");
316                    if let Some(url) = &url {
317                        cx.open_url(url);
318                    }
319                }
320            }))
321            .dismiss_on_click(cx.listener(|this, _, _window, cx| {
322                telemetry::event!("Skills Announcement Dismiss");
323                this.dismiss(cx);
324            }))
325    }
326}
327
328struct UpdateNotification;
329
330fn show_update_notification(cx: &mut App) {
331    let Some(updater) = AutoUpdater::get(cx) else {
332        return;
333    };
334
335    let mut version = updater.read(cx).current_version();
336    version.pre = semver::Prerelease::EMPTY;
337    version.build = semver::BuildMetadata::EMPTY;
338    let app_name = ReleaseChannel::global(cx).display_name();
339
340    if let Some(content) = announcement_for_version(&version, cx) {
341        show_app_notification(
342            NotificationId::unique::<UpdateNotification>(),
343            cx,
344            move |cx| cx.new(|cx| AnnouncementToastNotification::new(content.clone(), cx)),
345        );
346    } else {
347        show_app_notification(
348            NotificationId::unique::<UpdateNotification>(),
349            cx,
350            move |cx| {
351                let workspace_handle = cx.entity().downgrade();
352                cx.new(|cx| {
353                    MessageNotification::new(format!("Updated to {app_name} {}", version), cx)
354                        .primary_message("View Release Notes")
355                        .primary_on_click(move |window, cx| {
356                            if let Some(workspace) = workspace_handle.upgrade() {
357                                workspace.update(cx, |workspace, cx| {
358                                    crate::view_release_notes_locally(workspace, window, cx);
359                                })
360                            }
361                            cx.emit(DismissEvent);
362                        })
363                        .show_suppress_button(false)
364                })
365            },
366        );
367    }
368}
369
370/// Shows a notification across all workspaces if an update was previously automatically installed
371/// and this notification had not yet been shown.
372pub fn notify_if_app_was_updated(cx: &mut App) {
373    let Some(updater) = AutoUpdater::get(cx) else {
374        return;
375    };
376
377    if let ReleaseChannel::Nightly = ReleaseChannel::global(cx) {
378        return;
379    }
380
381    let should_show_notification = updater.read(cx).should_show_update_notification(cx);
382
383    cx.spawn(async move |cx| {
384        let should_show_notification = should_show_notification.await?;
385
386        if should_show_notification {
387            cx.update(|cx| {
388                show_update_notification(cx);
389                updater.update(cx, |updater, cx| {
390                    updater
391                        .set_should_show_update_notification(false, cx)
392                        .detach_and_log_err(cx);
393                });
394            });
395        }
396        anyhow::Ok(())
397    })
398    .detach();
399}
400
Served at tenant.openagents/omega Member data and write actions are omitted.