Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t3tools/desktop",
"version": "0.0.2",
"version": "0.0.3",
"private": true,
"main": "dist-electron/main.js",
"scripts": {
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand All @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "t3",
"version": "0.0.2",
"version": "0.0.3",
"repository": {
"type": "git",
"url": "https://github.com/pingdotgg/t3code",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t3tools/web",
"version": "0.0.2",
"version": "0.0.3",
"private": true,
"type": "module",
"scripts": {
Expand Down
168 changes: 159 additions & 9 deletions apps/web/src/routes/_chat.settings.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -211,6 +211,82 @@ function SettingsRouteView() {
Partial<Record<ProviderKind, string | null>>
>({});

const [updateState, setUpdateState] = useState<DesktopUpdateState | null>(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;
Expand Down Expand Up @@ -995,14 +1071,88 @@ function SettingsRouteView() {
</p>
</div>

<div className="flex items-center justify-between rounded-lg border border-border bg-background px-3 py-2">
<div>
<p className="text-sm font-medium text-foreground">Version</p>
<p className="text-xs text-muted-foreground">
Current version of the application.
</p>
<div className="space-y-3">
<div className="flex items-center justify-between rounded-lg border border-border bg-background px-3 py-2">
<div>
<p className="text-sm font-medium text-foreground">Version</p>
<p className="text-xs text-muted-foreground">
{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."}
</p>
{updateState?.checkedAt ? (
<p className="mt-0.5 text-[11px] text-muted-foreground/70">
Last checked: {new Date(updateState.checkedAt).toLocaleString()}
</p>
) : null}
</div>
<div className="ml-3 flex shrink-0 items-center gap-2">
<code className="text-xs font-medium text-muted-foreground">{APP_VERSION}</code>
{hasDesktopBridge ? (
<>
{updateState?.status === "available" ? (
<Button size="xs" onClick={handleDownloadUpdate}>
Download
</Button>
) : null}
{updateState?.status === "downloaded" ? (
<Button size="xs" onClick={handleInstallUpdate}>
Restart & Install
</Button>
) : null}
{updateState?.status === "error" &&
updateState.errorContext === "download" &&
updateState.availableVersion ? (
<Button size="xs" variant="outline" onClick={handleDownloadUpdate}>
Retry Download
</Button>
) : null}
{updateState?.status === "error" &&
updateState.errorContext === "install" &&
updateState.downloadedVersion ? (
<Button size="xs" variant="outline" onClick={handleInstallUpdate}>
Retry Install
</Button>
) : null}
<Button
size="xs"
variant="outline"
disabled={
isCheckingUpdate ||
updateState?.status === "checking" ||
updateState?.status === "downloading"
}
onClick={handleCheckForUpdate}
>
{isCheckingUpdate || updateState?.status === "checking"
? "Checking..."
: "Check for Updates"}
</Button>
</>
) : null}
</div>
</div>
<code className="text-xs font-medium text-muted-foreground">{APP_VERSION}</code>
{updateState?.status === "downloading" &&
typeof updateState.downloadPercent === "number" ? (
<div className="px-1">
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-primary transition-all duration-300"
style={{ width: `${updateState.downloadPercent}%` }}
/>
</div>
</div>
) : null}
</div>
</section>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t3tools/contracts",
"version": "0.0.2",
"version": "0.0.3",
"private": true,
"files": [
"dist"
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/src/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export interface DesktopBridge {
openExternal: (url: string) => Promise<boolean>;
onMenuAction: (listener: (action: string) => void) => () => void;
getUpdateState: () => Promise<DesktopUpdateState>;
checkForUpdate: () => Promise<DesktopUpdateState>;
downloadUpdate: () => Promise<DesktopUpdateActionResult>;
installUpdate: () => Promise<DesktopUpdateActionResult>;
onUpdateState: (listener: (state: DesktopUpdateState) => void) => () => void;
Expand Down
Loading