Skip to content

Commit b766b76

Browse files
authored
fix(tauri): add native About and Help menus (#596)
## Summary - replace the inert Tauri macOS About handler with the native About dialog - add a conventional Help menu with About on Windows and Linux - expose Get Updates through each platform menu and through native About only where the platform supports an actionable link - run the native WinGet updater from the Windows Help menu, with GitHub Releases as the error fallback ## Platform behavior - macOS: CodeNomad > About CodeNomad opens the native dialog; CodeNomad > Get Updates... opens the latest GitHub release - Windows: Help > About CodeNomad opens the native dialog; Help > Get Updates... runs the WinGet updater asynchronously and opens the latest GitHub release only if WinGet cannot start, fails, or cannot be monitored - Linux: Help > About CodeNomad opens the native dialog with an actionable Get updates link; Help > Get Updates... also provides direct access - the displayed application version comes from Tauri package metadata Tauri ignores website metadata in the native macOS About panel and renders it as inert text on Windows, so website metadata is included only on Linux. ## Windows updater integration PR #597 is now merged. The Windows menu action invokes its install_stable_update command without blocking the Tauri menu event loop. macOS and Linux retain the direct Releases URL behavior. ## Validation - cargo test --manifest-path packages/tauri-app/src-tauri/Cargo.toml: 65 passed - cargo check --manifest-path packages/tauri-app/src-tauri/Cargo.toml: passed - focused failed-update fallback regression test - git diff --check: passed Closes #592
1 parent 0bab9e3 commit b766b76

1 file changed

Lines changed: 109 additions & 6 deletions

File tree

  • packages/tauri-app/src-tauri/src

packages/tauri-app/src-tauri/src/main.rs

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ use keepawake::KeepAwake;
1919
use serde::Deserialize;
2020
use serde_json::json;
2121
use std::collections::{HashMap, HashSet};
22+
#[cfg(any(windows, test))]
23+
use std::future::Future;
2224
use std::sync::Mutex;
2325
use std::time::{SystemTime, UNIX_EPOCH};
24-
use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder};
26+
use tauri::menu::{AboutMetadata, MenuBuilder, MenuItem, PredefinedMenuItem, SubmenuBuilder};
2527
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
2628
use tauri::webview::{PageLoadEvent, Webview};
2729
use tauri::{
@@ -43,6 +45,7 @@ use std::os::windows::ffi::OsStrExt;
4345
use windows_sys::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
4446

4547
const ZOOM_STEP: f64 = 0.1;
48+
const RELEASES_URL: &str = "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest";
4649
const LOCAL_WINDOW_CONTEXT_SCRIPT: &str = "window.__CODENOMAD_WINDOW_CONTEXT__ = 'local';";
4750
const REMOTE_WINDOW_CONTEXT_SCRIPT: &str = "window.__CODENOMAD_WINDOW_CONTEXT__ = 'remote';";
4851

@@ -736,11 +739,20 @@ fn main() {
736739
}
737740
}
738741

