1import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts";
2
3export type DesktopUpdateButtonAction = "download" | "install" | "none";
4
5const DESKTOP_RELEASE_HISTORY_URL = "https://github.com/pingdotgg/t3code/releases";
6const DESKTOP_RELEASE_TAG_URL = `${DESKTOP_RELEASE_HISTORY_URL}/tag`;
7
8/**
9 * The main process fills `downloadedVersion` from the updater's `update-downloaded`
10 * event, which is dispatched on its own fiber. A download RPC can therefore resolve
11 * before that write lands, so fall back to the version the download was started for.
12 */
13export function getDesktopUpdateDownloadedVersion(state: DesktopUpdateState): string | null {
14 return state.downloadedVersion ?? state.availableVersion;
15}
16
17/** Release notes for an exact downloaded build; nightly suffixes are part of the tag. */
18export function getDesktopUpdateReleaseUrl(version: string | null): string | null {
19 const normalizedVersion = version?.trim();
20 if (!normalizedVersion) return null;
21 return `${DESKTOP_RELEASE_TAG_URL}/v${encodeURIComponent(normalizedVersion)}`;
22}
23
24export function getDesktopUpdateReleaseHistoryUrl(): string {
25 return DESKTOP_RELEASE_HISTORY_URL;
26}
27
28export function resolveDesktopUpdateButtonAction(
29 state: DesktopUpdateState,
30): DesktopUpdateButtonAction {
31 if (
32 state.downloadedVersion &&
33 (state.status === "downloaded" ||
34 (state.status === "error" &&
35 (state.errorContext === null || state.errorContext === "install")))
36 ) {
37 return "install";
38 }
39 if (state.status === "available") {
40 return "download";
41 }
42 if (state.status === "error") {
43 if (state.errorContext === "download" && state.availableVersion) {
44 return "download";
45 }
46 }
47 return "none";
48}
49
50export function shouldShowArm64IntelBuildWarning(state: DesktopUpdateState | null): boolean {
51 return state?.hostArch === "arm64" && state.appArch === "x64";
52}
53
54export function isDesktopUpdateButtonDisabled(state: DesktopUpdateState | null): boolean {
55 return state?.status === "downloading";
56}
57
58export function getArm64IntelBuildWarningDescription(state: DesktopUpdateState): string {
59 if (!shouldShowArm64IntelBuildWarning(state)) {
60 return "This install is using the correct architecture.";
61 }
62
63 const action = resolveDesktopUpdateButtonAction(state);
64 if (action === "download") {
65 return "This Mac has Apple Silicon, but The Knife is still running the Intel build under Rosetta. Download the available update to switch to the native Apple Silicon build.";
66 }
67 if (action === "install") {
68 return "This Mac has Apple Silicon, but The Knife is still running the Intel build under Rosetta. Restart to install the downloaded Apple Silicon build.";
69 }
70 return "This Mac has Apple Silicon, but The Knife is still running the Intel build under Rosetta. The next app update will replace it with the native Apple Silicon build.";
71}
72
73export function getDesktopUpdateButtonTooltip(state: DesktopUpdateState): string {
74 if (
75 state.notAppliedVersion !== undefined &&
76 (state.status === "available" || state.status === "downloaded") &&
77 getDesktopUpdateDownloadedVersion(state) === state.notAppliedVersion
78 ) {
79 return `Update ${state.notAppliedVersion} did not apply. Click to retry.`;
80 }
81 if (state.status === "available") {
82 return `Update ${state.availableVersion ?? "available"} ready to download`;
83 }
84 if (state.status === "downloading") {
85 const progress =
86 typeof state.downloadPercent === "number" ? ` (${Math.floor(state.downloadPercent)}%)` : "";
87 return `Downloading update${progress}`;
88 }
89 if (state.status === "downloaded") {
90 return state.installKind === "ui"
91 ? `UI update ${state.downloadedVersion ?? state.availableVersion ?? "ready"} downloaded. Reload to apply without interrupting running tasks.`
92 : `Update ${state.downloadedVersion ?? state.availableVersion ?? "ready"} downloaded. Click to restart and install.`;
93 }
94 if (state.status === "error") {
95 if (state.errorContext === "download" && state.availableVersion) {
96 return `Download failed for ${state.availableVersion}. Click to retry.`;
97 }
98 if (state.errorContext === "install" && state.downloadedVersion) {
99 return `Install failed for ${state.downloadedVersion}. Click to retry.`;
100 }
101 if (state.downloadedVersion) {
102 return `Update ${state.downloadedVersion} downloaded. Click to restart and install.`;
103 }
104 return state.message ?? "Update failed";
105 }
106 return "Up to date";
107}
108
109export function getDesktopUpdateInstallConfirmationMessage(
110 state: Pick<DesktopUpdateState, "availableVersion" | "downloadedVersion">,
111): string {
112 const version = state.downloadedVersion ?? state.availableVersion;
113 return `Install update${version ? ` ${version}` : ""} and restart The Knife?\n\nAny running tasks will be interrupted. Make sure you're ready before continuing.`;
114}
115
116export function getDesktopUpdateActionError(result: DesktopUpdateActionResult): string | null {
117 if (!result.accepted || result.completed) return null;
118 if (typeof result.state.message !== "string") return null;
119 const message = result.state.message.trim();
120 return message.length > 0 ? message : null;
121}
122
123export function shouldToastDesktopUpdateActionResult(result: DesktopUpdateActionResult): boolean {
124 return getDesktopUpdateActionError(result) !== null;
125}
126
127export function canCheckForUpdate(state: DesktopUpdateState | null): boolean {
128 if (!state || !state.enabled) return false;
129 return (
130 state.status !== "checking" && state.status !== "downloading" && state.status !== "disabled"
131 );
132}