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..7d4ce2cb8 --- /dev/null +++ b/packages/tauri-app/src-tauri/src/windows_update.rs @@ -0,0 +1,38 @@ +use std::process::Command; + +#[cfg(windows)] +use std::os::windows::process::CommandExt; + +#[tauri::command] +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); + + 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..381fe3c1a 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 ==================== @@ -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?.href) { - void handleOpenAction(item.action.href); - } }; // Handle delete @@ -209,21 +203,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 +362,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 4102dd8e9..45ab07a9b 100644 --- a/packages/ui/src/lib/notifications.tsx +++ b/packages/ui/src/lib/notifications.tsx @@ -11,16 +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 - } + action?: ToastAction } // ==================== Toast History Types ==================== @@ -42,10 +45,7 @@ export interface IToastHistoryItem { /** Read state (clicked) */ read: boolean; /** Action link (optional) */ - action?: { - label: string; - href: string; - }; + action?: ToastAction; } /** @@ -297,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< @@ -381,7 +395,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, })