From 25978d1313267fef5e439099a1b79375179ebd8e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:59:53 +1100 Subject: [PATCH 1/2] fix(desktop): explain why the update button is disabled When automatic updates are off (dev builds, T3CODE_DISABLE_AUTO_UPDATE, unsupported Linux installs), the sidebar update button was greyed out and its hover still said "Check for updates". The desktop app already computed the reason, but only the app-menu dialog showed it. DesktopUpdates now puts that reason into DesktopUpdateState.message when it builds the disabled state. The sidebar hover reads "Updates unavailable" with the reason, and the Settings About version row shows the reason, since its disabled button cannot show a tooltip. Co-Authored-By: Claude Opus 5.5 --- .../src/updates/DesktopUpdates.test.ts | 5 +++ apps/desktop/src/updates/DesktopUpdates.ts | 18 +++++----- .../components/desktopUpdate.logic.test.ts | 36 +++++++++++++++++++ .../web/src/components/desktopUpdate.logic.ts | 21 +++++++++++ .../components/settings/SettingsPanels.tsx | 4 ++- .../components/sidebar/SidebarUpdatePill.tsx | 16 +++++++-- 6 files changed, 87 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index 9745662c1209..4ab809b5e734 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -100,9 +100,14 @@ describe("DesktopUpdates", () => { const deb = yield* linuxState("deb\n"); assert.equal(deb.status, "idle"); + assert.equal(deb.message, null); const unmarked = yield* linuxState(undefined); assert.equal(unmarked.status, "disabled"); + assert.equal( + unmarked.message, + "Automatic updates on Linux require the AppImage or the .deb package.", + ); }), ); diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index 245925a6e731..910ded851026 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -213,13 +213,15 @@ function parseAppUpdateYml(raw: string): Effect.Effect, environment: DesktopEnvironment.DesktopEnvironment["Service"], ): DesktopUpdateState { + const enabled = Option.isNone(disabledReason); return { ...createInitialDesktopUpdateState(environment.appVersion, environment.runtimeInfo, channel), enabled, status: enabled ? "idle" : "disabled", + message: Option.getOrNull(disabledReason), }; } @@ -404,8 +406,6 @@ export const make = Effect.gen(function* () { }); }); - const shouldEnableAutoUpdates = resolveDisabledReason.pipe(Effect.map(Option.isNone)); - const checkForUpdates = Effect.fn("desktop.updates.checkForUpdates")(function* ( reason: string, actionReservation: "acquire" | "held" = "acquire", @@ -923,9 +923,9 @@ export const make = Effect.gen(function* () { } const settings = yield* desktopSettings.get; - const enabled = yield* shouldEnableAutoUpdates; - yield* setState(createBaseUpdateState(settings.updateChannel, enabled, environment)); - if (!enabled) { + const disabledReason = yield* resolveDisabledReason; + yield* setState(createBaseUpdateState(settings.updateChannel, disabledReason, environment)); + if (Option.isSome(disabledReason)) { return; } yield* Ref.set(updaterConfiguredRef, true); @@ -994,10 +994,10 @@ export const make = Effect.gen(function* () { ), ); - const enabled = yield* shouldEnableAutoUpdates; - yield* setState(createBaseUpdateState(nextChannel, enabled, environment)); + const disabledReason = yield* resolveDisabledReason; + yield* setState(createBaseUpdateState(nextChannel, disabledReason, environment)); - if (!enabled || !(yield* Ref.get(updaterConfiguredRef))) { + if (Option.isSome(disabledReason) || !(yield* Ref.get(updaterConfiguredRef))) { return yield* Ref.get(updateStateRef); } diff --git a/apps/web/src/components/desktopUpdate.logic.test.ts b/apps/web/src/components/desktopUpdate.logic.test.ts index fcc97825b681..21036688ffd9 100644 --- a/apps/web/src/components/desktopUpdate.logic.test.ts +++ b/apps/web/src/components/desktopUpdate.logic.test.ts @@ -6,6 +6,8 @@ import { getArm64IntelBuildWarningDescription, getDesktopUpdateActionError, getDesktopUpdateButtonTooltip, + getDesktopUpdateDisabledReason, + getDesktopUpdateIdleTooltip, getDesktopUpdateInstallConfirmationMessage, getDesktopUpdateReleaseHistoryUrl, getDesktopUpdateReleaseUrl, @@ -347,3 +349,37 @@ describe("getDesktopUpdateButtonTooltip", () => { ); }); }); + +describe("disabled update reason", () => { + const disabledState: DesktopUpdateState = { + ...baseState, + enabled: false, + status: "disabled", + message: "Automatic updates are only available in packaged production builds.", + }; + + it("explains why checking is unavailable", () => { + expect(getDesktopUpdateDisabledReason(disabledState)).toBe( + "Automatic updates are only available in packaged production builds.", + ); + expect(getDesktopUpdateIdleTooltip(disabledState)).toEqual({ + title: "Updates unavailable", + detail: "Automatic updates are only available in packaged production builds.", + }); + }); + + it("falls back to a generic reason when the desktop app sent none", () => { + expect(getDesktopUpdateDisabledReason({ ...disabledState, message: null })).toBe( + "Automatic updates are not available in this build.", + ); + }); + + it("keeps the check prompt when updates are available", () => { + expect(getDesktopUpdateDisabledReason(baseState)).toBeNull(); + expect(getDesktopUpdateDisabledReason(null)).toBeNull(); + expect(getDesktopUpdateIdleTooltip(baseState)).toEqual({ + title: "Check for updates", + detail: null, + }); + }); +}); diff --git a/apps/web/src/components/desktopUpdate.logic.ts b/apps/web/src/components/desktopUpdate.logic.ts index 4a169cb3ef40..04c929546d48 100644 --- a/apps/web/src/components/desktopUpdate.logic.ts +++ b/apps/web/src/components/desktopUpdate.logic.ts @@ -121,3 +121,24 @@ export function canCheckForUpdate(state: DesktopUpdateState | null): boolean { state.status !== "checking" && state.status !== "downloading" && state.status !== "disabled" ); } + +/** + * Why update checks are off, or null when they are available. The desktop app puts + * the reason it computed into `message` when it builds the disabled state. + */ +export function getDesktopUpdateDisabledReason( + state: Pick | null, +): string | null { + if (!state || (state.enabled && state.status !== "disabled")) return null; + return state.message?.trim() || "Automatic updates are not available in this build."; +} + +/** Hover text for the update button when there is nothing to download or install. */ +export function getDesktopUpdateIdleTooltip( + state: Pick | null, +): { readonly title: string; readonly detail: string | null } { + const disabledReason = getDesktopUpdateDisabledReason(state); + return disabledReason + ? { title: "Updates unavailable", detail: disabledReason } + : { title: "Check for updates", detail: null }; +} diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 131e8539282c..ab8990ac10b4 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -59,6 +59,7 @@ import { CliCommandSettingsRow } from "./CliCommandSettingsRow"; import { canCheckForUpdate, getDesktopUpdateButtonTooltip, + getDesktopUpdateDisabledReason, getDesktopUpdateInstallConfirmationMessage, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, @@ -420,10 +421,11 @@ function AboutVersionSection() { }; const buttonLabel = actionLabel[action] ?? statusLabel[updateState?.status ?? ""] ?? "Check for Updates"; + // A disabled button has pointer-events: none, so the row explains why checks are off. const description = action === "download" || action === "install" ? "Update available." - : "Current version of the application."; + : (getDesktopUpdateDisabledReason(updateState) ?? "Current version of the application."); return ( <> diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx index a1c82d3d51f3..c5094ee791ae 100644 --- a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx +++ b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx @@ -13,6 +13,7 @@ import { getArm64IntelBuildWarningDescription, getDesktopUpdateActionError, getDesktopUpdateButtonTooltip, + getDesktopUpdateIdleTooltip, getDesktopUpdateInstallConfirmationMessage, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, @@ -143,13 +144,15 @@ function SidebarUpdateControl() { isDownloading, showCheckIcon, }); + const idleTooltip = + !showUpdateDetails && !showCheckIcon ? getDesktopUpdateIdleTooltip(state) : null; const tooltip = showUpdateDetails ? state ? getDesktopUpdateButtonTooltip(state) : "Update available" : showCheckIcon ? "Checking for updates…" - : "Check for updates"; + : (idleTooltip?.title ?? "Check for updates"); const disabled = showCheckIcon ? true : showUpdateDetails @@ -300,7 +303,7 @@ function SidebarUpdateControl() { const updateButton = (