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