auto_update_ui.rs

  1use std::sync::Arc;
  2
  3use agent_settings::{AgentSettings, WindowLayout};
  4use auto_update::{AutoUpdater, release_notes_url};
  5use db::kvp::Dismissable;
  6use editor::{Editor, MultiBuffer};
  7use fs::Fs;
  8use gpui::{
  9    App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Window, actions, prelude::*,
 10};
 11use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
 12use notifications::status_toast::StatusToast;
 13use release_channel::{AppVersion, ReleaseChannel};
 14use semver::Version;
 15use serde::Deserialize;
 16use smol::io::AsyncReadExt;
 17use ui::{AnnouncementToast, ListBulletItem, ParallelAgentsIllustration, prelude::*};
 18use util::{ResultExt as _, maybe};
 19use workspace::{
 20    FocusWorkspaceSidebar, Workspace,
 21    notifications::{
 22        ErrorMessagePrompt, Notification, NotificationId, SuppressEvent, show_app_notification,
 23        simple_message_notification::MessageNotification,
 24    },
 25};
 26use zed_actions::{ShowUpdateNotification, assistant::FocusAgent};
 27
 28actions!(
 29    auto_update,
 30    [
 31        /// Opens the release notes for the current version in a new tab.
 32        ViewReleaseNotesLocally
 33    ]
 34);
 35
 36pub fn init(cx: &mut App) {
 37    notify_if_app_was_updated(cx);
 38    cx.observe_new(|workspace: &mut Workspace, _window, cx| {
 39        workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, window, cx| {
 40            view_release_notes_locally(workspace, window, cx);
 41        });
 42
 43        if matches!(
 44            ReleaseChannel::global(cx),
 45            ReleaseChannel::Nightly | ReleaseChannel::Dev
 46        ) {
 47            workspace.register_action(|_workspace, _: &ShowUpdateNotification, _window, cx| {
 48                show_update_notification(cx);
 49            });
 50        }
 51    })
 52    .detach();
 53}
 54
 55#[derive(Deserialize)]
 56struct ReleaseNotesBody {
 57    title: String,
 58    release_notes: String,
 59}
 60
 61fn notify_release_notes_failed_to_show(
 62    workspace: &mut Workspace,
 63    _window: &mut Window,
 64    cx: &mut Context<Workspace>,
 65) {
 66    struct ViewReleaseNotesError;
 67    workspace.show_notification(
 68        NotificationId::unique::<ViewReleaseNotesError>(),
 69        cx,
 70        |cx| {
 71            cx.new(move |cx| {
 72                let url = release_notes_url(cx);
 73                let mut prompt = ErrorMessagePrompt::new("Couldn't load release notes", cx);
 74                if let Some(url) = url {
 75                    prompt = prompt.with_link_button("View in Browser".to_string(), url);
 76                }
 77                prompt
 78            })
 79        },
 80    );
 81}
 82
 83fn view_release_notes_locally(
 84    workspace: &mut Workspace,
 85    window: &mut Window,
 86    cx: &mut Context<Workspace>,
 87) {
 88    let release_channel = ReleaseChannel::global(cx);
 89
 90    if matches!(
 91        release_channel,
 92        ReleaseChannel::Nightly | ReleaseChannel::Dev
 93    ) {
 94        if let Some(url) = release_notes_url(cx) {
 95            cx.open_url(&url);
 96        }
 97        return;
 98    }
 99
100    let version = AppVersion::global(cx).to_string();
101
102    let client = client::Client::global(cx).http_client();
103    let url = client.build_url(&format!(
104        "/api/release_notes/v2/{}/{}",
105        release_channel.dev_name(),
106        version
107    ));
108
109    let markdown = workspace
110        .app_state()
111        .languages
112        .language_for_name("Markdown");
113
114    cx.spawn_in(window, async move |workspace, cx| {
115        let markdown = markdown.await.log_err();
116        let response = client.get(&url, Default::default(), true).await;
117        let Some(mut response) = response.log_err() else {
118            workspace
119                .update_in(cx, notify_release_notes_failed_to_show)
120                .log_err();
121            return;
122        };
123
124        let mut body = Vec::new();
125        response.body_mut().read_to_end(&mut body).await.ok();
126
127        let body: serde_json::Result<ReleaseNotesBody> = serde_json::from_slice(body.as_slice());
128
129        let res: Option<()> = maybe!(async {
130            let body = body.ok()?;
131            let project = workspace
132                .read_with(cx, |workspace, _| workspace.project().clone())
133                .ok()?;
134            let (language_registry, buffer) = project.update(cx, |project, cx| {
135                (
136                    project.languages().clone(),
137                    project.create_buffer(markdown, false, cx),
138                )
139            });
140            let buffer = buffer.await.ok()?;
141            buffer.update(cx, |buffer, cx| {
142                buffer.edit([(0..0, body.release_notes)], None, cx)
143            });
144
145            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(body.title));
146
147            let ws_handle = workspace.clone();
148            workspace
149                .update_in(cx, |workspace, window, cx| {
150                    let editor =
151                        cx.new(|cx| Editor::for_multibuffer(buffer, Some(project), window, cx));
152                    let markdown_preview: Entity<MarkdownPreviewView> = MarkdownPreviewView::new(
153                        MarkdownPreviewMode::Default,
154                        editor,
155                        ws_handle,
156                        language_registry,
157                        window,
158                        cx,
159                    );
160                    workspace.add_item_to_active_pane(
161                        Box::new(markdown_preview),
162                        None,
163                        true,
164                        window,
165                        cx,
166                    );
167                    cx.notify();
168                })
169                .ok()
170        })
171        .await;
172        if res.is_none() {
173            workspace
174                .update_in(cx, notify_release_notes_failed_to_show)
175                .log_err();
176        }
177    })
178    .detach();
179}
180
181#[derive(Clone)]
182struct AnnouncementContent {
183    heading: SharedString,
184    description: SharedString,
185    bullet_items: Vec<SharedString>,
186    primary_action_label: SharedString,
187    primary_action_url: Option<SharedString>,
188    primary_action_callback: Option<Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>>,
189    secondary_action_url: Option<SharedString>,
190    on_dismiss: Option<Arc<dyn Fn(&mut App) + Send + Sync>>,
191}
192
193struct ParallelAgentAnnouncement;
194
195impl Dismissable for ParallelAgentAnnouncement {
196    const KEY: &'static str = "parallel-agent-announcement";
197}
198
199fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementContent> {
200    let version_with_parallel_agents = match ReleaseChannel::global(cx) {
201        ReleaseChannel::Stable => Version::new(0, 233, 0),
202        ReleaseChannel::Dev | ReleaseChannel::Nightly | ReleaseChannel::Preview => {
203            Version::new(0, 232, 0)
204        }
205    };
206
207    if *version >= version_with_parallel_agents && !ParallelAgentAnnouncement::dismissed(cx) {
208        let fs = <dyn Fs>::global(cx);
209        Some(AnnouncementContent {
210            heading: "Introducing Parallel Agents".into(),
211            description: "Run multiple threads of your favorite agents simultaneously across projects in a new workspace layout, tailored for agentic workflows.".into(),
212            bullet_items: vec![
213                "Use your favorite agents in parallel".into(),
214                "Optionally isolate agents using worktrees".into(),
215                "Combine multiple projects in one window".into(),
216            ],
217            primary_action_label: "Try Agentic Layout".into(),
218            primary_action_url: None,
219            primary_action_callback: Some(Arc::new(move |window, cx| {
220                let get_layout = AgentSettings::get_layout(cx);
221                let already_agent_layout = matches!(get_layout, WindowLayout::Agent(_));
222
223                let update;
224                if !already_agent_layout {
225                    update = Some(AgentSettings::set_layout(
226                        WindowLayout::Agent(None),
227                        fs.clone(),
228                        cx,
229                    ));
230                } else {
231                    update = None;
232                }
233
234                let revert_fs = fs.clone();
235                window
236                    .spawn(cx, async move |cx| {
237                        if let Some(update) = update {
238                            update.await.ok();
239                        }
240
241                        cx.update(|window, cx| {
242                            if !already_agent_layout {
243                                if let Some(workspace) = Workspace::for_window(window, cx) {
244                                    let toast = StatusToast::new(
245                                        "You are in the new agentic layout!",
246                                        cx,
247                                        move |this, _cx| {
248                                            this.icon(
249                                                Icon::new(IconName::Check)
250                                                    .size(IconSize::Small)
251                                                    .color(Color::Success),
252                                            )
253                                            .action("Revert", move |_window, cx| {
254                                                let _ = AgentSettings::set_layout(
255                                                    get_layout.clone(),
256                                                    revert_fs.clone(),
257                                                    cx,
258                                                );
259                                            })
260                                            .auto_dismiss(false)
261                                            .dismiss_button(true)
262                                        },
263                                    );
264
265                                    workspace.update(cx, |workspace, cx| {
266                                        workspace.toggle_status_toast(toast, cx);
267                                    });
268                                }
269                            }
270
271                            window.dispatch_action(Box::new(FocusWorkspaceSidebar), cx);
272                            window.dispatch_action(Box::new(FocusAgent), cx);
273                        })
274                    })
275                    .detach();
276            })),
277            on_dismiss: Some(Arc::new(|cx| {
278                ParallelAgentAnnouncement::set_dismissed(true, cx)
279            })),
280            secondary_action_url: Some("https://zed.dev/blog/".into()),
281        })
282    } else {
283        None
284    }
285}
286
287struct AnnouncementToastNotification {
288    focus_handle: FocusHandle,
289    content: AnnouncementContent,
290}
291
292impl AnnouncementToastNotification {
293    fn new(content: AnnouncementContent, cx: &mut App) -> Self {
294        Self {
295            focus_handle: cx.focus_handle(),
296            content,
297        }
298    }
299
300    fn dismiss(&mut self, cx: &mut Context<Self>) {
301        cx.emit(DismissEvent);
302        if let Some(on_dismiss) = &self.content.on_dismiss {
303            on_dismiss(cx);
304        }
305    }
306}
307
308impl Focusable for AnnouncementToastNotification {
309    fn focus_handle(&self, _cx: &App) -> FocusHandle {
310        self.focus_handle.clone()
311    }
312}
313
314impl EventEmitter<DismissEvent> for AnnouncementToastNotification {}
315impl EventEmitter<SuppressEvent> for AnnouncementToastNotification {}
316impl Notification for AnnouncementToastNotification {}
317
318impl Render for AnnouncementToastNotification {
319    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
320        AnnouncementToast::new()
321            .illustration(ParallelAgentsIllustration::new())
322            .heading(self.content.heading.clone())
323            .description(self.content.description.clone())
324            .bullet_items(
325                self.content
326                    .bullet_items
327                    .iter()
328                    .map(|item| ListBulletItem::new(item.clone())),
329            )
330            .primary_action_label(self.content.primary_action_label.clone())
331            .primary_on_click(cx.listener({
332                let url = self.content.primary_action_url.clone();
333                let callback = self.content.primary_action_callback.clone();
334                move |this, _, window, cx| {
335                    telemetry::event!("Parallel Agent Announcement Main Click");
336                    if let Some(callback) = &callback {
337                        callback(window, cx);
338                    }
339                    if let Some(url) = &url {
340                        cx.open_url(url);
341                    }
342                    this.dismiss(cx);
343                }
344            }))
345            .secondary_on_click(cx.listener({
346                let url = self.content.secondary_action_url.clone();
347                move |_, _, _window, cx| {
348                    telemetry::event!("Parallel Agent Announcement Secondary Click");
349                    if let Some(url) = &url {
350                        cx.open_url(url);
351                    }
352                }
353            }))
354            .dismiss_on_click(cx.listener(|this, _, _window, cx| {
355                telemetry::event!("Parallel Agent Announcement Dismiss");
356                this.dismiss(cx);
357            }))
358    }
359}
360
361struct UpdateNotification;
362
363fn show_update_notification(cx: &mut App) {
364    let Some(updater) = AutoUpdater::get(cx) else {
365        return;
366    };
367
368    let mut version = updater.read(cx).current_version();
369    version.pre = semver::Prerelease::EMPTY;
370    version.build = semver::BuildMetadata::EMPTY;
371    let app_name = ReleaseChannel::global(cx).display_name();
372
373    if let Some(content) = announcement_for_version(&version, cx) {
374        show_app_notification(
375            NotificationId::unique::<UpdateNotification>(),
376            cx,
377            move |cx| cx.new(|cx| AnnouncementToastNotification::new(content.clone(), cx)),
378        );
379    } else {
380        show_app_notification(
381            NotificationId::unique::<UpdateNotification>(),
382            cx,
383            move |cx| {
384                let workspace_handle = cx.entity().downgrade();
385                cx.new(|cx| {
386                    MessageNotification::new(format!("Updated to {app_name} {}", version), cx)
387                        .primary_message("View Release Notes")
388                        .primary_on_click(move |window, cx| {
389                            if let Some(workspace) = workspace_handle.upgrade() {
390                                workspace.update(cx, |workspace, cx| {
391                                    crate::view_release_notes_locally(workspace, window, cx);
392                                })
393                            }
394                            cx.emit(DismissEvent);
395                        })
396                        .show_suppress_button(false)
397                })
398            },
399        );
400    }
401}
402
403/// Shows a notification across all workspaces if an update was previously automatically installed
404/// and this notification had not yet been shown.
405pub fn notify_if_app_was_updated(cx: &mut App) {
406    let Some(updater) = AutoUpdater::get(cx) else {
407        return;
408    };
409
410    if let ReleaseChannel::Nightly = ReleaseChannel::global(cx) {
411        return;
412    }
413
414    let should_show_notification = updater.read(cx).should_show_update_notification(cx);
415
416    cx.spawn(async move |cx| {
417        let should_show_notification = should_show_notification.await?;
418
419        if should_show_notification {
420            cx.update(|cx| {
421                show_update_notification(cx);
422                updater.update(cx, |updater, cx| {
423                    updater
424                        .set_should_show_update_notification(false, cx)
425                        .detach_and_log_err(cx);
426                });
427            });
428        }
429        anyhow::Ok(())
430    })
431    .detach();
432}