Skip to content

Commit e586ea3

Browse files
authored
feat(windows): update CodeNomad from the in-app notification (#597)
## Summary - launch the official CodeNomad WinGet upgrade from the upgrade-required notification in the local Windows Tauri client - keep non-Windows, Electron and development release actions on their existing release links - wait for WinGet without blocking the async runtime and propagate launch or non-zero exit failures back to the UI - open the official release URL when the native update action fails - preserve the same explicit update action in the live toast and notification history ## Update behavior - run an exact silent upgrade for the NeuralNomadsAI.CodeNomad package from the WinGet source - do not force a reinstall or downgrade - do not attempt to restart CodeNomad after a successful update; the installer owns application shutdown and replacement ## Validation - npm run typecheck in packages/ui - npm run build in packages/ui - cargo test in packages/tauri-app/src-tauri (20 tests)
1 parent 7584ccf commit e586ea3

5 files changed

Lines changed: 70 additions & 34 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod desktop_event_transport;
99
mod linux_tls;
1010
mod managed_node;
1111
mod shutdown;
12+
mod windows_update;
1213

1314
use cli_manager::{CliProcessManager, CliStatus};
1415
use desktop_event_transport::{
@@ -656,7 +657,8 @@ fn main() {
656657
client_state::client_state_set_restore_enabled,
657658
client_state::client_state_clear,
658659
client_state::client_state_renderer_flushed,
659-
client_state::client_state_navigation_flushed
660+
client_state::client_state_navigation_flushed,
661+
windows_update::install_stable_update
660662
])
661663
.on_menu_event(|app_handle, event| {
662664
match event.id().0.as_str() {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use std::process::Command;
2+
3+
#[cfg(windows)]
4+
use std::os::windows::process::CommandExt;
5+
6+
#[tauri::command]
7+
pub async fn install_stable_update() -> Result<(), String> {
8+
tauri::async_runtime::spawn_blocking(|| {
9+
let mut command = Command::new("winget");
10+
command.args([
11+
"upgrade",
12+
"--exact",
13+
"--id",
14+
"NeuralNomadsAI.CodeNomad",
15+
"--source",
16+
"winget",
17+
"--silent",
18+
"--accept-source-agreements",
19+
"--accept-package-agreements",
20+
"--disable-interactivity",
21+
]);
22+
23+
#[cfg(windows)]
24+
command.creation_flags(0x08000000);
25+
26+
let status = command
27+
.status()
28+
.map_err(|err| format!("Unable to start the WinGet update: {err}"))?;
29+
30+
if status.success() {
31+
Ok(())
32+
} else {
33+
Err(format!("WinGet update failed with status {status}"))
34+
}
35+
})
36+
.await
37+
.map_err(|err| format!("Unable to monitor the WinGet update: {err}"))?
38+
}

packages/ui/src/components/toast-history-panel.tsx

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import {
2121
deleteToastHistoryItem,
2222
markAllToastHistoryAsRead,
2323
markToastHistoryAsRead,
24+
runToastAction,
2425
subscribeToastHistory,
2526
} from "../lib/notifications"
26-
import { isTauriHost } from "../lib/runtime-env"
2727

2828
// ==================== Types ====================
2929

@@ -182,15 +182,9 @@ const ToastHistoryPanel: Component<ToastHistoryPanelProps> = (props) => {
182182

183183
// Handle item click
184184
const handleItemClick = (item: IToastHistoryItem) => {
185-
// Mark as read
186185
if (!item.read) {
187186
markToastHistoryAsRead(item.id);
188187
}
189-
190-
// Open action link if exists
191-
if (item.action?.href) {
192-
void handleOpenAction(item.action.href);
193-
}
194188
};
195189

196190
// Handle delete
@@ -209,21 +203,6 @@ const ToastHistoryPanel: Component<ToastHistoryPanelProps> = (props) => {
209203
markAllToastHistoryAsRead();
210204
};
211205

212-
// Open external link
213-
async function handleOpenAction(href: string): Promise<void> {
214-
if (isTauriHost()) {
215-
try {
216-
const { openUrl } = await import("@tauri-apps/plugin-opener");
217-
await openUrl(href);
218-
return;
219-
} catch (error) {
220-
console.warn("[toast-history] unable to open via system opener", error);
221-
}
222-
}
223-
224-
window.open(href, "_blank", "noopener,noreferrer");
225-
}
226-
227206
// Stop click propagation from backdrop to panel
228207
const handleBackdropClick = (event: MouseEvent) => {
229208
if (event.target === event.currentTarget) {
@@ -383,8 +362,8 @@ const ToastHistoryPanel: Component<ToastHistoryPanelProps> = (props) => {
383362
class="toast-history-item-action inline-flex items-center gap-1 text-xs mt-1"
384363
onClick={(e) => {
385364
e.stopPropagation();
386-
if (item.action?.href) {
387-
void handleOpenAction(item.action.href);
365+
if (item.action) {
366+
void runToastAction(item.action);
388367
}
389368
}}
390369
>

packages/ui/src/lib/notifications.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,19 @@ export type ToastHandle = {
1111

1212
type ToastPosition = "top-left" | "top-right" | "top-center" | "bottom-left" | "bottom-right" | "bottom-center"
1313

14+
export type ToastAction = {
15+
label: string
16+
href: string
17+
onClick?: () => void | Promise<void>
18+
}
19+
1420
export type ToastPayload = {
1521
title?: string
1622
message: string
1723
variant: ToastVariant
1824
duration?: number
1925
position?: ToastPosition
20-
action?: {
21-
label: string
22-
href: string
23-
}
26+
action?: ToastAction
2427
}
2528

2629
// ==================== Toast History Types ====================
@@ -42,10 +45,7 @@ export interface IToastHistoryItem {
4245
/** Read state (clicked) */
4346
read: boolean;
4447
/** Action link (optional) */
45-
action?: {
46-
label: string;
47-
href: string;
48-
};
48+
action?: ToastAction;
4949
}
5050

5151
/**
@@ -297,6 +297,20 @@ async function openExternalUrl(url: string): Promise<void> {
297297
}
298298
}
299299

300+
export async function runToastAction(action: ToastAction): Promise<void> {
301+
if (!action.onClick) {
302+
await openExternalUrl(action.href)
303+
return
304+
}
305+
306+
try {
307+
await action.onClick()
308+
} catch (error) {
309+
console.warn("[notifications] action failed; opening fallback link", error)
310+
await openExternalUrl(action.href)
311+
}
312+
}
313+
300314
// ==================== Variant Accent Styles ====================
301315

302316
const variantAccent: Record<
@@ -381,7 +395,7 @@ export function showToastNotification(payload: ToastPayload): ToastHandle {
381395
<button
382396
type="button"
383397
class="mt-3 inline-flex items-center text-xs font-semibold uppercase tracking-wide text-sky-300 hover:text-sky-200"
384-
onClick={() => void openExternalUrl(payload.action!.href)}
398+
onClick={() => void runToastAction(payload.action!)}
385399
>
386400
{payload.action.label}
387401
</button>

packages/ui/src/stores/releases.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { createEffect, createSignal } from "solid-js"
2+
import { invoke } from "@tauri-apps/api/core"
23
import type { ServerMeta, SupportMeta } from "../../../server/src/api-types"
34
import { getServerMeta } from "../lib/server-meta"
45
import { showToastNotification, ToastHandle } from "../lib/notifications"
56
import { getLogger } from "../lib/logger"
67
import { tGlobal } from "../lib/i18n"
8+
import { isLocalWindow, isTauriHost } from "../lib/runtime-env"
79
import { hasInstances, showFolderSelection } from "./ui"
810

911
const log = getLogger("actions")
@@ -61,6 +63,7 @@ function ensureVisibilityEffect() {
6163
? {
6264
label: tGlobal("releases.upgradeRequired.action.getUpdate"),
6365
href: support.latestServerUrl,
66+
onClick: isTauriHost() && isLocalWindow() && navigator.userAgent.includes("Windows") ? () => invoke("install_stable_update") : undefined,
6467
}
6568
: undefined,
6669
})

0 commit comments

Comments
 (0)