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