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}