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."}

-