mirror of
https://github.com/block/buzz.git
synced 2026-11-13 21:01:53 +00:00
## Summary The Buzz 1.0 desktop app now lives in [block/buzz-app](https://github.com/block/buzz-app). This adds a banner to the 0.X desktop app that tells users, and lets them download the new app's installer without leaving the app. - A full-width banner sits under the top chrome: "A new Buzz app is here! This version is being retired, so grab the new one whenever you get a chance." - **Upgrade** downloads the installer for this machine inside the app. A dialog shows the progress, then the saved file's name and folder with **Show in Finder** (worded per platform) and **Done**. The file goes to the Downloads folder and is revealed in the file manager. It is never opened, mounted or run. - **Cancel**, Escape or the dialog's close button stops a running download and removes the partial file. - When no installer can be resolved for this machine (unsupported platform, or the manifest fetch fails or is slow), **Upgrade** opens `https://buzz.xyz` in the browser instead. - **More info** opens the [upgrade guide](https://github.com/block/buzz-app/blob/main/docs/upgrading.md) in the browser. - Dismissing (×, labelled "Remind me later") stores a timestamp in `localStorage`. The banner is read once per mount, so it comes back on the next launch (or community switch) once 24h have passed. - It is hidden in the huddle room window. ## Demo https://github.com/user-attachments/assets/1f1916e8-f32d-4ec0-8e60-fe7c85dd4a57 Recorded on macOS (Apple Silicon) from a native debug build with the real Rust backend: a full download, **Done**, then a second download cancelled part-way. - The network is throttled through a local proxy so the progress stays visible. Unthrottled, the installer arrives in about two seconds. - The app was launched with a scratch home folder, so the path in the dialog is a scratch path, and the Finder window that the reveal opens is left out of the recording. - Presses are accessibility presses, marked with an orange ring. The OS cursor is not shown. ## How the download works - **Which installer:** a Tauri command reports this install's key (`macos-arm64`, `macos-x64`, `windows-x64`, `linux-appimage` or `linux-deb`). The webview reads the matching entry from the [buzz-app download manifest](https://raw.githubusercontent.com/block/buzz-app/download-feed/downloads.json) once per banner mount. - **Validation:** Rust re-validates the URL on every call. It must be `https://github.com/block/buzz-app/releases/download/<tag>/<file>`, with a restricted tag and file-name character set and the extension this machine installs. The webview names a URL and nothing else. - **Redirects:** a dedicated HTTPS-only client follows at most 5 redirects, and only to `github.com`, `objects.githubusercontent.com` or `release-assets.githubusercontent.com`. - **Limits:** 512 MiB, 15 s to connect, 30 s for headers, 30 s without a byte, 60 min in total. One download at a time. - **Saving:** the body is written to `<name>.<random>.part` in the Downloads folder, then renamed to `<name>` (or `<name> (1)` and so on) without ever replacing an existing file. The partial file is removed on failure, on cancel and at app exit. A partial left by a crash or force-quit is removed on the next launch. - **Downloaded-from-the-internet marking:** macOS gets the `com.apple.quarantine` attribute a browser would write, and Windows gets the Mark of the Web (`Zone.Identifier`). If the mark cannot be written, the download fails and the file is not kept. - **Failures:** the dialog says what went wrong in plain words. Transient failures lead with **Try again**. Failures a retry cannot fix lead with **Download in browser**, which opens the validated installer URL. ## Testing - Unit: `deprecationBannerVisibility.test.mjs`, `newAppInstaller.test.mjs` (manifest and URL rules), `newAppInstallerDownload.test.mjs` (dialog state), and a jsdom test of the download hook. - Rust: `new_app_installer_tests.rs` covers URL validation, the redirect policy, limits, naming without overwrite, cancel and shutdown cleanup, launch cleanup, and the quarantine and Mark of the Web values. CI runs this crate's tests on Linux and on `windows-latest`, so the Mark of the Web test writes a real alternate data stream there. The quarantine test runs on macOS only, which CI builds but does not test. - Smoke: `tests/e2e/deprecation-banner.spec.ts` drives the banner, the download dialog, each failure's actions, cancel and the keyboard paths against the mock bridge. - Native, macOS arm64 debug build: the download was saved with `com.apple.quarantine` set and revealed in Finder; cancel removed the partial; the download kept running while the window was hidden; SIGTERM and a menu quit mid-download removed the partial; after a force kill the next launch removed it. Not verified by hand: - The Windows and Linux flows in a running app, including SmartScreen on Windows. - The Gatekeeper prompt when opening the downloaded DMG, and the macOS Downloads-folder consent prompt, which need a signed build. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Kalvin Chau <kalvin@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Matt Toohey <contact@matttoohey.com>
168 lines
8.8 KiB
TOML
168 lines
8.8 KiB
TOML
[workspace]
|
|
# Explicit: membership must NOT be inferred from the path-dependency edge below.
|
|
# With a bare `[workspace]` and no `members`, `cargo test/check --workspace`
|
|
# expands to a set that excludes this crate, and its gates pass green-and-empty
|
|
# over a real defect. Verified: Sami Arm A/D, Dawn `.scratch/armA`.
|
|
members = ["crates/buzz-terminal"]
|
|
|
|
[package]
|
|
name = "buzz-desktop"
|
|
version = "0.5.28"
|
|
description = "Buzz desktop app"
|
|
authors = ["you"]
|
|
edition = "2021"
|
|
|
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
|
|
[lib]
|
|
# The `_lib` suffix may seem redundant but it is necessary
|
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
|
name = "buzz_lib"
|
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
|
|
|
[features]
|
|
default = ["system-keyring"]
|
|
mesh-llm = ["dep:iroh", "dep:mesh-llm-sdk", "dep:mesh-llm-host-runtime", "dep:mesh-llm-client", "dep:mesh-llm-node", "dep:mesh-llm-system", "dep:mesh-llm-events"]
|
|
# OS keyring backing for desktop secret storage (nsec private keys). When
|
|
# disabled, secrets fall back to 0o600 files. On by default for real builds.
|
|
system-keyring = ["dep:keyring"]
|
|
|
|
[build-dependencies]
|
|
base64 = "0.22"
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
tauri-build = { version = "2", features = [] }
|
|
|
|
[target.'cfg(unix)'.dependencies]
|
|
libc = "0.2"
|
|
ctrlc = { version = "3", features = ["termination"] }
|
|
|
|
[target.'cfg(target_os = "linux")'.dependencies]
|
|
# `crypto-rust` is what selects `EncryptionType::Dh` over `Plain` for the
|
|
# Secret Service session (keyring-3.6.3/src/secret_service.rs:140-143,333-336),
|
|
# i.e. Diffie-Hellman transit encryption of secrets over the D-Bus session bus.
|
|
# Until MeshLLM 0.76 it arrived implicitly via feature unification with
|
|
# mesh-llm's own keyring 3; mesh-llm now uses keyring 4, so request it here
|
|
# explicitly rather than inheriting it by accident.
|
|
keyring = { version = "3.6.3", default-features = false, features = ["sync-secret-service", "crypto-rust", "vendored"], optional = true }
|
|
# Used directly (alongside tauri-plugin-notification) so we can hold the posting
|
|
# D-Bus connection open. GNOME 46+ dismisses a notification the moment that
|
|
# connection is dropped, which the plugin does immediately. Default features
|
|
# keep the pure-Rust zbus backend, matching the plugin (no libdbus needed).
|
|
notify-rust = "4"
|
|
# Enable getUserMedia in the WebKitGTK webview (see src/linux_media.rs). Pinned
|
|
# to the exact version wry links so both resolve to one webkit2gtk-sys and we
|
|
# don't get duplicate symbols; bump in lockstep with wry.
|
|
webkit2gtk = { version = "=2.0.2", features = ["v2_22"] }
|
|
|
|
[target.'cfg(target_os = "macos")'.dependencies]
|
|
block2 = { version = "0.6", default-features = false, features = ["std"] }
|
|
objc2 = { version = "0.6.4", default-features = false }
|
|
objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSEvent", "NSHapticFeedback", "NSMenu", "NSMenuItem", "NSStatusItem", "block2"] }
|
|
objc2-foundation = { version = "0.3.2", default-features = false, features = ["NSDictionary", "NSError", "NSBundle", "NSObject", "NSProcessInfo", "NSString"] }
|
|
objc2-user-notifications = { version = "0.3.2", default-features = false, features = ["block2", "UNNotification", "UNNotificationContent", "UNNotificationRequest", "UNNotificationResponse", "UNNotificationSettings", "UNNotificationTrigger", "UNUserNotificationCenter"] }
|
|
keyring = { version = "3.6.3", default-features = false, features = ["apple-native", "vendored"], optional = true }
|
|
security-framework = { version = "3.7.0", features = ["OSX_10_15"] }
|
|
window-vibrancy = "0.6"
|
|
user-idle = { version = "0.6", default-features = false }
|
|
plist = "1"
|
|
# Sets the quarantine attribute on the new-app installer the app downloads.
|
|
xattr = "1"
|
|
|
|
[target.'cfg(windows)'.dependencies]
|
|
windows-sys = { version = "0.61", features = ["Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Diagnostics_ToolHelp", "Win32_System_JobObjects", "Win32_System_Registry", "Win32_System_Threading", "Win32_Foundation"] }
|
|
keyring = { version = "3.6.3", default-features = false, features = ["windows-native", "vendored"], optional = true }
|
|
user-idle = { version = "0.6", default-features = false }
|
|
|
|
[dependencies]
|
|
atomic-write-file = "0.3"
|
|
anyhow = "1"
|
|
dirs = "6"
|
|
tauri = { version = "2", features = ["macos-private-api", "tray-icon"] }
|
|
tauri-plugin-deep-link = "2"
|
|
tauri-plugin-opener = "2"
|
|
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
|
tauri-plugin-window-state = "2"
|
|
tauri-plugin-dialog = "2"
|
|
tauri-plugin-updater = "2"
|
|
tauri-plugin-process = "2"
|
|
infer = "0.19"
|
|
hex = "0.4"
|
|
ed25519-dalek = "=3.0.0"
|
|
tokio = { version = "1", features = ["fs", "sync", "rt", "macros", "time", "net", "io-util"] }
|
|
tokio-tungstenite = { version = "0.29", features = ["rustls-tls-webpki-roots"] }
|
|
tokio-util = { version = "0.7", features = ["rt"] }
|
|
bytes = "1"
|
|
futures-util = "0.3"
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
serde_yaml = "0.9"
|
|
toml = "0.8"
|
|
nostr = { version = "0.44", features = ["nip44", "nip49"] }
|
|
# OS-entropy source for backup passphrase generation (already in the tree as a
|
|
# transitive dependency; pinned here for direct use).
|
|
getrandom = "0.2"
|
|
zeroize = "1"
|
|
percent-encoding = "2"
|
|
reqwest = { version = "0.13", features = ["json", "query", "stream", "blocking"] }
|
|
rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs", "std"] }
|
|
url = "2"
|
|
buzz_core_pkg = { package = "buzz-core", path = "../../crates/buzz-core" }
|
|
buzz_persona_pkg = { package = "buzz-persona", path = "../../crates/buzz-persona" }
|
|
buzz_sdk_pkg = { package = "buzz-sdk", path = "../../crates/buzz-sdk" }
|
|
buzz_agent_pkg = { package = "buzz-agent", path = "../../crates/buzz-agent" }
|
|
buzz_audio_client = { package = "buzz-audio-client", path = "../../crates/buzz-audio-client", default-features = false, features = ["webpki-roots"] }
|
|
buzz_voice_pkg = { package = "buzz-voice", path = "../../crates/buzz-voice" }
|
|
buzz_terminal = { package = "buzz-terminal", path = "crates/buzz-terminal" }
|
|
buzz_ws_client_pkg = { package = "buzz-ws-client", path = "../../crates/buzz-ws-client" }
|
|
portable-pty = "0.9"
|
|
iroh = { version = "1.0.3", optional = true }
|
|
mesh-llm-sdk = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.2", package = "mesh-llm-sdk", default-features = false, features = ["client", "serving"], optional = true }
|
|
mesh-llm-host-runtime = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.2", package = "mesh-llm-host-runtime", default-features = false, features = ["dynamic-native-runtime"], optional = true }
|
|
# Model catalog + hardware survey for the Share-compute model picker (same
|
|
# diagnose pattern as mesh-console). Lib name of mesh-llm-client is mesh_client.
|
|
mesh-llm-client = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.2", package = "mesh-llm-client", optional = true }
|
|
mesh-llm-node = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.2", package = "mesh-llm-node", optional = true }
|
|
mesh-llm-system = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.2", package = "mesh-llm-system", optional = true }
|
|
mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", tag = "v0.76.2", package = "mesh-llm-events", optional = true }
|
|
base64 = "0.22"
|
|
sha2 = "0.11"
|
|
tar = "0.4"
|
|
bzip2 = "0.6"
|
|
chrono = { version = "0.4", features = ["serde"] }
|
|
tauri-plugin-global-shortcut = "2"
|
|
tauri-plugin-notification = "2.3.3"
|
|
uuid = { version = "1", features = ["v4", "v5"] }
|
|
png = "0.18"
|
|
# wayland-data-control: without it arboard is X11-only on Linux, so copies made
|
|
# in a Wayland session land in XWayland's clipboard where Wayland-native apps
|
|
# never see them (set_text still returns Ok). The backing wl-clipboard-rs dep is
|
|
# target-gated inside arboard; at runtime X11 sessions still fall back to X11.
|
|
arboard = { version = "3", features = ["wayland-data-control"] }
|
|
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp", "gif"] }
|
|
zip = "9"
|
|
flate2 = "1"
|
|
sherpa-onnx = "1.12"
|
|
regex = "1"
|
|
# Match MeshLLM's SQLite ABI; keep u64 conversions available without mesh-llm.
|
|
rusqlite = { version = "0.40", features = ["bundled", "fallible_uint"] }
|
|
axum = "0.8"
|
|
rodio = "0.22"
|
|
earshot = "1.0"
|
|
rubato = "3.0"
|
|
audioadapter-buffers = "3.0"
|
|
tempfile = "3"
|
|
strip-ansi-escapes = "0.2"
|
|
tracing = "0.1"
|
|
|
|
[dev-dependencies]
|
|
tauri = { version = "2", features = ["test"] }
|
|
tauri-utils = "2"
|
|
# `test-util` enables tokio's paused-clock (`start_paused`) so the relay
|
|
# admission gate tests can assert exact wait durations without real sleeps.
|
|
tokio = { version = "1", features = ["test-util"] }
|
|
# The relay's media validation, so desktop-produced snapshots and voice notes
|
|
# can prove their full client-sanitize → relay-accept contract end to end.
|
|
buzz_media_pkg = { package = "buzz-media", path = "../../crates/buzz-media" }
|