Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dad415c
feat(web): move update button to sidebar footer as dismissable pill
Noojuno Mar 15, 2026
5f41c53
refactor(web): extract SidebarUpdatePill into self-contained component
Noojuno Mar 15, 2026
161d78f
feat(web): restore arm64 warning Alert UI in update pill
Noojuno Mar 15, 2026
05a9165
chore(web): remove test mocks from SidebarUpdatePill
Noojuno Mar 15, 2026
b0b60be
feat(desktop): add manual check-for-updates from web settings UI
AriajSarkar Mar 15, 2026
40c1777
refactor: use queryOptions pattern with real-time sync and multi-purp…
AriajSarkar Mar 16, 2026
17de2fc
feat(web): add tooltip to update button in settings
AriajSarkar Mar 17, 2026
1051bb8
fix(settings): remove redundant getCheckForUpdateButtonLabel, use act…
AriajSarkar Mar 24, 2026
8541519
fix: tooltip fallback returns 'Up to date' for idle/up-to-date states…
AriajSarkar Mar 24, 2026
f11e84a
feat(web): add about settings section for updates
shivamhwp Mar 28, 2026
f2b79c9
refine settings about layout
shivamhwp Mar 28, 2026
89b546f
Merge remote-tracking branch 'upstream/main' into update-overhaul
shivamhwp Mar 28, 2026
0bfc742
chore(web): clean up sidebar imports after merge
shivamhwp Mar 28, 2026
cf33c2c
Update About card description for available updates
shivamhwp Mar 28, 2026
513aef2
Fix about update card install action
shivamhwp Mar 28, 2026
c01b791
Fix sidebar context clicks and update status handling
shivamhwp Mar 28, 2026
1dd3783
Merge remote-tracking branch 'upstream/main' into update-overhaul
shivamhwp Mar 29, 2026
b38aa2b
chore(web): clean up sidebar after main merge
shivamhwp Mar 29, 2026
0febe9b
Merge About settings into General and redirect old route
shivamhwp Mar 29, 2026
480a7ff
Refine desktop update state caching
shivamhwp Mar 29, 2026
35c3d02
Remove desktop update error highlighting helper
shivamhwp Mar 29, 2026
1e6d5f2
Merge origin/main into update-overhaul
juliusmarminge Mar 29, 2026
524135c
Refine desktop update UI and install flow
juliusmarminge Mar 29, 2026
868f6c5
rm redirect route
juliusmarminge Mar 29, 2026
b98abaf
rm hideHeader
juliusmarminge Mar 29, 2026
2928da6
archive
juliusmarminge Mar 29, 2026
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
25 changes: 22 additions & 3 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 GET_WS_URL_CHANNEL = "desktop:get-ws-url";
const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3");
const STATE_DIR = Path.join(BASE_DIR, "userdata");
Expand Down Expand Up @@ -756,26 +758,28 @@ function shouldEnableAutoUpdates(): boolean {
);
}

