From 569deca0f0f01303dd81b4f1277833109c2213dd Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 15 Mar 2026 22:10:37 +0530 Subject: [PATCH 1/5] feat: add Check for Updates button in Settings page --- apps/desktop/src/main.ts | 7 +++++ apps/desktop/src/preload.ts | 2 ++ apps/web/src/routes/_chat.settings.tsx | 38 ++++++++++++++++++++++++-- packages/contracts/src/ipc.ts | 1 + 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 46068492950..7cf36bc1c73 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -56,6 +56,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"; @@ -1185,6 +1186,12 @@ function registerIpcHandlers(): void { ipcMain.removeHandler(UPDATE_GET_STATE_CHANNEL); ipcMain.handle(UPDATE_GET_STATE_CHANNEL, async () => updateState); + ipcMain.removeHandler(UPDATE_CHECK_CHANNEL); + ipcMain.handle(UPDATE_CHECK_CHANNEL, async () => { + await checkForUpdates("manual"); + return updateState; + }); + ipcMain.removeHandler(UPDATE_DOWNLOAD_CHANNEL); ipcMain.handle(UPDATE_DOWNLOAD_CHANNEL, async () => { const result = await downloadAvailableUpdate(); diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 1e1bb3bd8eb..f71a173d8b7 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -11,6 +11,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 wsUrl = process.env.T3CODE_DESKTOP_WS_URL ?? null; contextBridge.exposeInMainWorld("desktopBridge", { @@ -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/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index ecc6c533456..3aa8af01ce8 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 DesktopUpdateState, type ProviderKind } from "@t3tools/contracts"; import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; import { @@ -211,6 +211,40 @@ function SettingsRouteView() { Partial> >({}); + const [updateState, setUpdateState] = useState(null); + const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); + + useEffect(() => { + if (!isElectron || !window.desktopBridge) return; + const bridge = window.desktopBridge; + void bridge.getUpdateState().then(setUpdateState); + const unsubscribe = bridge.onUpdateState(setUpdateState); + return unsubscribe; + }, []); + + const handleCheckForUpdate = useCallback(async () => { + if (!isElectron || !window.desktopBridge) return; + setIsCheckingUpdate(true); + try { + const state = await window.desktopBridge.checkForUpdate(); + setUpdateState(state); + } finally { + setIsCheckingUpdate(false); + } + }, []); + + const handleDownloadUpdate = useCallback(async () => { + if (!isElectron || !window.desktopBridge) return; + const result = await window.desktopBridge.downloadUpdate(); + setUpdateState(result.state); + }, []); + + const handleInstallUpdate = useCallback(async () => { + if (!isElectron || !window.desktopBridge) return; + const result = await window.desktopBridge.installUpdate(); + setUpdateState(result.state); + }, []); + const codexBinaryPath = settings.codexBinaryPath; const codexHomePath = settings.codexHomePath; const accentColor = settings.accentColor; diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index 5aa66cfa685..1e492282dbc 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -117,6 +117,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 100bba8ad38f9f3cbc9073af61089dc170a611aa Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 15 Mar 2026 22:17:42 +0530 Subject: [PATCH 2/5] chore: bump version to 0.0.3 and refine update UI in settings --- apps/desktop/package.json | 2 +- apps/web/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e151b9f6fcd..8dd575b5a6b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/desktop", - "version": "0.0.2", + "version": "0.0.3", "private": true, "main": "dist-electron/main.js", "scripts": { diff --git a/apps/web/package.json b/apps/web/package.json index 1162b97fc0e..7d4458031c3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/web", - "version": "0.0.2", + "version": "0.0.3", "private": true, "type": "module", "scripts": { From f02779b45683382d1c6bb0a847b75949f3335a93 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 15 Mar 2026 22:31:12 +0530 Subject: [PATCH 3/5] fix: wire update UI into About section and bump all packages to 0.0.3 - Add Check for Updates button, download/install actions, and progress bar to the About section in Settings - Bump server and contracts package versions to match desktop and web --- apps/server/package.json | 2 +- apps/web/src/routes/_chat.settings.tsx | 79 +++++++++++++++++++++++--- packages/contracts/package.json | 2 +- 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index bb8e480657a..cc739c9bafc 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "t3", - "version": "0.0.2", + "version": "0.0.3", "repository": { "type": "git", "url": "https://github.com/pingdotgg/t3code", diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 3aa8af01ce8..bf9502b90ff 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -1029,14 +1029,79 @@ function SettingsRouteView() {

-
-
-

Version

-

- Current version of the application. -

+
+
+
+

Version

+

+ {updateState?.status === "up-to-date" + ? "You're on the latest version." + : updateState?.status === "checking" + ? "Checking for updates..." + : updateState?.status === "available" + ? `Version ${updateState.availableVersion ?? "unknown"} is available.` + : updateState?.status === "downloading" + ? `Downloading update${typeof updateState.downloadPercent === "number" ? ` (${Math.floor(updateState.downloadPercent)}%)` : ""}...` + : updateState?.status === "downloaded" + ? `Version ${updateState.downloadedVersion ?? updateState.availableVersion ?? "unknown"} is ready to install.` + : updateState?.status === "error" + ? (updateState.message ?? "Update check failed.") + : "Current version of the application."} +

+ {updateState?.checkedAt ? ( +

+ Last checked: {new Date(updateState.checkedAt).toLocaleString()} +

+ ) : null} +
+
+ {APP_VERSION} + {isElectron ? ( + <> + {updateState?.status === "available" ? ( + + ) : null} + {updateState?.status === "downloaded" ? ( + + ) : null} + {updateState?.status === "error" && updateState.errorContext === "download" && updateState.availableVersion ? ( + + ) : null} + {updateState?.status === "error" && updateState.errorContext === "install" && updateState.downloadedVersion ? ( + + ) : null} + + + ) : null} +
- {APP_VERSION} + {updateState?.status === "downloading" && typeof updateState.downloadPercent === "number" ? ( +
+
+
+
+
+ ) : null}
diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 30ee2cabe0f..6b7b632a3fd 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/contracts", - "version": "0.0.2", + "version": "0.0.3", "private": true, "files": [ "dist" From 67de18b72bb351880eaf584492970f447fb0e625 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 15 Mar 2026 23:13:08 +0530 Subject: [PATCH 4/5] style: format settings file with oxfmt --- apps/web/src/routes/_chat.settings.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index bf9502b90ff..583d5a0d1a2 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -1068,12 +1068,16 @@ function SettingsRouteView() { Restart & Install ) : null} - {updateState?.status === "error" && updateState.errorContext === "download" && updateState.availableVersion ? ( + {updateState?.status === "error" && + updateState.errorContext === "download" && + updateState.availableVersion ? ( ) : null} - {updateState?.status === "error" && updateState.errorContext === "install" && updateState.downloadedVersion ? ( + {updateState?.status === "error" && + updateState.errorContext === "install" && + updateState.downloadedVersion ? ( @@ -1081,7 +1085,11 @@ function SettingsRouteView() {
- {updateState?.status === "downloading" && typeof updateState.downloadPercent === "number" ? ( + {updateState?.status === "downloading" && + typeof updateState.downloadPercent === "number" ? (
Date: Sun, 15 Mar 2026 23:23:42 +0530 Subject: [PATCH 5/5] fix: add error handling for IPC calls and guard update UI behind desktopBridge --- apps/web/src/routes/_chat.settings.tsx | 74 ++++++++++++++++++++------ 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 583d5a0d1a2..733a06d7e70 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -214,36 +214,78 @@ function SettingsRouteView() { const [updateState, setUpdateState] = useState(null); const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); + const hasDesktopBridge = isElectron && !!window.desktopBridge; + useEffect(() => { - if (!isElectron || !window.desktopBridge) return; - const bridge = window.desktopBridge; - void bridge.getUpdateState().then(setUpdateState); + if (!hasDesktopBridge) return; + const bridge = window.desktopBridge!; + void bridge + .getUpdateState() + .then(setUpdateState) + .catch(() => {}); const unsubscribe = bridge.onUpdateState(setUpdateState); return unsubscribe; - }, []); + }, [hasDesktopBridge]); const handleCheckForUpdate = useCallback(async () => { - if (!isElectron || !window.desktopBridge) return; + if (!hasDesktopBridge) return; setIsCheckingUpdate(true); try { - const state = await window.desktopBridge.checkForUpdate(); + const state = await window.desktopBridge!.checkForUpdate(); setUpdateState(state); + } catch { + setUpdateState((prev) => + prev + ? { + ...prev, + status: "error", + message: "Failed to check for updates.", + errorContext: "check", + } + : prev, + ); } finally { setIsCheckingUpdate(false); } - }, []); + }, [hasDesktopBridge]); const handleDownloadUpdate = useCallback(async () => { - if (!isElectron || !window.desktopBridge) return; - const result = await window.desktopBridge.downloadUpdate(); - setUpdateState(result.state); - }, []); + if (!hasDesktopBridge) return; + try { + const result = await window.desktopBridge!.downloadUpdate(); + setUpdateState(result.state); + } catch (error) { + setUpdateState((prev) => + prev + ? { + ...prev, + status: "error", + message: error instanceof Error ? error.message : "Failed to download update.", + errorContext: "download", + } + : prev, + ); + } + }, [hasDesktopBridge]); const handleInstallUpdate = useCallback(async () => { - if (!isElectron || !window.desktopBridge) return; - const result = await window.desktopBridge.installUpdate(); - setUpdateState(result.state); - }, []); + if (!hasDesktopBridge) return; + try { + const result = await window.desktopBridge!.installUpdate(); + setUpdateState(result.state); + } catch (error) { + setUpdateState((prev) => + prev + ? { + ...prev, + status: "error", + message: error instanceof Error ? error.message : "Failed to install update.", + errorContext: "install", + } + : prev, + ); + } + }, [hasDesktopBridge]); const codexBinaryPath = settings.codexBinaryPath; const codexHomePath = settings.codexHomePath; @@ -1056,7 +1098,7 @@ function SettingsRouteView() {
{APP_VERSION} - {isElectron ? ( + {hasDesktopBridge ? ( <> {updateState?.status === "available" ? (