| author | |
| committer | |
| log | 304a14de7d6f0b9977c8ecc4bced5b5f359ce02e |
| tree | ebdb7308e27c2e02ffa9c3591b539517b27a97e8 |
| parent | c585128bbe0311615c58921909467f49600cb85f |
| signature | Signed by PGP key B5690EEEBB952194 |
10 files changed, 224 insertions(+), 11 deletions(-)
apps/desktop/src/main.ts+17-2| ... | @@ -31,6 +31,7 @@ import { | ... | @@ -31,6 +31,7 @@ import { |
| 31 | reduceDesktopUpdateStateOnNoUpdate, | 31 | reduceDesktopUpdateStateOnNoUpdate, |
| 32 | reduceDesktopUpdateStateOnUpdateAvailable, | 32 | reduceDesktopUpdateStateOnUpdateAvailable, |
| 33 | } from "./updateMachine"; | 33 | } from "./updateMachine"; |
| 34 | import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch"; | ||
| 34 | 35 | ||
| 35 | fixPath(); | 36 | fixPath(); |
| 36 | 37 | ||
| ... | @@ -80,7 +81,13 @@ let backendLogSink: RotatingFileSink | null = null; | ... | @@ -80,7 +81,13 @@ let backendLogSink: RotatingFileSink | null = null; |
| 80 | let restoreStdIoCapture: (() => void) | null = null; | 81 | let restoreStdIoCapture: (() => void) | null = null; |
| 81 | 82 | ||
| 82 | let destructiveMenuIconCache: Electron.NativeImage | null | undefined; | 83 | let destructiveMenuIconCache: Electron.NativeImage | null | undefined; |
| 83 | const initialUpdateState = (): DesktopUpdateState => createInitialDesktopUpdateState(app.getVersion()); | 84 | const desktopRuntimeInfo = resolveDesktopRuntimeInfo({ |
| 85 | platform: process.platform, | ||
| 86 | processArch: process.arch, | ||
| 87 | runningUnderArm64Translation: app.runningUnderARM64Translation === true, | ||
| 88 | }); | ||
| 89 | const initialUpdateState = (): DesktopUpdateState => | ||
| 90 | createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo); | ||
| 84 | 91 | ||
| 85 | function logTimestamp(): string { | 92 | function logTimestamp(): string { |
| 86 | return new Date().toISOString(); | 93 | return new Date().toISOString(); |
| ... | @@ -708,6 +715,7 @@ async function downloadAvailableUpdate(): Promise<{ accepted: boolean; completed | ... | @@ -708,6 +715,7 @@ async function downloadAvailableUpdate(): Promise<{ accepted: boolean; completed |
| 708 | } | 715 | } |
| 709 | updateDownloadInFlight = true; | 716 | updateDownloadInFlight = true; |
| 710 | setUpdateState(reduceDesktopUpdateStateOnDownloadStart(updateState)); | 717 | setUpdateState(reduceDesktopUpdateStateOnDownloadStart(updateState)); |
| 718 | autoUpdater.disableDifferentialDownload = isArm64HostRunningIntelBuild(desktopRuntimeInfo); | ||
| 711 | console.info("[desktop-updater] Downloading update..."); | 719 | console.info("[desktop-updater] Downloading update..."); |
| 712 | 720 | ||
| 713 | try { | 721 | try { |
| ... | @@ -746,7 +754,7 @@ async function installDownloadedUpdate(): Promise<{ accepted: boolean; completed | ... | @@ -746,7 +754,7 @@ async function installDownloadedUpdate(): Promise<{ accepted: boolean; completed |
| 746 | function configureAutoUpdater(): void { | 754 | function configureAutoUpdater(): void { |
| 747 | const enabled = shouldEnableAutoUpdates(); | 755 | const enabled = shouldEnableAutoUpdates(); |
| 748 | setUpdateState({ | 756 | setUpdateState({ |
| 749 | ...createInitialDesktopUpdateState(app.getVersion()), | 757 | ...createInitialDesktopUpdateState(app.getVersion(), desktopRuntimeInfo), |
| 750 | enabled, | 758 | enabled, |
| 751 | status: enabled ? "idle" : "disabled", | 759 | status: enabled ? "idle" : "disabled", |
| 752 | }); | 760 | }); |
| ... | @@ -780,8 +788,15 @@ function configureAutoUpdater(): void { | ... | @@ -780,8 +788,15 @@ function configureAutoUpdater(): void { |
| 780 | autoUpdater.channel = DESKTOP_UPDATE_CHANNEL; | 788 | autoUpdater.channel = DESKTOP_UPDATE_CHANNEL; |
| 781 | autoUpdater.allowPrerelease = DESKTOP_UPDATE_ALLOW_PRERELEASE; | 789 | autoUpdater.allowPrerelease = DESKTOP_UPDATE_ALLOW_PRERELEASE; |
| 782 | autoUpdater.allowDowngrade = false; | 790 | autoUpdater.allowDowngrade = false; |
| 791 | autoUpdater.disableDifferentialDownload = isArm64HostRunningIntelBuild(desktopRuntimeInfo); | ||
| 783 | let lastLoggedDownloadMilestone = -1; | 792 | let lastLoggedDownloadMilestone = -1; |
| 784 | 793 | ||
| 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 | |||
| 785 | autoUpdater.on("checking-for-update", () => { | 800 | autoUpdater.on("checking-for-update", () => { |
| 786 | console.info("[desktop-updater] Looking for updates..."); | 801 | console.info("[desktop-updater] Looking for updates..."); |
| 787 | }); | 802 | }); |
apps/desktop/src/runtimeArch.test.ts created+49| ... | @@ -0,0 +1,49 @@ | ||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runtimeArch"; | ||
| 4 | |||
| 5 | describe("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 @@ | ||
| 1 | import type { DesktopRuntimeArch, DesktopRuntimeInfo } from "@t3tools/contracts"; | ||
| 2 | |||
| 3 | interface ResolveDesktopRuntimeInfoInput { | ||
| 4 | readonly platform: NodeJS.Platform; | ||
| 5 | readonly processArch: string; | ||
| 6 | readonly runningUnderArm64Translation: boolean; | ||
| 7 | } | ||
| 8 | |||
| 9 | function normalizeDesktopArch(arch: string): DesktopRuntimeArch { | ||
| 10 | if (arch === "arm64") return "arm64"; | ||
| 11 | if (arch === "x64") return "x64"; | ||
| 12 | return "other"; | ||
| 13 | } | ||
| 14 | |||
| 15 | export 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 | |||
| 38 | export 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 { | ... | @@ -13,11 +13,17 @@ import { |
| 13 | reduceDesktopUpdateStateOnUpdateAvailable, | 13 | reduceDesktopUpdateStateOnUpdateAvailable, |
| 14 | } from "./updateMachine"; | 14 | } from "./updateMachine"; |
| 15 | 15 | ||
| 16 | const runtimeInfo = { | ||
| 17 | hostArch: "x64", | ||
| 18 | appArch: "x64", | ||
| 19 | runningUnderArm64Translation: false, | ||
| 20 | } as const; | ||
| 21 | |||
| 16 | describe("updateMachine", () => { | 22 | describe("updateMachine", () => { |
| 17 | it("clears transient errors when a check starts", () => { | 23 | it("clears transient errors when a check starts", () => { |
| 18 | const state = reduceDesktopUpdateStateOnCheckStart( | 24 | const state = reduceDesktopUpdateStateOnCheckStart( |
| 19 | { | 25 | { |
| 20 | ...createInitialDesktopUpdateState("1.0.0"), | 26 | ...createInitialDesktopUpdateState("1.0.0", runtimeInfo), |
| 21 | enabled: true, | 27 | enabled: true, |
| 22 | status: "error", | 28 | status: "error", |
| 23 | message: "network", | 29 | message: "network", |
| ... | @@ -36,7 +42,7 @@ describe("updateMachine", () => { | ... | @@ -36,7 +42,7 @@ describe("updateMachine", () => { |
| 36 | it("records a check failure without exposing an action", () => { | 42 | it("records a check failure without exposing an action", () => { |
| 37 | const state = reduceDesktopUpdateStateOnCheckFailure( | 43 | const state = reduceDesktopUpdateStateOnCheckFailure( |
| 38 | { | 44 | { |
| 39 | ...createInitialDesktopUpdateState("1.0.0"), | 45 | ...createInitialDesktopUpdateState("1.0.0", runtimeInfo), |
| 40 | enabled: true, | 46 | enabled: true, |
| 41 | status: "checking", | 47 | status: "checking", |
| 42 | }, | 48 | }, |
| ... | @@ -52,7 +58,7 @@ describe("updateMachine", () => { | ... | @@ -52,7 +58,7 @@ describe("updateMachine", () => { |
| 52 | it("preserves available version on download failure for retry", () => { | 58 | it("preserves available version on download failure for retry", () => { |
| 53 | const state = reduceDesktopUpdateStateOnDownloadFailure( | 59 | const state = reduceDesktopUpdateStateOnDownloadFailure( |
| 54 | { | 60 | { |
| 55 | ...createInitialDesktopUpdateState("1.0.0"), | 61 | ...createInitialDesktopUpdateState("1.0.0", runtimeInfo), |
| 56 | enabled: true, | 62 | enabled: true, |
| 57 | status: "downloading", | 63 | status: "downloading", |
| 58 | availableVersion: "1.1.0", | 64 | availableVersion: "1.1.0", |
| ... | @@ -70,7 +76,7 @@ describe("updateMachine", () => { | ... | @@ -70,7 +76,7 @@ describe("updateMachine", () => { |
| 70 | it("transitions to downloaded and then preserves install retry state", () => { | 76 | it("transitions to downloaded and then preserves install retry state", () => { |
| 71 | const downloaded = reduceDesktopUpdateStateOnDownloadComplete( | 77 | const downloaded = reduceDesktopUpdateStateOnDownloadComplete( |
| 72 | { | 78 | { |
| 73 | ...createInitialDesktopUpdateState("1.0.0"), | 79 | ...createInitialDesktopUpdateState("1.0.0", runtimeInfo), |
| 74 | enabled: true, | 80 | enabled: true, |
| 75 | status: "downloading", | 81 | status: "downloading", |
| 76 | availableVersion: "1.1.0", | 82 | availableVersion: "1.1.0", |
| ... | @@ -89,7 +95,7 @@ describe("updateMachine", () => { | ... | @@ -89,7 +95,7 @@ describe("updateMachine", () => { |
| 89 | it("clears stale download state when no update is available", () => { | 95 | it("clears stale download state when no update is available", () => { |
| 90 | const state = reduceDesktopUpdateStateOnNoUpdate( | 96 | const state = reduceDesktopUpdateStateOnNoUpdate( |
| 91 | { | 97 | { |
| 92 | ...createInitialDesktopUpdateState("1.0.0"), | 98 | ...createInitialDesktopUpdateState("1.0.0", runtimeInfo), |
| 93 | enabled: true, | 99 | enabled: true, |
| 94 | status: "error", | 100 | status: "error", |
| 95 | availableVersion: "1.1.0", | 101 | availableVersion: "1.1.0", |
| ... | @@ -111,7 +117,7 @@ describe("updateMachine", () => { | ... | @@ -111,7 +117,7 @@ describe("updateMachine", () => { |
| 111 | it("tracks available, download start, and progress cleanly", () => { | 117 | it("tracks available, download start, and progress cleanly", () => { |
| 112 | const available = reduceDesktopUpdateStateOnUpdateAvailable( | 118 | const available = reduceDesktopUpdateStateOnUpdateAvailable( |
| 113 | { | 119 | { |
| 114 | ...createInitialDesktopUpdateState("1.0.0"), | 120 | ...createInitialDesktopUpdateState("1.0.0", runtimeInfo), |
| 115 | enabled: true, | 121 | enabled: true, |
| 116 | status: "checking", | 122 | status: "checking", |
| 117 | }, | 123 | }, |
apps/desktop/src/updateMachine.ts+8-3| ... | @@ -1,12 +1,18 @@ | ... | @@ -1,12 +1,18 @@ |
| 1 | import type { DesktopUpdateState } from "@t3tools/contracts"; | 1 | import type { DesktopRuntimeInfo, DesktopUpdateState } from "@t3tools/contracts"; |
| 2 | 2 | ||
| 3 | import { getCanRetryAfterDownloadFailure, nextStatusAfterDownloadFailure } from "./updateState"; | 3 | import { getCanRetryAfterDownloadFailure, nextStatusAfterDownloadFailure } from "./updateState"; |
| 4 | 4 | ||
| 5 | export function createInitialDesktopUpdateState(currentVersion: string): DesktopUpdateState { | 5 | export function createInitialDesktopUpdateState( |
| 6 | currentVersion: string, | ||
| 7 | runtimeInfo: DesktopRuntimeInfo, | ||
| 8 | ): DesktopUpdateState { | ||
| 6 | return { | 9 | return { |
| 7 | enabled: false, | 10 | enabled: false, |
| 8 | status: "disabled", | 11 | status: "disabled", |
| 9 | currentVersion, | 12 | currentVersion, |
| 13 | hostArch: runtimeInfo.hostArch, | ||
| 14 | appArch: runtimeInfo.appArch, | ||
| 15 | runningUnderArm64Translation: runtimeInfo.runningUnderArm64Translation, | ||
| 10 | availableVersion: null, | 16 | availableVersion: null, |
| 11 | downloadedVersion: null, | 17 | downloadedVersion: null, |
| 12 | downloadPercent: null, | 18 | downloadPercent: null, |
| ... | @@ -152,4 +158,3 @@ export function reduceDesktopUpdateStateOnInstallFailure( | ... | @@ -152,4 +158,3 @@ export function reduceDesktopUpdateStateOnInstallFailure( |
| 152 | canRetry: true, | 158 | canRetry: true, |
| 153 | }; | 159 | }; |
| 154 | } | 160 | } |
| 155 |
apps/desktop/src/updateState.test.ts+3| ... | @@ -12,6 +12,9 @@ const baseState: DesktopUpdateState = { | ... | @@ -12,6 +12,9 @@ const baseState: DesktopUpdateState = { |
| 12 | enabled: true, | 12 | enabled: true, |
| 13 | status: "idle", | 13 | status: "idle", |
| 14 | currentVersion: "1.0.0", | 14 | currentVersion: "1.0.0", |
| 15 | hostArch: "x64", | ||
| 16 | appArch: "x64", | ||
| 17 | runningUnderArm64Translation: false, | ||
| 15 | availableVersion: null, | 18 | availableVersion: null, |
| 16 | downloadedVersion: null, | 19 | downloadedVersion: null, |
| 17 | downloadPercent: null, | 20 | downloadPercent: null, |
apps/web/src/components/Sidebar.tsx+34| ... | @@ -8,6 +8,7 @@ import { | ... | @@ -8,6 +8,7 @@ import { |
| 8 | SettingsIcon, | 8 | SettingsIcon, |
| 9 | SquarePenIcon, | 9 | SquarePenIcon, |
| 10 | TerminalIcon, | 10 | TerminalIcon, |
| 11 | TriangleAlertIcon, | ||
| 11 | } from "lucide-react"; | 12 | } from "lucide-react"; |
| 12 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | 13 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 13 | import { | 14 | import { |
| ... | @@ -36,14 +37,18 @@ import { type DraftThreadEnvMode, useComposerDraftStore } from "../composerDraft | ... | @@ -36,14 +37,18 @@ import { type DraftThreadEnvMode, useComposerDraftStore } from "../composerDraft |
| 36 | import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; | 37 | import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; |
| 37 | import { toastManager } from "./ui/toast"; | 38 | import { toastManager } from "./ui/toast"; |
| 38 | import { | 39 | import { |
| 40 | getArm64IntelBuildWarningDescription, | ||
| 39 | getDesktopUpdateActionError, | 41 | getDesktopUpdateActionError, |
| 40 | getDesktopUpdateButtonTooltip, | 42 | getDesktopUpdateButtonTooltip, |
| 41 | isDesktopUpdateButtonDisabled, | 43 | isDesktopUpdateButtonDisabled, |
| 42 | resolveDesktopUpdateButtonAction, | 44 | resolveDesktopUpdateButtonAction, |
| 45 | shouldShowArm64IntelBuildWarning, | ||
| 43 | shouldHighlightDesktopUpdateError, | 46 | shouldHighlightDesktopUpdateError, |
| 44 | shouldShowDesktopUpdateButton, | 47 | shouldShowDesktopUpdateButton, |
| 45 | shouldToastDesktopUpdateActionResult, | 48 | shouldToastDesktopUpdateActionResult, |
| 46 | } from "./desktopUpdate.logic"; | 49 | } from "./desktopUpdate.logic"; |
| 50 | import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert"; | ||
| 51 | import { Button } from "./ui/button"; | ||
| 47 | import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible"; | 52 | import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible"; |
| 48 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; | 53 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 49 | import { | 54 | import { |
| ... | @@ -887,6 +892,12 @@ export default function Sidebar() { | ... | @@ -887,6 +892,12 @@ export default function Sidebar() { |
| 887 | const desktopUpdateButtonAction = desktopUpdateState | 892 | const desktopUpdateButtonAction = desktopUpdateState |
| 888 | ? resolveDesktopUpdateButtonAction(desktopUpdateState) | 893 | ? resolveDesktopUpdateButtonAction(desktopUpdateState) |
| 889 | : "none"; | 894 | : "none"; |
| 895 | const showArm64IntelBuildWarning = | ||
| 896 | isElectron && shouldShowArm64IntelBuildWarning(desktopUpdateState); | ||
| 897 | const arm64IntelBuildWarningDescription = | ||
| 898 | desktopUpdateState && showArm64IntelBuildWarning | ||
| 899 | ? getArm64IntelBuildWarningDescription(desktopUpdateState) | ||
| 900 | : null; | ||
| 890 | const desktopUpdateButtonInteractivityClasses = desktopUpdateButtonDisabled | 901 | const desktopUpdateButtonInteractivityClasses = desktopUpdateButtonDisabled |
| 891 | ? "cursor-not-allowed opacity-60" | 902 | ? "cursor-not-allowed opacity-60" |
| 892 | : "hover:bg-accent hover:text-foreground"; | 903 | : "hover:bg-accent hover:text-foreground"; |
| ... | @@ -1030,6 +1041,29 @@ export default function Sidebar() { | ... | @@ -1030,6 +1041,29 @@ export default function Sidebar() { |
| 1030 | )} | 1041 | )} |
| 1031 | 1042 | ||
| 1032 | <SidebarContent className="gap-0"> | 1043 | <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} | ||
| 1033 | <SidebarGroup className="px-2 py-2"> | 1067 | <SidebarGroup className="px-2 py-2"> |
| 1034 | <div className="mb-1 flex items-center justify-between px-2"> | 1068 | <div className="mb-1 flex items-center justify-between px-2"> |
| 1035 | <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60"> | 1069 | <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"; | ... | @@ -2,11 +2,13 @@ import { describe, expect, it } from "vitest"; |
| 2 | import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts"; | 2 | import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts"; |
| 3 | 3 | ||
| 4 | import { | 4 | import { |
| 5 | getArm64IntelBuildWarningDescription, | ||
| 5 | getDesktopUpdateActionError, | 6 | getDesktopUpdateActionError, |
| 6 | getDesktopUpdateButtonTooltip, | 7 | getDesktopUpdateButtonTooltip, |
| 7 | isDesktopUpdateButtonDisabled, | 8 | isDesktopUpdateButtonDisabled, |
| 8 | resolveDesktopUpdateButtonAction, | 9 | resolveDesktopUpdateButtonAction, |
| 9 | shouldHighlightDesktopUpdateError, | 10 | shouldHighlightDesktopUpdateError, |
| 11 | shouldShowArm64IntelBuildWarning, | ||
| 10 | shouldShowDesktopUpdateButton, | 12 | shouldShowDesktopUpdateButton, |
| 11 | shouldToastDesktopUpdateActionResult, | 13 | shouldToastDesktopUpdateActionResult, |
| 12 | } from "./desktopUpdate.logic"; | 14 | } from "./desktopUpdate.logic"; |
| ... | @@ -15,6 +17,9 @@ const baseState: DesktopUpdateState = { | ... | @@ -15,6 +17,9 @@ const baseState: DesktopUpdateState = { |
| 15 | enabled: true, | 17 | enabled: true, |
| 16 | status: "idle", | 18 | status: "idle", |
| 17 | currentVersion: "1.0.0", | 19 | currentVersion: "1.0.0", |
| 20 | hostArch: "x64", | ||
| 21 | appArch: "x64", | ||
| 22 | runningUnderArm64Translation: false, | ||
| 18 | availableVersion: null, | 23 | availableVersion: null, |
| 19 | downloadedVersion: null, | 24 | downloadedVersion: null, |
| 20 | downloadPercent: null, | 25 | downloadPercent: null, |
| ... | @@ -171,4 +176,30 @@ describe("desktop update UI helpers", () => { | ... | @@ -171,4 +176,30 @@ describe("desktop update UI helpers", () => { |
| 171 | }), | 176 | }), |
| 172 | ).toBe(false); | 177 | ).toBe(false); |
| 173 | }); | 178 | }); |
| 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 | }); | ||
| 174 | }); | 205 | }); |
apps/web/src/components/desktopUpdate.logic.ts+19| ... | @@ -32,10 +32,29 @@ export function shouldShowDesktopUpdateButton(state: DesktopUpdateState | null): | ... | @@ -32,10 +32,29 @@ export function shouldShowDesktopUpdateButton(state: DesktopUpdateState | null): |
| 32 | return resolveDesktopUpdateButtonAction(state) !== "none"; | 32 | return resolveDesktopUpdateButtonAction(state) !== "none"; |
| 33 | } | 33 | } |
| 34 | 34 | ||
| 35 | export function shouldShowArm64IntelBuildWarning(state: DesktopUpdateState | null): boolean { | ||
| 36 | return state?.hostArch === "arm64" && state.appArch === "x64"; | ||
| 37 | } | ||
| 38 | |||
| 35 | export function isDesktopUpdateButtonDisabled(state: DesktopUpdateState | null): boolean { | 39 | export function isDesktopUpdateButtonDisabled(state: DesktopUpdateState | null): boolean { |
| 36 | return state?.status === "downloading"; | 40 | return state?.status === "downloading"; |
| 37 | } | 41 | } |
| 38 | 42 | ||
| 43 | export 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 | |||
| 39 | export function getDesktopUpdateButtonTooltip(state: DesktopUpdateState): string { | 58 | export function getDesktopUpdateButtonTooltip(state: DesktopUpdateState): string { |
| 40 | if (state.status === "available") { | 59 | if (state.status === "available") { |
| 41 | return `Update ${state.availableVersion ?? "available"} ready to download`; | 60 | return `Update ${state.availableVersion ?? "available"} ready to download`; |
packages/contracts/src/ipc.ts+11| ... | @@ -58,10 +58,21 @@ export type DesktopUpdateStatus = | ... | @@ -58,10 +58,21 @@ export type DesktopUpdateStatus = |
| 58 | | "downloaded" | 58 | | "downloaded" |
| 59 | | "error"; | 59 | | "error"; |
| 60 | 60 | ||
| 61 | export type DesktopRuntimeArch = "arm64" | "x64" | "other"; | ||
| 62 | |||
| 63 | export interface DesktopRuntimeInfo { | ||
| 64 | hostArch: DesktopRuntimeArch; | ||
| 65 | appArch: DesktopRuntimeArch; | ||
| 66 | runningUnderArm64Translation: boolean; | ||
| 67 | } | ||
| 68 | |||
| 61 | export interface DesktopUpdateState { | 69 | export interface DesktopUpdateState { |
| 62 | enabled: boolean; | 70 | enabled: boolean; |
| 63 | status: DesktopUpdateStatus; | 71 | status: DesktopUpdateStatus; |
| 64 | currentVersion: string; | 72 | currentVersion: string; |
| 73 | hostArch: DesktopRuntimeArch; | ||
| 74 | appArch: DesktopRuntimeArch; | ||
| 75 | runningUnderArm64Translation: boolean; | ||
| 65 | availableVersion: string | null; | 76 | availableVersion: string | null; |
| 66 | downloadedVersion: string | null; | 77 | downloadedVersion: string | null; |
| 67 | downloadPercent: number | null; | 78 | downloadPercent: number | null; |