logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
36
crates/auto_update_ui/Cargo.toml
Normal file
36
crates/auto_update_ui/Cargo.toml
Normal file
@@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "auto_update_ui"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/auto_update_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
auto_update.workspace = true
|
||||
client.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
notifications.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
1
crates/auto_update_ui/LICENSE-GPL
Symbolic link
1
crates/auto_update_ui/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
437
crates/auto_update_ui/src/auto_update_ui.rs
Normal file
437
crates/auto_update_ui/src/auto_update_ui.rs
Normal file
@@ -0,0 +1,437 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{AgentSettings, WindowLayout};
|
||||
use auto_update::{AutoUpdater, release_notes_url};
|
||||
use db::kvp::Dismissable;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, TaskExt, Window, actions,
|
||||
prelude::*,
|
||||
};
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use notifications::status_toast::StatusToast;
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings as _;
|
||||
use smol::io::AsyncReadExt;
|
||||
use ui::{AnnouncementToast, ListBulletItem, ParallelAgentsIllustration, prelude::*};
|
||||
use util::{ResultExt as _, maybe};
|
||||
use workspace::{
|
||||
FocusWorkspaceSidebar, Workspace,
|
||||
notifications::{
|
||||
ErrorMessagePrompt, Notification, NotificationId, SuppressEvent, show_app_notification,
|
||||
simple_message_notification::MessageNotification,
|
||||
},
|
||||
};
|
||||
use zed_actions::{ShowUpdateNotification, assistant::FocusAgent};
|
||||
|
||||
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>,
|
||||
) {
|
||||
struct ViewReleaseNotesError;
|
||||
workspace.show_notification(
|
||||
NotificationId::unique::<ViewReleaseNotesError>(),
|
||||
cx,
|
||||
|cx| {
|
||||
cx.new(move |cx| {
|
||||
let url = release_notes_url(cx);
|
||||
let mut prompt = ErrorMessagePrompt::new("Couldn't load release notes", cx);
|
||||
if let Some(url) = url {
|
||||
prompt = prompt.with_link_button("View in Browser".to_string(), url);
|
||||
}
|
||||
prompt
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
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 ParallelAgentAnnouncement;
|
||||
|
||||
impl Dismissable for ParallelAgentAnnouncement {
|
||||
const KEY: &'static str = "parallel-agent-announcement";
|
||||
}
|
||||
|
||||
fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementContent> {
|
||||
let version_with_parallel_agents = match ReleaseChannel::global(cx) {
|
||||
ReleaseChannel::Stable => Version::new(0, 233, 0),
|
||||
ReleaseChannel::Dev | ReleaseChannel::Nightly | ReleaseChannel::Preview => {
|
||||
Version::new(0, 232, 0)
|
||||
}
|
||||
};
|
||||
|
||||
if *version >= version_with_parallel_agents
|
||||
&& !ParallelAgentAnnouncement::dismissed(cx)
|
||||
&& !project::DisableAiSettings::get_global(cx).disable_ai
|
||||
{
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
Some(AnnouncementContent {
|
||||
heading: "Introducing Parallel Agents".into(),
|
||||
description: "Run multiple threads of your favorite agents simultaneously across projects in a new workspace layout, tailored for agentic workflows.".into(),
|
||||
bullet_items: vec![
|
||||
"Use your favorite agents in parallel".into(),
|
||||
"Optionally isolate agents using worktrees".into(),
|
||||
"Combine multiple projects in one window".into(),
|
||||
],
|
||||
primary_action_label: "Try Agentic Layout".into(),
|
||||
primary_action_url: None,
|
||||
primary_action_callback: Some(Arc::new(move |window, cx| {
|
||||
let get_layout = AgentSettings::get_layout(cx);
|
||||
let already_agent_layout = matches!(get_layout, WindowLayout::Agent(_));
|
||||
|
||||
let update;
|
||||
if !already_agent_layout {
|
||||
update = Some(AgentSettings::set_layout(
|
||||
WindowLayout::Agent(None),
|
||||
fs.clone(),
|
||||
cx,
|
||||
));
|
||||
} else {
|
||||
update = None;
|
||||
}
|
||||
|
||||
let revert_fs = fs.clone();
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
if let Some(update) = update {
|
||||
update.await.ok();
|
||||
}
|
||||
|
||||
cx.update(|window, cx| {
|
||||
if !already_agent_layout {
|
||||
if let Some(workspace) = Workspace::for_window(window, cx) {
|
||||
let toast = StatusToast::new(
|
||||
"You are in the new agentic layout!",
|
||||
cx,
|
||||
move |this, _cx| {
|
||||
this.icon(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
.action("Revert", move |_window, cx| {
|
||||
let _ = AgentSettings::set_layout(
|
||||
get_layout.clone(),
|
||||
revert_fs.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.auto_dismiss(false)
|
||||
.dismiss_button(true)
|
||||
},
|
||||
);
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_status_toast(toast, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.dispatch_action(Box::new(FocusWorkspaceSidebar), cx);
|
||||
window.dispatch_action(Box::new(FocusAgent), cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
})),
|
||||
on_dismiss: Some(Arc::new(|cx| {
|
||||
ParallelAgentAnnouncement::set_dismissed(true, cx)
|
||||
})),
|
||||
secondary_action_url: Some("https://zed.dev/blog/".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(ParallelAgentsIllustration::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())
|
||||
.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!("Parallel Agent 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!("Parallel Agent Announcement Secondary Click");
|
||||
if let Some(url) = &url {
|
||||
cx.open_url(url);
|
||||
}
|
||||
}
|
||||
}))
|
||||
.dismiss_on_click(cx.listener(|this, _, _window, cx| {
|
||||
telemetry::event!("Parallel Agent 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();
|
||||
}
|
||||
Reference in New Issue
Block a user