739-
// App menu (macOS)
740-
"about" => {
741-
// TODO: Implement about dialog
742-
println!("About menu item clicked");
742+
"get_updates" => {
743+
#[cfg(windows)]
744+
{
745+
let app_handle = app_handle.clone();
746+
tauri::async_runtime::spawn(run_update_with_fallback(
747+
windows_update::install_stable_update(),
748+
move || open_releases_page(&app_handle),
749+
));
750+
}
751+
752+
#[cfg(not(windows))]
753+
open_releases_page(app_handle);
743754
}
755+
// App menu (macOS)
744756
"hide" => {
745757
if let Some(window) = app_handle.get_webview_window("main") {
746758
let _ = window.hide();
@@ -841,11 +853,27 @@ fn build_menu(app: &AppHandle) -> tauri::Result<()> {
841853

842854
// Create submenus
843855
let mut submenus = Vec::new();
856+
let about_item = PredefinedMenuItem::about(
857+
app,
858+
Some("About CodeNomad"),
859+
Some(build_about_metadata(
860+
&app.package_info().version.to_string(),
861+
cfg!(target_os = "linux"),
862+
)),
863+
)?;
864+
let get_updates_item = MenuItem::with_id(
865+
app,
866+
"get_updates",
867+
"Get Updates...",
868+
true,
869+
None::<&str>,
870+
)?;
844871

845872
// App menu (macOS only)
846873
if is_mac {
847874
let app_menu = SubmenuBuilder::new(app, "CodeNomad")
848-
.text("about", "About CodeNomad")
875+
.item(&about_item)
876+
.item(&get_updates_item)
849877
.separator()
850878
.text("hide", "Hide CodeNomad")
851879
.text("hide_others", "Hide Others")
@@ -983,6 +1011,15 @@ fn build_menu(app: &AppHandle) -> tauri::Result<()> {
9831011
};
9841012
submenus.push(window_menu);
9851013

1014+
if !is_mac {
1015+
let help_menu = SubmenuBuilder::new(app, "Help")
1016+
.item(&get_updates_item)
1017+
.separator()
1018+
.item(&about_item)
1019+
.build()?;
1020+
submenus.push(help_menu);
1021+
}
1022+
9861023
// Build the main menu with all submenus
9871024
let submenu_refs: Vec<&dyn tauri::menu::IsMenuItem<_>> = submenus
9881025
.iter()
@@ -993,3 +1030,69 @@ fn build_menu(app: &AppHandle) -> tauri::Result<()> {
9931030
app.set_menu(menu)?;
9941031
Ok(())
9951032
}
1033+
1034+
#[cfg(any(windows, test))]
1035+
async fn run_update_with_fallback(
1036+
update: impl Future<Output = Result<(), String>>,
1037+
fallback: impl FnOnce(),
1038+
) {
1039+
if let Err(err) = update.await {
1040+
eprintln!("[tauri] WinGet update failed, opening the releases page: {err}");
1041+
fallback();
1042+
}
1043+
}
1044+
1045+
fn open_releases_page(app_handle: &AppHandle) {
1046+
if let Err(err) = app_handle.opener().open_url(RELEASES_URL, None::<&str>) {
1047+
eprintln!("[tauri] failed to open the CodeNomad releases page: {err}");
1048+
}
1049+
}
1050+
1051+
fn build_about_metadata(version: &str, include_update_link: bool) -> AboutMetadata<'static> {
1052+
AboutMetadata {
1053+
name: Some("CodeNomad".to_string()),
1054+
version: Some(version.to_string()),
1055+
authors: Some(vec!["Neural Nomads AI".to_string()]),
1056+
comments: Some("A desktop workspace for OpenCode.".to_string()),
1057+
license: Some("MIT".to_string()),
1058+
website: include_update_link.then(|| RELEASES_URL.to_string()),
1059+
website_label: include_update_link.then(|| "Get updates".to_string()),
1060+
..Default::default()
1061+
}
1062+
}
1063+
1064+
#[cfg(test)]
1065+
mod menu_tests {
1066+
use super::{build_about_metadata, run_update_with_fallback, RELEASES_URL};
1067+
use std::sync::atomic::{AtomicBool, Ordering};
1068+
1069+
#[test]
1070+
fn failed_update_uses_release_fallback() {
1071+
let fallback_called = AtomicBool::new(false);
1072+
1073+
tauri::async_runtime::block_on(run_update_with_fallback(
1074+
async { Err("update failed".to_string()) },
1075+
|| fallback_called.store(true, Ordering::Relaxed),
1076+
));
1077+
1078+
assert!(fallback_called.load(Ordering::Relaxed));
1079+
}
1080+
1081+
#[test]
1082+
fn about_metadata_includes_version_and_supported_update_link() {
1083+
let metadata = build_about_metadata("1.2.3", true);
1084+
1085+
assert_eq!(metadata.name.as_deref(), Some("CodeNomad"));
1086+
assert_eq!(metadata.version.as_deref(), Some("1.2.3"));
1087+
assert_eq!(metadata.website.as_deref(), Some(RELEASES_URL));
1088+
assert_eq!(metadata.website_label.as_deref(), Some("Get updates"));
1089+
}
1090+
1091+
#[test]
1092+
fn about_metadata_omits_unsupported_update_link() {
1093+
let metadata = build_about_metadata("1.2.3", false);
1094+
1095+
assert_eq!(metadata.website, None);
1096+
assert_eq!(metadata.website_label, None);
1097+
}
1098+
}

0 commit comments

Comments
 (0)