Skip to repository content400 lines · 13.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:39:13.332Z 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
auto_update_ui.rs
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