From 461844dd25f924d5292dc3e13f7018e020f509bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Tue, 14 Jul 2026 21:51:16 +0200 Subject: [PATCH 1/3] feat(tauri): install stable Windows updates with WinGet Replace the official release notification link with a native WinGet upgrade on local Tauri Windows sessions. Other hosts and platforms continue opening the existing release URL. Run WinGet silently without a console window and let the NSIS installer close the application. Validated with Cargo check, the UI TypeScript check, and a live upgrade from CodeNomad 0.17.0 to 0.18.0. --- packages/tauri-app/src-tauri/src/main.rs | 4 ++- .../tauri-app/src-tauri/src/windows_update.rs | 29 +++++++++++++++++++ packages/ui/src/lib/notifications.tsx | 3 +- packages/ui/src/stores/releases.ts | 3 ++ 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 packages/tauri-app/src-tauri/src/windows_update.rs diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs index 6aa69d259..123701a84 100644 --- a/packages/tauri-app/src-tauri/src/main.rs +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -9,6 +9,7 @@ mod desktop_event_transport; mod linux_tls; mod managed_node; mod shutdown; +mod windows_update; use cli_manager::{CliProcessManager, CliStatus}; use desktop_event_transport::{ @@ -656,7 +657,8 @@ fn main() { client_state::client_state_set_restore_enabled, client_state::client_state_clear, client_state::client_state_renderer_flushed, - client_state::client_state_navigation_flushed + client_state::client_state_navigation_flushed, + windows_update::install_stable_update ]) .on_menu_event(|app_handle, event| { match event.id().0.as_str() { diff --git a/packages/tauri-app/src-tauri/src/windows_update.rs b/packages/tauri-app/src-tauri/src/windows_update.rs new file mode 100644 index 000000000..82a0c9a9e --- /dev/null +++ b/packages/tauri-app/src-tauri/src/windows_update.rs @@ -0,0 +1,29 @@ +use std::process::Command; + +#[cfg(windows)] +use std::os::windows::process::CommandExt; + +#[tauri::command] +pub fn install_stable_update() -> Result<(), String> { + let mut command = Command::new("winget"); + command.args([ + "upgrade", + "--exact", + "--id", + "NeuralNomadsAI.CodeNomad", + "--source", + "winget", + "--silent", + "--accept-source-agreements", + "--accept-package-agreements", + "--disable-interactivity", + ]); + + #[cfg(windows)] + command.creation_flags(0x08000000); + + command + .spawn() + .map(|_| ()) + .map_err(|err| format!("Unable to start the WinGet update: {err}")) +} diff --git a/packages/ui/src/lib/notifications.tsx b/packages/ui/src/lib/notifications.tsx index 4102dd8e9..8d4270b1d 100644 --- a/packages/ui/src/lib/notifications.tsx +++ b/packages/ui/src/lib/notifications.tsx @@ -20,6 +20,7 @@ export type ToastPayload = { action?: { label: string href: string + onClick?: () => void | Promise } } @@ -381,7 +382,7 @@ export function showToastNotification(payload: ToastPayload): ToastHandle { diff --git a/packages/ui/src/stores/releases.ts b/packages/ui/src/stores/releases.ts index f1f6c4e72..38dcd94b5 100644 --- a/packages/ui/src/stores/releases.ts +++ b/packages/ui/src/stores/releases.ts @@ -1,9 +1,11 @@ import { createEffect, createSignal } from "solid-js" +import { invoke } from "@tauri-apps/api/core" import type { ServerMeta, SupportMeta } from "../../../server/src/api-types" import { getServerMeta } from "../lib/server-meta" import { showToastNotification, ToastHandle } from "../lib/notifications" import { getLogger } from "../lib/logger" import { tGlobal } from "../lib/i18n" +import { isLocalWindow, isTauriHost } from "../lib/runtime-env" import { hasInstances, showFolderSelection } from "./ui" const log = getLogger("actions") @@ -61,6 +63,7 @@ function ensureVisibilityEffect() { ? { label: tGlobal("releases.upgradeRequired.action.getUpdate"), href: support.latestServerUrl, + onClick: isTauriHost() && isLocalWindow() && navigator.userAgent.includes("Windows") ? () => invoke("install_stable_update") : undefined, } : undefined, }) From 0b635a1316759ef606fcc8cd467dcd7251acfdd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Tue, 14 Jul 2026 22:12:31 +0200 Subject: [PATCH 2/3] fix(tauri): fall back when WinGet updates fail Wait for WinGet while CodeNomad remains active so launch and non-zero exit failures propagate back to the UI without blocking the async runtime. Use one notification action path for live toasts and history entries, opening the release URL whenever the native update action rejects. Successful installs remain owned by WinGet and do not attempt an in-process restart. Validated with the UI typecheck and build plus the Rust test suite. --- .../tauri-app/src-tauri/src/windows_update.rs | 49 +++++++++++-------- .../ui/src/components/toast-history-panel.tsx | 25 ++-------- packages/ui/src/lib/notifications.tsx | 33 +++++++++---- 3 files changed, 57 insertions(+), 50 deletions(-) diff --git a/packages/tauri-app/src-tauri/src/windows_update.rs b/packages/tauri-app/src-tauri/src/windows_update.rs index 82a0c9a9e..7d4ce2cb8 100644 --- a/packages/tauri-app/src-tauri/src/windows_update.rs +++ b/packages/tauri-app/src-tauri/src/windows_update.rs @@ -4,26 +4,35 @@ use std::process::Command; use std::os::windows::process::CommandExt; #[tauri::command] -pub fn install_stable_update() -> Result<(), String> { - let mut command = Command::new("winget"); - command.args([ - "upgrade", - "--exact", - "--id", - "NeuralNomadsAI.CodeNomad", - "--source", - "winget", - "--silent", - "--accept-source-agreements", - "--accept-package-agreements", - "--disable-interactivity", - ]); +pub async fn install_stable_update() -> Result<(), String> { + tauri::async_runtime::spawn_blocking(|| { + let mut command = Command::new("winget"); + command.args([ + "upgrade", + "--exact", + "--id", + "NeuralNomadsAI.CodeNomad", + "--source", + "winget", + "--silent", + "--accept-source-agreements", + "--accept-package-agreements", + "--disable-interactivity", + ]); - #[cfg(windows)] - command.creation_flags(0x08000000); + #[cfg(windows)] + command.creation_flags(0x08000000); - command - .spawn() - .map(|_| ()) - .map_err(|err| format!("Unable to start the WinGet update: {err}")) + let status = command + .status() + .map_err(|err| format!("Unable to start the WinGet update: {err}"))?; + + if status.success() { + Ok(()) + } else { + Err(format!("WinGet update failed with status {status}")) + } + }) + .await + .map_err(|err| format!("Unable to monitor the WinGet update: {err}"))? } diff --git a/packages/ui/src/components/toast-history-panel.tsx b/packages/ui/src/components/toast-history-panel.tsx index 7e7d91150..4cbc231a9 100644 --- a/packages/ui/src/components/toast-history-panel.tsx +++ b/packages/ui/src/components/toast-history-panel.tsx @@ -21,9 +21,9 @@ import { deleteToastHistoryItem, markAllToastHistoryAsRead, markToastHistoryAsRead, + runToastAction, subscribeToastHistory, } from "../lib/notifications" -import { isTauriHost } from "../lib/runtime-env" // ==================== Types ==================== @@ -188,8 +188,8 @@ const ToastHistoryPanel: Component = (props) => { } // Open action link if exists - if (item.action?.href) { - void handleOpenAction(item.action.href); + if (item.action) { + void runToastAction(item.action); } }; @@ -209,21 +209,6 @@ const ToastHistoryPanel: Component = (props) => { markAllToastHistoryAsRead(); }; - // Open external link - async function handleOpenAction(href: string): Promise { - if (isTauriHost()) { - try { - const { openUrl } = await import("@tauri-apps/plugin-opener"); - await openUrl(href); - return; - } catch (error) { - console.warn("[toast-history] unable to open via system opener", error); - } - } - - window.open(href, "_blank", "noopener,noreferrer"); - } - // Stop click propagation from backdrop to panel const handleBackdropClick = (event: MouseEvent) => { if (event.target === event.currentTarget) { @@ -383,8 +368,8 @@ const ToastHistoryPanel: Component = (props) => { class="toast-history-item-action inline-flex items-center gap-1 text-xs mt-1" onClick={(e) => { e.stopPropagation(); - if (item.action?.href) { - void handleOpenAction(item.action.href); + if (item.action) { + void runToastAction(item.action); } }} > diff --git a/packages/ui/src/lib/notifications.tsx b/packages/ui/src/lib/notifications.tsx index 8d4270b1d..45ab07a9b 100644 --- a/packages/ui/src/lib/notifications.tsx +++ b/packages/ui/src/lib/notifications.tsx @@ -11,17 +11,19 @@ export type ToastHandle = { type ToastPosition = "top-left" | "top-right" | "top-center" | "bottom-left" | "bottom-right" | "bottom-center" +export type ToastAction = { + label: string + href: string + onClick?: () => void | Promise +} + export type ToastPayload = { title?: string message: string variant: ToastVariant duration?: number position?: ToastPosition - action?: { - label: string - href: string - onClick?: () => void | Promise - } + action?: ToastAction } // ==================== Toast History Types ==================== @@ -43,10 +45,7 @@ export interface IToastHistoryItem { /** Read state (clicked) */ read: boolean; /** Action link (optional) */ - action?: { - label: string; - href: string; - }; + action?: ToastAction; } /** @@ -298,6 +297,20 @@ async function openExternalUrl(url: string): Promise { } } +export async function runToastAction(action: ToastAction): Promise { + if (!action.onClick) { + await openExternalUrl(action.href) + return + } + + try { + await action.onClick() + } catch (error) { + console.warn("[notifications] action failed; opening fallback link", error) + await openExternalUrl(action.href) + } +} + // ==================== Variant Accent Styles ==================== const variantAccent: Record< @@ -382,7 +395,7 @@ export function showToastNotification(payload: ToastPayload): ToastHandle { From 6a493a51a82c27bffd786f0e954cdc84dd6d1a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Wed, 15 Jul 2026 22:44:46 +0200 Subject: [PATCH 3/3] fix(ui): require explicit notification actions Keep history-row activation limited to marking the notification as read. Actionable operations such as WinGet updates now run only from the dedicated action button, preventing accidental installs when users select a notification. Validated with the UI TypeScript check and production build. --- packages/ui/src/components/toast-history-panel.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/ui/src/components/toast-history-panel.tsx b/packages/ui/src/components/toast-history-panel.tsx index 4cbc231a9..381fe3c1a 100644 --- a/packages/ui/src/components/toast-history-panel.tsx +++ b/packages/ui/src/components/toast-history-panel.tsx @@ -182,15 +182,9 @@ const ToastHistoryPanel: Component = (props) => { // Handle item click const handleItemClick = (item: IToastHistoryItem) => { - // Mark as read if (!item.read) { markToastHistoryAsRead(item.id); } - - // Open action link if exists - if (item.action) { - void runToastAction(item.action); - } }; // Handle delete