Skip to content
Open
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
17 changes: 17 additions & 0 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as Effect from "effect/Effect";
import type {
DesktopTheme,
DesktopUpdateActionResult,
DesktopUpdateCheckResult,
DesktopUpdateState,
} from "@t3tools/contracts";
import { autoUpdater } from "electron-updater";
Expand Down Expand Up @@ -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 BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3");
const STATE_DIR = Path.join(BASE_DIR, "userdata");
const DESKTOP_SCHEME = "t3";
Expand Down Expand Up @@ -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<string, never> {
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 @@ -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;
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
15 changes: 11 additions & 4 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
ArrowDownToLineIcon,
ArrowLeftIcon,
ChevronRightIcon,
FolderIcon,
GitPullRequestIcon,
PlusIcon,
RocketIcon,
SettingsIcon,
SquarePenIcon,
TerminalIcon,
Expand Down Expand Up @@ -1062,7 +1062,9 @@ export default function Sidebar() {
? "text-sky-400"
: shouldHighlightDesktopUpdateError(desktopUpdateState)
? "text-rose-500 animate-pulse"
: "text-amber-500 animate-pulse";
: desktopUpdateState?.status === "available"
? "text-emerald-500"
: "text-muted-foreground";
const newThreadShortcutLabel = useMemo(
() =>
shortcutLabelForCommand(keybindings, "chat.newLocal") ??
Expand Down Expand Up @@ -1106,6 +1108,11 @@ export default function Sidebar() {
}

if (desktopUpdateButtonAction === "install") {
const version = desktopUpdateState.downloadedVersion ?? desktopUpdateState.availableVersion;
const confirmed = window.confirm(
`Install update${version ? ` ${version}` : ""} and restart T3 Code?\n\nAny running tasks will be interrupted. Make sure you're ready before continuing.`,
);
if (!confirmed) return;
void bridge
.installUpdate()
.then((result) => {
Expand Down Expand Up @@ -1185,10 +1192,10 @@ export default function Sidebar() {
aria-label={desktopUpdateTooltip}
aria-disabled={desktopUpdateButtonDisabled || undefined}
disabled={desktopUpdateButtonDisabled}
className={`inline-flex size-7 ml-auto mt-1.5 items-center justify-center rounded-md text-muted-foreground transition-colors ${desktopUpdateButtonInteractivityClasses} ${desktopUpdateButtonClasses}`}
className={`inline-flex size-7 ml-auto mt-1.5 items-center justify-center rounded-md transition-colors ${desktopUpdateButtonInteractivityClasses} ${desktopUpdateButtonClasses}`}
onClick={handleDesktopUpdateButtonClick}
>
<RocketIcon className="size-3.5" />
<ArrowDownToLineIcon className="size-3.5" />
</button>
}
/>
Expand Down
101 changes: 101 additions & 0 deletions apps/web/src/components/desktopUpdate.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { describe, expect, it } from "vitest";
import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts";

import {
canCheckForUpdate,
getArm64IntelBuildWarningDescription,
getCheckForUpdateButtonLabel,
getDesktopUpdateActionError,
getDesktopUpdateButtonTooltip,
isDesktopUpdateButtonDisabled,
Expand Down Expand Up @@ -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 'Download Update' when an update is available", () => {
expect(
getCheckForUpdateButtonLabel({
...baseState,
status: "available",
availableVersion: "1.2.0",
}),
).toBe("Download Update");
});

it("returns 'Downloading…' while downloading", () => {
expect(
getCheckForUpdateButtonLabel({ ...baseState, status: "downloading", downloadPercent: 30 }),
).toBe("Downloading…");
});

it("returns 'Install Update' when downloaded", () => {
expect(
getCheckForUpdateButtonLabel({
...baseState,
status: "downloaded",
downloadedVersion: "1.2.0",
}),
).toBe("Install Update");
});

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");
});
});
17 changes: 17 additions & 0 deletions apps/web/src/components/desktopUpdate.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 "Download Update";
if (state.status === "downloading") return "Downloading…";
if (state.status === "downloaded") return "Install Update";
return "Check for Updates";
}
18 changes: 18 additions & 0 deletions apps/web/src/lib/desktopUpdateReactQuery.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
Loading