399 lines · 13.5 KB · Rust
RawDownload
use std::sync::Arc; use agent_skills::GLOBAL_SKILLS_DIR_DISPLAY;use auto_update::{AutoUpdater, release_notes_url};use client::zed_urls;use db::kvp::Dismissable;use editor::{Editor, MultiBuffer};use gpui::{    App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, TaskExt, Window, actions,    prelude::*,};use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};use prompt_store::rules_to_skills_migration;use release_channel::{AppVersion, ReleaseChannel};use semver::Version;use serde::Deserialize;use smol::io::AsyncReadExt;use ui::{AnnouncementToast, ListBulletItem, SkillsIllustration, prelude::*};use util::{ResultExt as _, maybe};use workspace::{    Workspace,    notifications::{        Notification, NotificationId, SuppressEvent, show_app_notification,        simple_message_notification::MessageNotification,    },    workspace_error::{ErrorAction, ErrorSeverity, WorkspaceError},};use zed_actions::ShowUpdateNotification; actions!(    auto_update,    [        /// Opens the release notes for the current version in a new tab.        ViewReleaseNotesLocally    ]); pub fn init(cx: &mut App) {    notify_if_app_was_updated(cx);    cx.observe_new(|workspace: &mut Workspace, _window, cx| {        workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, window, cx| {            view_release_notes_locally(workspace, window, cx);        });         if matches!(            ReleaseChannel::global(cx),            ReleaseChannel::Nightly | ReleaseChannel::Dev        ) {            workspace.register_action(|_workspace, _: &ShowUpdateNotification, _window, cx| {                show_update_notification(cx);            });        }    })    .detach();} #[derive(Deserialize)]struct ReleaseNotesBody {    title: String,    release_notes: String,} fn notify_release_notes_failed_to_show(    workspace: &mut Workspace,    _window: &mut Window,    cx: &mut Context<Workspace>,) {    let url = release_notes_url(cx);     struct ReleaseNotesError {        url: Option<String>,    }     impl WorkspaceError for ReleaseNotesError {        fn primary_message(&self) -> SharedString {            "Couldn't load release notes".into()        }        fn severity(&self) -> ErrorSeverity {            ErrorSeverity::Error        }        fn primary_action(&self) -> ErrorAction {            self.url                .clone()                .map(|url| ErrorAction::link("View in Browser", url))                .unwrap_or_else(ErrorAction::dismiss)        }    }     workspace.show_error(ReleaseNotesError { url }, cx);} fn view_release_notes_locally(    workspace: &mut Workspace,    window: &mut Window,    cx: &mut Context<Workspace>,) {    let release_channel = ReleaseChannel::global(cx);     if matches!(        release_channel,        ReleaseChannel::Nightly | ReleaseChannel::Dev    ) {        if let Some(url) = release_notes_url(cx) {            cx.open_url(&url);        }        return;    }     let version = AppVersion::global(cx).to_string();     let client = client::Client::global(cx).http_client();    let url = client.build_url(&format!(        "/api/release_notes/v2/{}/{}",        release_channel.dev_name(),        version    ));     let markdown = workspace        .app_state()        .languages        .language_for_name("Markdown");     cx.spawn_in(window, async move |workspace, cx| {        let markdown = markdown.await.log_err();        let response = client.get(&url, Default::default(), true).await;        let Some(mut response) = response.log_err() else {            workspace                .update_in(cx, notify_release_notes_failed_to_show)                .log_err();            return;        };         let mut body = Vec::new();        response.body_mut().read_to_end(&mut body).await.ok();         let body: serde_json::Result<ReleaseNotesBody> = serde_json::from_slice(body.as_slice());         let res: Option<()> = maybe!(async {            let body = body.ok()?;            let project = workspace                .read_with(cx, |workspace, _| workspace.project().clone())                .ok()?;            let (language_registry, buffer) = project.update(cx, |project, cx| {                (                    project.languages().clone(),                    project.create_buffer(markdown, false, cx),                )            });            let buffer = buffer.await.ok()?;            buffer.update(cx, |buffer, cx| {                buffer.edit([(0..0, body.release_notes)], None, cx)            });             let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(body.title));             let ws_handle = workspace.clone();            workspace                .update_in(cx, |workspace, window, cx| {                    let editor =                        cx.new(|cx| Editor::for_multibuffer(buffer, Some(project), window, cx));                    let markdown_preview: Entity<MarkdownPreviewView> = MarkdownPreviewView::new(                        MarkdownPreviewMode::Default,                        editor,                        ws_handle,                        language_registry,                        window,                        cx,                    );                    workspace.add_item_to_active_pane(                        Box::new(markdown_preview),                        None,                        true,                        window,                        cx,                    );                    cx.notify();                })                .ok()        })        .await;        if res.is_none() {            workspace                .update_in(cx, notify_release_notes_failed_to_show)                .log_err();        }    })    .detach();} #[derive(Clone)]struct AnnouncementContent {    heading: SharedString,    description: SharedString,    bullet_items: Vec<SharedString>,    primary_action_label: SharedString,    secondary_action_label: SharedString,    primary_action_url: Option<SharedString>,    primary_action_callback: Option<Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>>,    secondary_action_url: Option<SharedString>,    on_dismiss: Option<Arc<dyn Fn(&mut App) + Send + Sync>>,} struct SkillsAnnouncement; impl Dismissable for SkillsAnnouncement {    const KEY: &'static str = "skills_announcement_dismissed";} fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementContent> {    let version_with_skills = match ReleaseChannel::global(cx) {        ReleaseChannel::Stable => Version::new(1, 4, 0),        ReleaseChannel::Dev | ReleaseChannel::Nightly | ReleaseChannel::Preview => {            Version::new(1, 4, 0)        }    };     if *version >= version_with_skills && !SkillsAnnouncement::dismissed(cx) {        // Only mention the Rules → Skills migration if the user actually        // had Rules that got migrated. New users (and existing users who        // never created a Rule) would otherwise be confused by a bullet        // referring to "your rules" that don't exist.        let migrated_anything =            rules_to_skills_migration::migration_result().is_some_and(|result| !result.is_empty());         let mut bullet_items: Vec<SharedString> = Vec::with_capacity(3);        bullet_items            .push(format!("Skills live in {GLOBAL_SKILLS_DIR_DISPLAY}/<name>/SKILL.md").into());        bullet_items.push("Type / to manually invoke a skill".into());        if migrated_anything {            bullet_items.push(                "The Rules Library is making way for skills: your default rules are now in a global AGENTS.md, and your other rules have been converted to skills".into(),            );        }         Some(AnnouncementContent {            heading: "Introducing Skills Support".into(),            description: "Extend the agent with focused instructions and domain knowledge.".into(),            bullet_items,            primary_action_label: "Try Now".into(),            secondary_action_label: "Read Documentation".into(),            primary_action_url: None,            primary_action_callback: Some(Arc::new(move |window, cx| {                window.dispatch_action(Box::new(zed_actions::assistant::FocusAgent), cx);            })),            on_dismiss: Some(Arc::new(|cx| SkillsAnnouncement::set_dismissed(true, cx))),            secondary_action_url: Some(zed_urls::skills_docs(cx).into()),        })    } else {        None    }} struct AnnouncementToastNotification {    focus_handle: FocusHandle,    content: AnnouncementContent,} impl AnnouncementToastNotification {    fn new(content: AnnouncementContent, cx: &mut App) -> Self {        Self {            focus_handle: cx.focus_handle(),            content,        }    }     fn dismiss(&mut self, cx: &mut Context<Self>) {        cx.emit(DismissEvent);        if let Some(on_dismiss) = &self.content.on_dismiss {            on_dismiss(cx);        }    }} impl Focusable for AnnouncementToastNotification {    fn focus_handle(&self, _cx: &App) -> FocusHandle {        self.focus_handle.clone()    }} impl EventEmitter<DismissEvent> for AnnouncementToastNotification {}impl EventEmitter<SuppressEvent> for AnnouncementToastNotification {}impl Notification for AnnouncementToastNotification {} impl Render for AnnouncementToastNotification {    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        AnnouncementToast::new()            .illustration(SkillsIllustration::new())            .heading(self.content.heading.clone())            .description(self.content.description.clone())            .bullet_items(                self.content                    .bullet_items                    .iter()                    .map(|item| ListBulletItem::new(item.clone())),            )            .primary_action_label(self.content.primary_action_label.clone())            .secondary_action_label(self.content.secondary_action_label.clone())            .primary_on_click(cx.listener({                let url = self.content.primary_action_url.clone();                let callback = self.content.primary_action_callback.clone();                move |this, _, window, cx| {                    telemetry::event!("Skills Announcement Main Click");                    if let Some(callback) = &callback {                        callback(window, cx);                    }                    if let Some(url) = &url {                        cx.open_url(url);                    }                    this.dismiss(cx);                }            }))            .secondary_on_click(cx.listener({                let url = self.content.secondary_action_url.clone();                move |_, _, _window, cx| {                    telemetry::event!("Skills Announcement Secondary Click");                    if let Some(url) = &url {                        cx.open_url(url);                    }                }            }))            .dismiss_on_click(cx.listener(|this, _, _window, cx| {                telemetry::event!("Skills Announcement Dismiss");                this.dismiss(cx);            }))    }} struct UpdateNotification; fn show_update_notification(cx: &mut App) {    let Some(updater) = AutoUpdater::get(cx) else {        return;    };     let mut version = updater.read(cx).current_version();    version.pre = semver::Prerelease::EMPTY;    version.build = semver::BuildMetadata::EMPTY;    let app_name = ReleaseChannel::global(cx).display_name();     if let Some(content) = announcement_for_version(&version, cx) {        show_app_notification(            NotificationId::unique::<UpdateNotification>(),            cx,            move |cx| cx.new(|cx| AnnouncementToastNotification::new(content.clone(), cx)),        );    } else {        show_app_notification(            NotificationId::unique::<UpdateNotification>(),            cx,            move |cx| {                let workspace_handle = cx.entity().downgrade();                cx.new(|cx| {                    MessageNotification::new(format!("Updated to {app_name} {}", version), cx)                        .primary_message("View Release Notes")                        .primary_on_click(move |window, cx| {                            if let Some(workspace) = workspace_handle.upgrade() {                                workspace.update(cx, |workspace, cx| {                                    crate::view_release_notes_locally(workspace, window, cx);                                })                            }                            cx.emit(DismissEvent);                        })                        .show_suppress_button(false)                })            },        );    }} /// Shows a notification across all workspaces if an update was previously automatically installed/// and this notification had not yet been shown.pub fn notify_if_app_was_updated(cx: &mut App) {    let Some(updater) = AutoUpdater::get(cx) else {        return;    };     if let ReleaseChannel::Nightly = ReleaseChannel::global(cx) {        return;    }     let should_show_notification = updater.read(cx).should_show_update_notification(cx);     cx.spawn(async move |cx| {        let should_show_notification = should_show_notification.await?;         if should_show_notification {            cx.update(|cx| {                show_update_notification(cx);                updater.update(cx, |updater, cx| {                    updater                        .set_should_show_update_notification(false, cx)                        .detach_and_log_err(cx);                });            });        }        anyhow::Ok(())    })    .detach();}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t