auto_update_ui.rs

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