authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-03-09 04:01:23-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-09 04:01:23-07:00
log304a14de7d6f0b9977c8ecc4bced5b5f359ce02e
treeebdb7308e27c2e02ffa9c3591b539517b27a97e8
parentc585128bbe0311615c58921909467f49600cb85f
signature Signed by PGP key B5690EEEBB952194

Fix users who installed intel builds on arm macs (#641)


10 files changed, 224 insertions(+), 11 deletions(-)

apps/desktop/src/main.ts+17-2
......@@ -31,6 +31,7 @@ import {
3131 reduceDesktopUpdateStateOnNoUpdate,
3232 reduceDesktopUpdateStateOnUpdateAvailable,
3333} from "./updateMachine";
34import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch";
3435
3536fixPath();
3637
......@@ -80,7 +81,13 @@ let backendLogSink: RotatingFileSink | null = null;
8081let restoreStdIoCapture: (() => void) | null = null;
8182
8283let destructiveMenuIconCache: Electron.NativeImage | null | undefined;
83const initialUpdateState = (): DesktopUpdateState => createInitialDesktopUpdateState(app.getVersion());
84const desktopRuntimeInfo = resolveDesktopRuntimeInfo({
85 platform: process.platform,
86 processArch: process.arch,
87 runningUnderArm64Translation: app.runningUnderARM64Translation === true,
88});
89const initialUpdateState = (): DesktopUpdateState =>
90 createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo);
8491
8592function logTimestamp(): string {
8693 return new Date().toISOString();
......@@ -708,6 +715,7 @@ async function downloadAvailableUpdate(): Promise<{ accepted: boolean; completed
708715 }
709716 updateDownloadInFlight = true;
710717 setUpdateState(reduceDesktopUpdateStateOnDownloadStart(updateState));
718 autoUpdater.disableDifferentialDownload = isArm64HostRunningIntelBuild(desktopRuntimeInfo);
711719 console.info("[desktop-updater] Downloading update...");
712720
713721 try {
......@@ -746,7 +754,7 @@ async function installDownloadedUpdate(): Promise<{ accepted: boolean; completed
746754function configureAutoUpdater(): void {
747755 const enabled = shouldEnableAutoUpdates();
748756 setUpdateState({
749 ...createInitialDesktopUpdateState(app.getVersion()),
757 ...createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo),
750758 enabled,
751759 status: enabled ? "idle" : "disabled",
752760 });
......@@ -780,8 +788,15 @@ function configureAutoUpdater(): void {
780788 autoUpdater.channel = DESKTOP_UPDATE_CHANNEL;
781789 autoUpdater.allowPrerelease = DESKTOP_UPDATE_ALLOW_PRERELEASE;
782790 autoUpdater.allowDowngrade = false;
791 autoUpdater.disableDifferentialDownload = isArm64HostRunningIntelBuild(desktopRuntimeInfo);
783792 let lastLoggedDownloadMilestone = -1;
784793
794 if (isArm64HostRunningIntelBuild(desktopRuntimeInfo)) {
795 console.info(
796 "[desktop-updater] Apple Silicon host detected while running Intel build; updates will switch to arm64 packages.",
797 );
798 }
799
785800 autoUpdater.on("checking-for-update", () => {
786801 console.info("[desktop-updater] Looking for updates...");
787802 });
apps/desktop/src/runtimeArch.test.ts created+49
......@@ -0,0 +1,49 @@
1import { describe, expect, it } from "vitest";
2
3import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch";
4
5describe("resolveDesktopRuntimeInfo", () => {
6 it("detects Rosetta-translated Intel builds on Apple Silicon", () => {
7 const runtimeInfo = resolveDesktopRuntimeInfo({
8 platform: "darwin",
9 processArch: "x64",
10 runningUnderArm64Translation: true,
11 });
12
13 expect(runtimeInfo).toEqual({
14 hostArch: "arm64",
15 appArch: "x64",
16 runningUnderArm64Translation: true,
17 });
18 expect(isArm64HostRunningIntelBuild(runtimeInfo)).toBe(true);
19 });
20
21 it("detects native Apple Silicon builds", () => {
22 const runtimeInfo = resolveDesktopRuntimeInfo({
23 platform: "darwin",
24 processArch: "arm64",
25 runningUnderArm64Translation: false,
26 });
27
28 expect(runtimeInfo).toEqual({
29 hostArch: "arm64",
30 appArch: "arm64",
31 runningUnderArm64Translation: false,
32 });
33 expect(isArm64HostRunningIntelBuild(runtimeInfo)).toBe(false);
34 });
35
36 it("passes through non-mac builds without translation", () => {
37 const runtimeInfo = resolveDesktopRuntimeInfo({
38 platform: "linux",
39 processArch: "x64",
40 runningUnderArm64Translation: true,
41 });
42
43 expect(runtimeInfo).toEqual({
44 hostArch: "x64",
45 appArch: "x64",
46 runningUnderArm64Translation: false,
47 });
48 });
49});
apps/desktop/src/runtimeArch.ts created+40
......@@ -0,0 +1,40 @@
1import type { DesktopRuntimeArch, DesktopRuntimeInfo } from "@t3tools/contracts";
2
3interface ResolveDesktopRuntimeInfoInput {
4 readonly platform: NodeJS.Platform;
5 readonly processArch: string;
6 readonly runningUnderArm64Translation: boolean;
7}
8
9function normalizeDesktopArch(arch: string): DesktopRuntimeArch {
10 if (arch === "arm64") return "arm64";
11 if (arch === "x64") return "x64";
12 return "other";
13}
14
15export function resolveDesktopRuntimeInfo(
16 input: ResolveDesktopRuntimeInfoInput,
17): DesktopRuntimeInfo {
18 const appArch = normalizeDesktopArch(input.processArch);
19
20 if (input.platform !== "darwin") {
21 return {
22 hostArch: appArch,
23 appArch,
24 runningUnderArm64Translation: false,
25 };
26 }
27
28 const hostArch =
29 appArch === "arm64" || input.runningUnderArm64Translation ? "arm64" : appArch;
30
31 return {
32 hostArch,
33 appArch,
34 runningUnderArm64Translation: input.runningUnderArm64Translation,
35 };
36}
37
38export function isArm64HostRunningIntelBuild(runtimeInfo: DesktopRuntimeInfo): boolean {
39 return runtimeInfo.hostArch === "arm64" && runtimeInfo.appArch === "x64";
40}
apps/desktop/src/updateMachine.test.ts+12-6
......@@ -13,11 +13,17 @@ import {
1313 reduceDesktopUpdateStateOnUpdateAvailable,
1414} from "./updateMachine";
1515
16const runtimeInfo = {
17 hostArch: "x64",
18 appArch: "x64",
19 runningUnderArm64Translation: false,
20} as const;
21
1622describe("updateMachine", () => {
1723 it("clears transient errors when a check starts", () => {
1824 const state = reduceDesktopUpdateStateOnCheckStart(
1925 {
20 ...createInitialDesktopUpdateState("1.0.0"),
26 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo),
2127 enabled: true,
2228 status: "error",
2329 message: "network",
......@@ -36,7 +42,7 @@ describe("updateMachine", () => {
3642 it("records a check failure without exposing an action", () => {
3743 const state = reduceDesktopUpdateStateOnCheckFailure(
3844 {
39 ...createInitialDesktopUpdateState("1.0.0"),
45 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo),
4046 enabled: true,
4147 status: "checking",
4248 },
......@@ -52,7 +58,7 @@ describe("updateMachine", () => {
5258 it("preserves available version on download failure for retry", () => {
5359 const state = reduceDesktopUpdateStateOnDownloadFailure(
5460 {
55 ...createInitialDesktopUpdateState("1.0.0"),
61 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo),
5662 enabled: true,
5763 status: "downloading",
5864 availableVersion: "1.1.0",
......@@ -70,7 +76,7 @@ describe("updateMachine", () => {
7076 it("transitions to downloaded and then preserves install retry state", () => {
7177 const downloaded = reduceDesktopUpdateStateOnDownloadComplete(
7278 {
73 ...createInitialDesktopUpdateState("1.0.0"),
79 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo),
7480 enabled: true,
7581 status: "downloading",
7682 availableVersion: "1.1.0",
......@@ -89,7 +95,7 @@ describe("updateMachine", () => {
8995 it("clears stale download state when no update is available", () => {
9096 const state = reduceDesktopUpdateStateOnNoUpdate(
9197 {
92 ...createInitialDesktopUpdateState("1.0.0"),
98 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo),
9399 enabled: true,
94100 status: "error",
95101 availableVersion: "1.1.0",
......@@ -111,7 +117,7 @@ describe("updateMachine", () => {
111117 it("tracks available, download start, and progress cleanly", () => {
112118 const available = reduceDesktopUpdateStateOnUpdateAvailable(
113119 {
114 ...createInitialDesktopUpdateState("1.0.0"),
120 ...createInitialDesktopUpdateState("1.0.0", runtimeInfo),
115121 enabled: true,
116122 status: "checking",
117123 },
apps/desktop/src/updateMachine.ts+8-3
......@@ -1,12 +1,18 @@
1import type { DesktopUpdateState } from "@t3tools/contracts";
1import type { DesktopRuntimeInfo, DesktopUpdateState } from "@t3tools/contracts";
22
33import { getCanRetryAfterDownloadFailure, nextStatusAfterDownloadFailure } from "./updateState";
44
5export function createInitialDesktopUpdateState(currentVersion: string): DesktopUpdateState {
5export function createInitialDesktopUpdateState(
6 currentVersion: string,
7 runtimeInfo: DesktopRuntimeInfo,
8): DesktopUpdateState {
69 return {
710 enabled: false,
811 status: "disabled",
912 currentVersion,
13 hostArch: runtimeInfo.hostArch,
14 appArch: runtimeInfo.appArch,
15 runningUnderArm64Translation: runtimeInfo.runningUnderArm64Translation,
1016 availableVersion: null,
1117 downloadedVersion: null,
1218 downloadPercent: null,
......@@ -152,4 +158,3 @@ export function reduceDesktopUpdateStateOnInstallFailure(
152158 canRetry: true,
153159 };
154160}
155
apps/desktop/src/updateState.test.ts+3
......@@ -12,6 +12,9 @@ const baseState: DesktopUpdateState = {
1212 enabled: true,
1313 status: "idle",
1414 currentVersion: "1.0.0",
15 hostArch: "x64",
16 appArch: "x64",
17 runningUnderArm64Translation: false,
1518 availableVersion: null,
1619 downloadedVersion: null,
1720 downloadPercent: null,
apps/web/src/components/Sidebar.tsx+34
......@@ -8,6 +8,7 @@ import {
88 SettingsIcon,
99 SquarePenIcon,
1010 TerminalIcon,
11 TriangleAlertIcon,
1112} from "lucide-react";
1213import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1314import {
......@@ -36,14 +37,18 @@ import { type DraftThreadEnvMode, useComposerDraftStore } from "../composerDraft
3637import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
3738import { toastManager } from "./ui/toast";
3839import {
40 getArm64IntelBuildWarningDescription,
3941 getDesktopUpdateActionError,
4042 getDesktopUpdateButtonTooltip,
4143 isDesktopUpdateButtonDisabled,
4244 resolveDesktopUpdateButtonAction,
45 shouldShowArm64IntelBuildWarning,
4346 shouldHighlightDesktopUpdateError,
4447 shouldShowDesktopUpdateButton,
4548 shouldToastDesktopUpdateActionResult,
4649} from "./desktopUpdate.logic";
50import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert";
51import { Button } from "./ui/button";
4752import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible";
4853import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
4954import {
......@@ -887,6 +892,12 @@ export default function Sidebar() {
887892 const desktopUpdateButtonAction = desktopUpdateState
888893 ? resolveDesktopUpdateButtonAction(desktopUpdateState)
889894 : "none";
895 const showArm64IntelBuildWarning =
896 isElectron && shouldShowArm64IntelBuildWarning(desktopUpdateState);
897 const arm64IntelBuildWarningDescription =
898 desktopUpdateState && showArm64IntelBuildWarning
899 ? getArm64IntelBuildWarningDescription(desktopUpdateState)
900 : null;
890901 const desktopUpdateButtonInteractivityClasses = desktopUpdateButtonDisabled
891902 ? "cursor-not-allowed opacity-60"
892903 : "hover:bg-accent hover:text-foreground";
......@@ -1030,6 +1041,29 @@ export default function Sidebar() {
10301041 )}
10311042
10321043 <SidebarContent className="gap-0">
1044 {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? (
1045 <SidebarGroup className="px-2 pt-2 pb-0">
1046 <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8">
1047 <TriangleAlertIcon />
1048 <AlertTitle>Intel build on Apple Silicon</AlertTitle>
1049 <AlertDescription>{arm64IntelBuildWarningDescription}</AlertDescription>
1050 {desktopUpdateButtonAction !== "none" ? (
1051 <AlertAction>
1052 <Button
1053 size="xs"
1054 variant="outline"
1055 disabled={desktopUpdateButtonDisabled}
1056 onClick={handleDesktopUpdateButtonClick}
1057 >
1058 {desktopUpdateButtonAction === "download"
1059 ? "Download ARM build"
1060 : "Install ARM build"}
1061 </Button>
1062 </AlertAction>
1063 ) : null}
1064 </Alert>
1065 </SidebarGroup>
1066 ) : null}
10331067 <SidebarGroup className="px-2 py-2">
10341068 <div className="mb-1 flex items-center justify-between px-2">
10351069 <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60">
apps/web/src/components/desktopUpdate.logic.test.ts+31
......@@ -2,11 +2,13 @@ import { describe, expect, it } from "vitest";
22import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts";
33
44import {
5 getArm64IntelBuildWarningDescription,
56 getDesktopUpdateActionError,
67 getDesktopUpdateButtonTooltip,
78 isDesktopUpdateButtonDisabled,
89 resolveDesktopUpdateButtonAction,
910 shouldHighlightDesktopUpdateError,
11 shouldShowArm64IntelBuildWarning,
1012 shouldShowDesktopUpdateButton,
1113 shouldToastDesktopUpdateActionResult,
1214} from "./desktopUpdate.logic";
......@@ -15,6 +17,9 @@ const baseState: DesktopUpdateState = {
1517 enabled: true,
1618 status: "idle",
1719 currentVersion: "1.0.0",
20 hostArch: "x64",
21 appArch: "x64",
22 runningUnderArm64Translation: false,
1823 availableVersion: null,
1924 downloadedVersion: null,
2025 downloadPercent: null,
......@@ -171,4 +176,30 @@ describe("desktop update UI helpers", () => {
171176 }),
172177 ).toBe(false);
173178 });
179
180 it("shows an Apple Silicon warning for Intel builds under Rosetta", () => {
181 const state: DesktopUpdateState = {
182 ...baseState,
183 hostArch: "arm64",
184 appArch: "x64",
185 runningUnderArm64Translation: true,
186 };
187
188 expect(shouldShowArm64IntelBuildWarning(state)).toBe(true);
189 expect(getArm64IntelBuildWarningDescription(state)).toContain("Apple Silicon");
190 expect(getArm64IntelBuildWarningDescription(state)).toContain("Intel build");
191 });
192
193 it("changes the warning copy when a native build update is ready to download", () => {
194 const state: DesktopUpdateState = {
195 ...baseState,
196 hostArch: "arm64",
197 appArch: "x64",
198 runningUnderArm64Translation: true,
199 status: "available",
200 availableVersion: "1.1.0",
201 };
202
203 expect(getArm64IntelBuildWarningDescription(state)).toContain("Download the available update");
204 });
174205});
apps/web/src/components/desktopUpdate.logic.ts+19
......@@ -32,10 +32,29 @@ export function shouldShowDesktopUpdateButton(state: DesktopUpdateState | null):
3232 return resolveDesktopUpdateButtonAction(state) !== "none";
3333}
3434
35export function shouldShowArm64IntelBuildWarning(state: DesktopUpdateState | null): boolean {
36 return state?.hostArch === "arm64" && state.appArch === "x64";
37}
38
3539export function isDesktopUpdateButtonDisabled(state: DesktopUpdateState | null): boolean {
3640 return state?.status === "downloading";
3741}
3842
43export function getArm64IntelBuildWarningDescription(state: DesktopUpdateState): string {
44 if (!shouldShowArm64IntelBuildWarning(state)) {
45 return "This install is using the correct architecture.";
46 }
47
48 const action = resolveDesktopUpdateButtonAction(state);
49 if (action === "download") {
50 return "This Mac has Apple Silicon, but T3 Code is still running the Intel build under Rosetta. Download the available update to switch to the native Apple Silicon build.";
51 }
52 if (action === "install") {
53 return "This Mac has Apple Silicon, but T3 Code is still running the Intel build under Rosetta. Restart to install the downloaded Apple Silicon build.";
54 }
55 return "This Mac has Apple Silicon, but T3 Code is still running the Intel build under Rosetta. The next app update will replace it with the native Apple Silicon build.";
56}
57
3958export function getDesktopUpdateButtonTooltip(state: DesktopUpdateState): string {
4059 if (state.status === "available") {
4160 return `Update ${state.availableVersion ?? "available"} ready to download`;
packages/contracts/src/ipc.ts+11
......@@ -58,10 +58,21 @@ export type DesktopUpdateStatus =
5858 | "downloaded"
5959 | "error";
6060
61export type DesktopRuntimeArch = "arm64" | "x64" | "other";
62
63export interface DesktopRuntimeInfo {
64 hostArch: DesktopRuntimeArch;
65 appArch: DesktopRuntimeArch;
66 runningUnderArm64Translation: boolean;
67}
68
6169export interface DesktopUpdateState {
6270 enabled: boolean;
6371 status: DesktopUpdateStatus;
6472 currentVersion: string;
73 hostArch: DesktopRuntimeArch;
74 appArch: DesktopRuntimeArch;
75 runningUnderArm64Translation: boolean;
6576 availableVersion: string | null;
6677 downloadedVersion: string | null;
6778 downloadPercent: number | null;