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" ? (