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