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}