From c6933426a32ca3b21b37667842ed5aeed12ecef4 Mon Sep 17 00:00:00 2001
From: Ariaj Sarkar
Date: Sun, 15 Mar 2026 21:48:57 +0530
Subject: [PATCH 1/3] feat(desktop): add manual check-for-updates from web
settings UI
Add checkForUpdate IPC channel so the web renderer can trigger an update
check on demand. Surface a Check for Updates button in Settings > About
(desktop only) with contextual labels and error handling.
- Add DesktopUpdateCheckResult type and DesktopBridge.checkForUpdate()
- Wire UPDATE_CHECK_CHANNEL in preload and main process IPC handler
- Add canCheckForUpdate/getCheckForUpdateButtonLabel logic helpers
- Add DesktopUpdateCheckSection component in settings About section
- Guard IPC handler when updater is not configured (local/dev builds)
- Add 15 unit tests for new logic functions
Closes #1107
---
apps/desktop/src/main.ts | 17 +++
apps/desktop/src/preload.ts | 2 +
.../components/desktopUpdate.logic.test.ts | 101 ++++++++++++++++
.../web/src/components/desktopUpdate.logic.ts | 17 +++
apps/web/src/routes/_chat.settings.tsx | 114 ++++++++++++++++--
packages/contracts/src/ipc.ts | 6 +
6 files changed, 248 insertions(+), 9 deletions(-)
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
index 460684929..866356f67 100644
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -20,6 +20,7 @@ import * as Effect from "effect/Effect";
import type {
DesktopTheme,
DesktopUpdateActionResult,
+ DesktopUpdateCheckResult,
DesktopUpdateState,
} from "@t3tools/contracts";
import { autoUpdater } from "electron-updater";
@@ -56,6 +57,7 @@ const UPDATE_STATE_CHANNEL = "desktop:update-state";
const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
+const UPDATE_CHECK_CHANNEL = "desktop:update-check";
const STATE_DIR =
process.env.T3CODE_STATE_DIR?.trim() || Path.join(OS.homedir(), ".t3", "userdata");
const DESKTOP_SCHEME = "t3";
@@ -1211,6 +1213,21 @@ function registerIpcHandlers(): void {
state: updateState,
} satisfies DesktopUpdateActionResult;
});
+
+ ipcMain.removeHandler(UPDATE_CHECK_CHANNEL);
+ ipcMain.handle(UPDATE_CHECK_CHANNEL, async () => {
+ if (!updaterConfigured) {
+ return {
+ checked: false,
+ state: updateState,
+ } satisfies DesktopUpdateCheckResult;
+ }
+ await checkForUpdates("web-ui");
+ return {
+ checked: true,
+ state: updateState,
+ } satisfies DesktopUpdateCheckResult;
+ });
}
function getIconOption(): { icon: string } | Record {
diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts
index 1e1bb3bd8..29f23cde6 100644
--- a/apps/desktop/src/preload.ts
+++ b/apps/desktop/src/preload.ts
@@ -9,6 +9,7 @@ const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
const MENU_ACTION_CHANNEL = "desktop:menu-action";
const UPDATE_STATE_CHANNEL = "desktop:update-state";
const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
+const UPDATE_CHECK_CHANNEL = "desktop:update-check";
const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
const wsUrl = process.env.T3CODE_DESKTOP_WS_URL ?? null;
@@ -32,6 +33,7 @@ contextBridge.exposeInMainWorld("desktopBridge", {
};
},
getUpdateState: () => ipcRenderer.invoke(UPDATE_GET_STATE_CHANNEL),
+ checkForUpdate: () => ipcRenderer.invoke(UPDATE_CHECK_CHANNEL),
downloadUpdate: () => ipcRenderer.invoke(UPDATE_DOWNLOAD_CHANNEL),
installUpdate: () => ipcRenderer.invoke(UPDATE_INSTALL_CHANNEL),
onUpdateState: (listener) => {
diff --git a/apps/web/src/components/desktopUpdate.logic.test.ts b/apps/web/src/components/desktopUpdate.logic.test.ts
index 984eebd6b..ac06c8924 100644
--- a/apps/web/src/components/desktopUpdate.logic.test.ts
+++ b/apps/web/src/components/desktopUpdate.logic.test.ts
@@ -2,7 +2,9 @@ import { describe, expect, it } from "vitest";
import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts";
import {
+ canCheckForUpdate,
getArm64IntelBuildWarningDescription,
+ getCheckForUpdateButtonLabel,
getDesktopUpdateActionError,
getDesktopUpdateButtonTooltip,
isDesktopUpdateButtonDisabled,
@@ -207,3 +209,102 @@ describe("desktop update UI helpers", () => {
expect(getArm64IntelBuildWarningDescription(state)).toContain("Download the available update");
});
});
+
+describe("canCheckForUpdate", () => {
+ it("returns false for null state", () => {
+ expect(canCheckForUpdate(null)).toBe(false);
+ });
+
+ it("returns false when updates are disabled", () => {
+ expect(canCheckForUpdate({ ...baseState, enabled: false, status: "disabled" })).toBe(false);
+ });
+
+ it("returns false while checking", () => {
+ expect(canCheckForUpdate({ ...baseState, status: "checking" })).toBe(false);
+ });
+
+ it("returns false while downloading", () => {
+ expect(canCheckForUpdate({ ...baseState, status: "downloading", downloadPercent: 50 })).toBe(
+ false,
+ );
+ });
+
+ it("returns true when idle", () => {
+ expect(canCheckForUpdate({ ...baseState, status: "idle" })).toBe(true);
+ });
+
+ it("returns true when up-to-date", () => {
+ expect(canCheckForUpdate({ ...baseState, status: "up-to-date" })).toBe(true);
+ });
+
+ it("returns true when an update is available", () => {
+ expect(
+ canCheckForUpdate({ ...baseState, status: "available", availableVersion: "1.1.0" }),
+ ).toBe(true);
+ });
+
+ it("returns true on error so the user can retry", () => {
+ expect(
+ canCheckForUpdate({
+ ...baseState,
+ status: "error",
+ errorContext: "check",
+ message: "network",
+ }),
+ ).toBe(true);
+ });
+});
+
+describe("getCheckForUpdateButtonLabel", () => {
+ it("returns the default label for null state", () => {
+ expect(getCheckForUpdateButtonLabel(null)).toBe("Check for Updates");
+ });
+
+ it("returns 'Checking…' while checking", () => {
+ expect(getCheckForUpdateButtonLabel({ ...baseState, status: "checking" })).toBe("Checking…");
+ });
+
+ it("returns 'Up to Date' when up-to-date", () => {
+ expect(getCheckForUpdateButtonLabel({ ...baseState, status: "up-to-date" })).toBe("Up to Date");
+ });
+
+ it("returns the available version when an update is available", () => {
+ expect(
+ getCheckForUpdateButtonLabel({
+ ...baseState,
+ status: "available",
+ availableVersion: "1.2.0",
+ }),
+ ).toBe("Update Available: 1.2.0");
+ });
+
+ it("returns 'Downloading…' while downloading", () => {
+ expect(
+ getCheckForUpdateButtonLabel({ ...baseState, status: "downloading", downloadPercent: 30 }),
+ ).toBe("Downloading…");
+ });
+
+ it("returns 'Update Ready to Install' when downloaded", () => {
+ expect(
+ getCheckForUpdateButtonLabel({
+ ...baseState,
+ status: "downloaded",
+ downloadedVersion: "1.2.0",
+ }),
+ ).toBe("Update Ready to Install");
+ });
+
+ it("returns the default label for idle and error states", () => {
+ expect(getCheckForUpdateButtonLabel({ ...baseState, status: "idle" })).toBe(
+ "Check for Updates",
+ );
+ expect(
+ getCheckForUpdateButtonLabel({
+ ...baseState,
+ status: "error",
+ errorContext: "check",
+ message: "fail",
+ }),
+ ).toBe("Check for Updates");
+ });
+});
diff --git a/apps/web/src/components/desktopUpdate.logic.ts b/apps/web/src/components/desktopUpdate.logic.ts
index faf30883c..6d6ff0154 100644
--- a/apps/web/src/components/desktopUpdate.logic.ts
+++ b/apps/web/src/components/desktopUpdate.logic.ts
@@ -94,3 +94,20 @@ export function shouldHighlightDesktopUpdateError(state: DesktopUpdateState | nu
if (!state || state.status !== "error") return false;
return state.errorContext === "download" || state.errorContext === "install";
}
+
+export function canCheckForUpdate(state: DesktopUpdateState | null): boolean {
+ if (!state || !state.enabled) return false;
+ return (
+ state.status !== "checking" && state.status !== "downloading" && state.status !== "disabled"
+ );
+}
+
+export function getCheckForUpdateButtonLabel(state: DesktopUpdateState | null): string {
+ if (!state) return "Check for Updates";
+ if (state.status === "checking") return "Checking…";
+ if (state.status === "up-to-date") return "Up to Date";
+ if (state.status === "available") return `Update Available: ${state.availableVersion ?? ""}`;
+ if (state.status === "downloading") return "Downloading…";
+ if (state.status === "downloaded") return "Update Ready to Install";
+ return "Check for Updates";
+}
diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx
index b4afcdefa..f3bcc8793 100644
--- a/apps/web/src/routes/_chat.settings.tsx
+++ b/apps/web/src/routes/_chat.settings.tsx
@@ -1,7 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
-import { useCallback, useState } from "react";
-import { type ProviderKind } from "@t3tools/contracts";
+import { useCallback, useEffect, useState } from "react";
+import { type ProviderKind, type DesktopUpdateState } from "@t3tools/contracts";
import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
import { MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings";
import { resolveAndPersistPreferredEditor } from "../editorPreferences";
@@ -20,6 +20,7 @@ import {
} from "../components/ui/select";
import { Switch } from "../components/ui/switch";
import { APP_VERSION } from "../branding";
+import { canCheckForUpdate, getCheckForUpdateButtonLabel } from "../components/desktopUpdate.logic";
import { SidebarInset } from "~/components/ui/sidebar";
const THEME_OPTIONS = [
@@ -92,6 +93,97 @@ function patchCustomModels(provider: ProviderKind, models: string[]) {
}
}
+function DesktopUpdateCheckSection() {
+ const [updateState, setUpdateState] = useState(null);
+ const [checkError, setCheckError] = useState(null);
+
+ useEffect(() => {
+ const bridge = window.desktopBridge;
+ if (
+ !bridge ||
+ typeof bridge.getUpdateState !== "function" ||
+ typeof bridge.onUpdateState !== "function"
+ ) {
+ return;
+ }
+
+ let disposed = false;
+ let receivedSubscriptionUpdate = false;
+ const unsubscribe = bridge.onUpdateState((nextState) => {
+ if (disposed) return;
+ receivedSubscriptionUpdate = true;
+ setUpdateState(nextState);
+ });
+
+ void bridge
+ .getUpdateState()
+ .then((nextState) => {
+ if (disposed || receivedSubscriptionUpdate) return;
+ setUpdateState(nextState);
+ })
+ .catch(() => undefined);
+
+ return () => {
+ disposed = true;
+ unsubscribe();
+ };
+ }, []);
+
+ const handleCheckForUpdate = useCallback(() => {
+ const bridge = window.desktopBridge;
+ if (!bridge || typeof bridge.checkForUpdate !== "function") return;
+ setCheckError(null);
+
+ void bridge
+ .checkForUpdate()
+ .then((result) => {
+ setUpdateState(result.state);
+ if (!result.checked) {
+ setCheckError(
+ result.state.message ?? "Automatic updates are not available in this build.",
+ );
+ }
+ })
+ .catch((error: unknown) => {
+ setCheckError(error instanceof Error ? error.message : "Update check failed.");
+ });
+ }, []);
+
+ const buttonLabel = getCheckForUpdateButtonLabel(updateState);
+ const buttonDisabled = !canCheckForUpdate(updateState);
+
+ return (
+
+
+
+
Updates
+
+ {updateState?.checkedAt
+ ? `Last checked: ${new Date(updateState.checkedAt).toLocaleString()}`
+ : "Check for available updates."}
+
+
+
+
+
+ {checkError ?
{checkError}
: null}
+
+ {updateState?.status === "error" && updateState.errorContext === "check" ? (
+
+ {updateState.message ?? "Could not check for updates."}
+
+ ) : null}
+
+ );
+}
+
function SettingsRouteView() {
const { theme, setTheme, resolvedTheme } = useTheme();
const { settings, defaults, updateSettings } = useAppSettings();
@@ -674,14 +766,18 @@ function SettingsRouteView() {
-
-
-
Version
-
- Current version of the application.
-
+
+
+
+
Version
+
+ Current version of the application.
+
+
+
{APP_VERSION}
-
{APP_VERSION}
+
+ {isElectron ?
: null}
diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts
index b9127fb17..32b6f109b 100644
--- a/packages/contracts/src/ipc.ts
+++ b/packages/contracts/src/ipc.ts
@@ -94,6 +94,11 @@ export interface DesktopUpdateActionResult {
state: DesktopUpdateState;
}
+export interface DesktopUpdateCheckResult {
+ checked: boolean;
+ state: DesktopUpdateState;
+}
+
export interface DesktopBridge {
getWsUrl: () => string | null;
pickFolder: () => Promise
;
@@ -106,6 +111,7 @@ export interface DesktopBridge {
openExternal: (url: string) => Promise;
onMenuAction: (listener: (action: string) => void) => () => void;
getUpdateState: () => Promise;
+ checkForUpdate: () => Promise;
downloadUpdate: () => Promise;
installUpdate: () => Promise;
onUpdateState: (listener: (state: DesktopUpdateState) => void) => () => void;
From f57619ddc4d15afc296fa1a918a5ea042f9bdb93 Mon Sep 17 00:00:00 2001
From: Ariaj Sarkar
Date: Tue, 17 Mar 2026 00:31:36 +0530
Subject: [PATCH 2/3] refactor: use useQuery for update state instead of manual
useEffect
---
apps/web/src/routes/_chat.settings.tsx | 54 ++++++++------------------
1 file changed, 17 insertions(+), 37 deletions(-)
diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx
index f3bcc8793..ec64e0a8c 100644
--- a/apps/web/src/routes/_chat.settings.tsx
+++ b/apps/web/src/routes/_chat.settings.tsx
@@ -1,7 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";
-import { useQuery } from "@tanstack/react-query";
-import { useCallback, useEffect, useState } from "react";
-import { type ProviderKind, type DesktopUpdateState } from "@t3tools/contracts";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { useCallback, useState } from "react";
+import { type ProviderKind } from "@t3tools/contracts";
import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
import { MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings";
import { resolveAndPersistPreferredEditor } from "../editorPreferences";
@@ -94,40 +94,20 @@ function patchCustomModels(provider: ProviderKind, models: string[]) {
}
function DesktopUpdateCheckSection() {
- const [updateState, setUpdateState] = useState(null);
+ const queryClient = useQueryClient();
+ const updateStateQuery = useQuery({
+ queryKey: ["desktop", "updateState"],
+ queryFn: async () => {
+ const bridge = window.desktopBridge;
+ if (!bridge || typeof bridge.getUpdateState !== "function") return null;
+ return bridge.getUpdateState();
+ },
+ staleTime: Infinity,
+ enabled: isElectron,
+ });
const [checkError, setCheckError] = useState(null);
- useEffect(() => {
- const bridge = window.desktopBridge;
- if (
- !bridge ||
- typeof bridge.getUpdateState !== "function" ||
- typeof bridge.onUpdateState !== "function"
- ) {
- return;
- }
-
- let disposed = false;
- let receivedSubscriptionUpdate = false;
- const unsubscribe = bridge.onUpdateState((nextState) => {
- if (disposed) return;
- receivedSubscriptionUpdate = true;
- setUpdateState(nextState);
- });
-
- void bridge
- .getUpdateState()
- .then((nextState) => {
- if (disposed || receivedSubscriptionUpdate) return;
- setUpdateState(nextState);
- })
- .catch(() => undefined);
-
- return () => {
- disposed = true;
- unsubscribe();
- };
- }, []);
+ const updateState = updateStateQuery.data ?? null;
const handleCheckForUpdate = useCallback(() => {
const bridge = window.desktopBridge;
@@ -137,7 +117,7 @@ function DesktopUpdateCheckSection() {
void bridge
.checkForUpdate()
.then((result) => {
- setUpdateState(result.state);
+ queryClient.setQueryData(["desktop", "updateState"], result.state);
if (!result.checked) {
setCheckError(
result.state.message ?? "Automatic updates are not available in this build.",
@@ -147,7 +127,7 @@ function DesktopUpdateCheckSection() {
.catch((error: unknown) => {
setCheckError(error instanceof Error ? error.message : "Update check failed.");
});
- }, []);
+ }, [queryClient]);
const buttonLabel = getCheckForUpdateButtonLabel(updateState);
const buttonDisabled = !canCheckForUpdate(updateState);
From e8409668ca7bc68c8c9e06ed7f141c6f16a7728e Mon Sep 17 00:00:00 2001
From: Ariaj Sarkar
Date: Tue, 17 Mar 2026 01:11:39 +0530
Subject: [PATCH 3/3] refactor: use queryOptions pattern with real-time sync
and multi-purpose button
---
.../components/desktopUpdate.logic.test.ts | 8 +-
.../web/src/components/desktopUpdate.logic.ts | 4 +-
apps/web/src/lib/desktopUpdateReactQuery.ts | 18 +++++
apps/web/src/routes/_chat.settings.tsx | 77 +++++++++++++------
4 files changed, 79 insertions(+), 28 deletions(-)
create mode 100644 apps/web/src/lib/desktopUpdateReactQuery.ts
diff --git a/apps/web/src/components/desktopUpdate.logic.test.ts b/apps/web/src/components/desktopUpdate.logic.test.ts
index ac06c8924..a37601d81 100644
--- a/apps/web/src/components/desktopUpdate.logic.test.ts
+++ b/apps/web/src/components/desktopUpdate.logic.test.ts
@@ -268,14 +268,14 @@ describe("getCheckForUpdateButtonLabel", () => {
expect(getCheckForUpdateButtonLabel({ ...baseState, status: "up-to-date" })).toBe("Up to Date");
});
- it("returns the available version when an update is available", () => {
+ it("returns 'Download Update' when an update is available", () => {
expect(
getCheckForUpdateButtonLabel({
...baseState,
status: "available",
availableVersion: "1.2.0",
}),
- ).toBe("Update Available: 1.2.0");
+ ).toBe("Download Update");
});
it("returns 'Downloading…' while downloading", () => {
@@ -284,14 +284,14 @@ describe("getCheckForUpdateButtonLabel", () => {
).toBe("Downloading…");
});
- it("returns 'Update Ready to Install' when downloaded", () => {
+ it("returns 'Install Update' when downloaded", () => {
expect(
getCheckForUpdateButtonLabel({
...baseState,
status: "downloaded",
downloadedVersion: "1.2.0",
}),
- ).toBe("Update Ready to Install");
+ ).toBe("Install Update");
});
it("returns the default label for idle and error states", () => {
diff --git a/apps/web/src/components/desktopUpdate.logic.ts b/apps/web/src/components/desktopUpdate.logic.ts
index 6d6ff0154..cc6ae95d6 100644
--- a/apps/web/src/components/desktopUpdate.logic.ts
+++ b/apps/web/src/components/desktopUpdate.logic.ts
@@ -106,8 +106,8 @@ export function getCheckForUpdateButtonLabel(state: DesktopUpdateState | null):
if (!state) return "Check for Updates";
if (state.status === "checking") return "Checking…";
if (state.status === "up-to-date") return "Up to Date";
- if (state.status === "available") return `Update Available: ${state.availableVersion ?? ""}`;
+ if (state.status === "available") return "Download Update";
if (state.status === "downloading") return "Downloading…";
- if (state.status === "downloaded") return "Update Ready to Install";
+ if (state.status === "downloaded") return "Install Update";
return "Check for Updates";
}
diff --git a/apps/web/src/lib/desktopUpdateReactQuery.ts b/apps/web/src/lib/desktopUpdateReactQuery.ts
new file mode 100644
index 000000000..ba1db1798
--- /dev/null
+++ b/apps/web/src/lib/desktopUpdateReactQuery.ts
@@ -0,0 +1,18 @@
+import { queryOptions } from "@tanstack/react-query";
+
+export const desktopUpdateQueryKeys = {
+ all: ["desktop", "update"] as const,
+ state: () => ["desktop", "update", "state"] as const,
+};
+
+export function desktopUpdateStateQueryOptions() {
+ return queryOptions({
+ queryKey: desktopUpdateQueryKeys.state(),
+ queryFn: async () => {
+ const bridge = window.desktopBridge;
+ if (!bridge || typeof bridge.getUpdateState !== "function") return null;
+ return bridge.getUpdateState();
+ },
+ staleTime: Infinity,
+ });
+}
diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx
index ec64e0a8c..2ae963752 100644
--- a/apps/web/src/routes/_chat.settings.tsx
+++ b/apps/web/src/routes/_chat.settings.tsx
@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useCallback, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { type ProviderKind } from "@t3tools/contracts";
import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
import { MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings";
@@ -20,7 +20,12 @@ import {
} from "../components/ui/select";
import { Switch } from "../components/ui/switch";
import { APP_VERSION } from "../branding";
-import { canCheckForUpdate, getCheckForUpdateButtonLabel } from "../components/desktopUpdate.logic";
+import {
+ canCheckForUpdate,
+ getCheckForUpdateButtonLabel,
+ resolveDesktopUpdateButtonAction,
+} from "../components/desktopUpdate.logic";
+import { desktopUpdateStateQueryOptions } from "../lib/desktopUpdateReactQuery";
import { SidebarInset } from "~/components/ui/sidebar";
const THEME_OPTIONS = [
@@ -95,29 +100,62 @@ function patchCustomModels(provider: ProviderKind, models: string[]) {
function DesktopUpdateCheckSection() {
const queryClient = useQueryClient();
- const updateStateQuery = useQuery({
- queryKey: ["desktop", "updateState"],
- queryFn: async () => {
- const bridge = window.desktopBridge;
- if (!bridge || typeof bridge.getUpdateState !== "function") return null;
- return bridge.getUpdateState();
- },
- staleTime: Infinity,
- enabled: isElectron,
- });
+ const updateStateQuery = useQuery(desktopUpdateStateQueryOptions());
const [checkError, setCheckError] = useState(null);
const updateState = updateStateQuery.data ?? null;
- const handleCheckForUpdate = useCallback(() => {
+ useEffect(() => {
+ const bridge = window.desktopBridge;
+ if (!bridge || typeof bridge.onUpdateState !== "function") return;
+
+ const opts = desktopUpdateStateQueryOptions();
+ const unsubscribe = bridge.onUpdateState((nextState) => {
+ queryClient.setQueryData(opts.queryKey, nextState);
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, [queryClient]);
+
+ const handleButtonClick = useCallback(() => {
const bridge = window.desktopBridge;
- if (!bridge || typeof bridge.checkForUpdate !== "function") return;
+ if (!bridge) return;
setCheckError(null);
+ const action = updateState ? resolveDesktopUpdateButtonAction(updateState) : "none";
+ const opts = desktopUpdateStateQueryOptions();
+
+ if (action === "download") {
+ void bridge
+ .downloadUpdate()
+ .then((result) => {
+ queryClient.setQueryData(opts.queryKey, result.state);
+ })
+ .catch((error: unknown) => {
+ setCheckError(error instanceof Error ? error.message : "Download failed.");
+ });
+ return;
+ }
+
+ if (action === "install") {
+ void bridge
+ .installUpdate()
+ .then((result) => {
+ queryClient.setQueryData(opts.queryKey, result.state);
+ })
+ .catch((error: unknown) => {
+ setCheckError(error instanceof Error ? error.message : "Install failed.");
+ });
+ return;
+ }
+
+ if (typeof bridge.checkForUpdate !== "function") return;
void bridge
.checkForUpdate()
.then((result) => {
- queryClient.setQueryData(["desktop", "updateState"], result.state);
+ queryClient.setQueryData(opts.queryKey, result.state);
if (!result.checked) {
setCheckError(
result.state.message ?? "Automatic updates are not available in this build.",
@@ -127,7 +165,7 @@ function DesktopUpdateCheckSection() {
.catch((error: unknown) => {
setCheckError(error instanceof Error ? error.message : "Update check failed.");
});
- }, [queryClient]);
+ }, [queryClient, updateState]);
const buttonLabel = getCheckForUpdateButtonLabel(updateState);
const buttonDisabled = !canCheckForUpdate(updateState);
@@ -143,12 +181,7 @@ function DesktopUpdateCheckSection() {
: "Check for available updates."}
-