diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e151b9f6fcda..8dd575b5a6bc 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/desktop/src/main.ts b/apps/desktop/src/main.ts index 46068492950c..7cf36bc1c730 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 1e1bb3bd8ebe..f71a173d8b7e 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/server/package.json b/apps/server/package.json index bb8e480657a7..cc739c9bafc8 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/package.json b/apps/web/package.json index 1162b97fc0ee..7d4458031c33 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": { diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index ecc6c533456a..733a06d7e704 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,82 @@ function SettingsRouteView() { Partial> >({}); + const [updateState, setUpdateState] = useState(null); + const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); + + const hasDesktopBridge = isElectron && !!window.desktopBridge; + + useEffect(() => { + 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 (!hasDesktopBridge) return; + setIsCheckingUpdate(true); + try { + 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 (!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 (!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; const accentColor = settings.accentColor; @@ -995,14 +1071,88 @@ 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} + {hasDesktopBridge ? ( + <> + {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 30ee2cabe0fe..6b7b632a3fd0 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" diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index 5aa66cfa6850..1e492282dbc3 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;