async function checkForUpdates(reason: string): Promise<void> {
if (isQuitting || !updaterConfigured || updateCheckInFlight) return;
async function checkForUpdates(reason: string): Promise<boolean> {
if (isQuitting || !updaterConfigured || updateCheckInFlight) return false;
if (updateState.status === "downloading" || updateState.status === "downloaded") {
console.info(
`[desktop-updater] Skipping update check (${reason}) while status=${updateState.status}.`,
);
return;
return false;
}
updateCheckInFlight = true;
setUpdateState(reduceDesktopUpdateStateOnCheckStart(updateState, new Date().toISOString()));
console.info(`[desktop-updater] Checking for updates (${reason})...`);

try {
await autoUpdater.checkForUpdates();
return true;
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
setUpdateState(
reduceDesktopUpdateStateOnCheckFailure(updateState, message, new Date().toISOString()),
);
console.error(`[desktop-updater] Failed to check for updates: ${message}`);
return true;
} finally {
updateCheckInFlight = false;
}
Expand Down Expand Up @@ -1263,6 +1267,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;
}
const checked = await checkForUpdates("web-ui");
return {
checked,
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 GET_WS_URL_CHANNEL = "desktop:get-ws-url";
Expand All @@ -35,6 +36,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
4 changes: 3 additions & 1 deletion apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ import {
getDesktopUpdateButtonTooltip,
isDesktopUpdateButtonDisabled,
resolveDesktopUpdateButtonAction,
shouldShowArm64IntelBuildWarning,
shouldHighlightDesktopUpdateError,
shouldShowArm64IntelBuildWarning,
shouldShowDesktopUpdateButton,
shouldToastDesktopUpdateActionResult,
} from "./desktopUpdate.logic";
Expand Down Expand Up @@ -120,6 +120,7 @@ import {
sortProjectsForSidebar,
sortThreadsForSidebar,
} from "./Sidebar.logic";
import { SidebarUpdatePill } from "./sidebar/SidebarUpdatePill";
import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
import { useSettings, useUpdateSettings } from "~/hooks/useSettings";

Expand Down Expand Up @@ -2006,6 +2007,7 @@ export default function Sidebar() {

<SidebarSeparator />
<SidebarFooter className="p-2">
<SidebarUpdatePill />
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
Expand Down
96 changes: 76 additions & 20 deletions apps/web/src/components/desktopUpdate.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { describe, expect, it } from "vitest";
import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts";

import {
canCheckForUpdate,
getArm64IntelBuildWarningDescription,
getDesktopUpdateActionError,
getDesktopUpdateButtonTooltip,
isDesktopUpdateButtonDisabled,
resolveDesktopUpdateButtonAction,
shouldHighlightDesktopUpdateError,
shouldShowArm64IntelBuildWarning,
shouldShowDesktopUpdateButton,
shouldToastDesktopUpdateActionResult,
Expand Down Expand Up @@ -69,6 +69,16 @@ describe("desktop update button state", () => {
expect(getDesktopUpdateButtonTooltip(state)).toContain("Click to retry");
});

it("prefers install when a downloaded version already exists", () => {
const state: DesktopUpdateState = {
...baseState,
status: "available",
availableVersion: "1.1.0",
downloadedVersion: "1.1.0",
};
expect(resolveDesktopUpdateButtonAction(state)).toBe("install");
});

it("hides the button for non-actionable check errors", () => {
const state: DesktopUpdateState = {
...baseState,
Expand Down Expand Up @@ -169,25 +179,6 @@ describe("desktop update UI helpers", () => {
).toBe(false);
});

it("highlights only actionable updater errors", () => {
expect(
shouldHighlightDesktopUpdateError({
...baseState,
status: "error",
errorContext: "download",
canRetry: true,
}),
).toBe(true);
expect(
shouldHighlightDesktopUpdateError({
...baseState,
status: "error",
errorContext: "check",
canRetry: true,
}),
).toBe(false);
});

it("shows an Apple Silicon warning for Intel builds under Rosetta", () => {
const state: DesktopUpdateState = {
...baseState,
Expand All @@ -214,3 +205,68 @@ 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 false once an update has been downloaded", () => {
expect(
canCheckForUpdate({
...baseState,
status: "downloaded",
availableVersion: "1.1.0",
downloadedVersion: "1.1.0",
}),
).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("getDesktopUpdateButtonTooltip", () => {
it("returns 'Up to date' for non-actionable states", () => {
expect(getDesktopUpdateButtonTooltip({ ...baseState, status: "idle" })).toBe("Up to date");
expect(getDesktopUpdateButtonTooltip({ ...baseState, status: "up-to-date" })).toBe(
"Up to date",
);
});
});
21 changes: 14 additions & 7 deletions apps/web/src/components/desktopUpdate.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@ export type DesktopUpdateButtonAction = "download" | "install" | "none";
export function resolveDesktopUpdateButtonAction(
state: DesktopUpdateState,
): DesktopUpdateButtonAction {
if (state.downloadedVersion) {
return "install";
}
if (state.status === "available") {
return "download";
}
if (state.status === "downloaded") {
return "install";
}
if (state.status === "error") {
if (state.errorContext === "install" && state.downloadedVersion) {
return "install";
}
if (state.errorContext === "download" && state.availableVersion) {
return "download";
}
Expand Down Expand Up @@ -76,7 +73,7 @@ export function getDesktopUpdateButtonTooltip(state: DesktopUpdateState): string
}
return state.message ?? "Update failed";
}
return "Update available";
return "Up to date";
}

export function getDesktopUpdateActionError(result: DesktopUpdateActionResult): string | null {
Expand All @@ -94,3 +91,13 @@ 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 !== "downloaded" &&
state.status !== "disabled"
);
}
Loading
Loading