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