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