diff --git a/CHANGELOG.md b/CHANGELOG.md index e737db3..fbab451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## Desktop [0.1.4] — 2026-04-18 + +### Fixed + +- Tray icon left-click is no longer a dead no-op on macOS: the tray menu now pops on left-click and a fallback handler shows the main window if the menu fails to open +- Tray icon renders as a real monochrome silhouette in the macOS menu bar instead of a black square — the previous full-color carrier-pigeon `icon.png` was being declared as a template image, which macOS rendered as a solid blob +- First launch now shows the main window so the onboarding wizard is reachable; only returning users with `auto_start` enabled and a saved worker secret get the silent tray-only launch + +### Added + +- New `crates/modelrelay-desktop/icons/tray-icon.png` and `tray-icon@2x.png` — monochrome black-on-alpha derivatives of the carrier-pigeon mark, sized for the macOS menu bar (22×22 / 44×44) + ## Desktop [0.1.3] — 2026-04-18 ### Changed diff --git a/Cargo.lock b/Cargo.lock index 324e333..e83061b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2573,7 +2573,7 @@ dependencies = [ [[package]] name = "modelrelay-desktop" -version = "0.1.3" +version = "0.1.4" dependencies = [ "modelrelay-protocol", "modelrelay-worker", diff --git a/crates/modelrelay-desktop/Cargo.toml b/crates/modelrelay-desktop/Cargo.toml index 1b6d90e..be63576 100644 --- a/crates/modelrelay-desktop/Cargo.toml +++ b/crates/modelrelay-desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "modelrelay-desktop" -version = "0.1.3" +version = "0.1.4" description = "Native desktop tray app for ModelRelay workers" edition.workspace = true license.workspace = true diff --git a/crates/modelrelay-desktop/icons/tray-icon.png b/crates/modelrelay-desktop/icons/tray-icon.png new file mode 100644 index 0000000..dd22a78 Binary files /dev/null and b/crates/modelrelay-desktop/icons/tray-icon.png differ diff --git a/crates/modelrelay-desktop/icons/tray-icon@2x.png b/crates/modelrelay-desktop/icons/tray-icon@2x.png new file mode 100644 index 0000000..b5c8499 Binary files /dev/null and b/crates/modelrelay-desktop/icons/tray-icon@2x.png differ diff --git a/crates/modelrelay-desktop/src/main.rs b/crates/modelrelay-desktop/src/main.rs index 976a933..e1c2a31 100644 --- a/crates/modelrelay-desktop/src/main.rs +++ b/crates/modelrelay-desktop/src/main.rs @@ -4,7 +4,7 @@ use modelrelay_desktop::{AppSettings, AppStatus, WorkerManager, updater}; use tauri::{ Emitter, Manager, menu::{MenuBuilder, MenuItemBuilder}, - tray::TrayIconBuilder, + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, }; #[tauri::command] @@ -48,6 +48,76 @@ async fn stop_worker(manager: tauri::State<'_, WorkerManager>) -> Result<(), Str Ok(()) } +fn build_tray(app: &tauri::App) -> tauri::Result<()> { + let show = MenuItemBuilder::with_id("show", "Open Dashboard").build(app)?; + let settings = MenuItemBuilder::with_id("settings", "Settings").build(app)?; + let check_updates = + MenuItemBuilder::with_id("check_updates", "Check for Updates\u{2026}").build(app)?; + let quit = MenuItemBuilder::with_id("quit", "Quit").build(app)?; + + let menu = MenuBuilder::new(app) + .item(&show) + .item(&settings) + .separator() + .item(&check_updates) + .separator() + .item(&quit) + .build()?; + + TrayIconBuilder::new() + .tooltip("ModelRelay - Disconnected") + .menu(&menu) + .show_menu_on_left_click(true) + .on_tray_icon_event(|tray, event| { + // Belt-and-suspenders fallback: even if the platform fails to pop + // the menu on left-click (seen on macOS in v0.1.3), reveal the main + // window so the tray is never a dead UI element. + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + let app = tray.app_handle(); + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + }) + .on_menu_event(|app, event| match event.id().as_ref() { + "show" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + let _ = window.emit("navigate-tab", "dashboard"); + } + } + "settings" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + let _ = window.emit("navigate-tab", "settings"); + } + } + "check_updates" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + let _ = window.emit("navigate-tab", "settings"); + } + let _ = app.emit("updater-manual-check", ()); + } + "quit" => { + app.exit(0); + } + _ => {} + }) + .build(app)?; + + Ok(()) +} + #[tauri::command] async fn check_for_update(app: tauri::AppHandle) -> Result { updater::fetch_update_summary(&app).await @@ -92,7 +162,10 @@ fn main() { let manager = WorkerManager::new(settings_path); - // If auto_start is enabled, start the worker immediately + // If auto_start is enabled and the user has a saved worker secret, start + // the worker immediately and stay silent in the tray. Otherwise show the + // main window so first-run users (and returning users without auto_start) + // see the onboarding/dashboard UI even if the tray click is misbehaving. let rt = app.handle().clone(); let auto_start = { let settings_file = app_data_dir.join("settings.json"); @@ -112,56 +185,12 @@ fn main() { tracing::error!(error = %e, "auto-start failed"); } }); + } else if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); } - let show = MenuItemBuilder::with_id("show", "Open Dashboard").build(app)?; - let settings = MenuItemBuilder::with_id("settings", "Settings").build(app)?; - let check_updates = - MenuItemBuilder::with_id("check_updates", "Check for Updates\u{2026}") - .build(app)?; - let quit = MenuItemBuilder::with_id("quit", "Quit").build(app)?; - - let menu = MenuBuilder::new(app) - .item(&show) - .item(&settings) - .separator() - .item(&check_updates) - .separator() - .item(&quit) - .build()?; - - let _tray = TrayIconBuilder::new() - .tooltip("ModelRelay - Disconnected") - .menu(&menu) - .on_menu_event(|app, event| match event.id().as_ref() { - "show" => { - if let Some(window) = app.get_webview_window("main") { - let _ = window.show(); - let _ = window.set_focus(); - let _ = window.emit("navigate-tab", "dashboard"); - } - } - "settings" => { - if let Some(window) = app.get_webview_window("main") { - let _ = window.show(); - let _ = window.set_focus(); - let _ = window.emit("navigate-tab", "settings"); - } - } - "check_updates" => { - if let Some(window) = app.get_webview_window("main") { - let _ = window.show(); - let _ = window.set_focus(); - let _ = window.emit("navigate-tab", "settings"); - } - let _ = app.emit("updater-manual-check", ()); - } - "quit" => { - app.exit(0); - } - _ => {} - }) - .build(app)?; + build_tray(app)?; // Kick off a silent update check shortly after launch. Errors are // logged and never block startup. diff --git a/crates/modelrelay-desktop/tauri.conf.json b/crates/modelrelay-desktop/tauri.conf.json index 150cd64..8ee855b 100644 --- a/crates/modelrelay-desktop/tauri.conf.json +++ b/crates/modelrelay-desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nickel-org/nickel.rs/master/docs/tauri.conf.json", "productName": "ModelRelay", - "version": "0.1.3", + "version": "0.1.4", "identifier": "com.modelrelay.desktop", "build": { "frontendDist": "./ui" @@ -18,7 +18,7 @@ } ], "trayIcon": { - "iconPath": "icons/icon.png", + "iconPath": "icons/tray-icon.png", "iconAsTemplate": true } },