From 48b0d22ddbbb81bdc33765da47b4e8e151d7f8c8 Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Sun, 15 Mar 2026 14:24:50 -0400 Subject: [PATCH 1/4] feat: make check for updates menu item dynamic --- apps/desktop/src/main.ts | 83 ++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index c3dba6016e..838e013c41 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -14,6 +14,7 @@ import { nativeTheme, protocol, shell, + MenuItem, } from "electron"; import type { MenuItemConstructorOptions } from "electron"; import * as Effect from "effect/Effect"; @@ -277,6 +278,8 @@ let updateCheckInFlight = false; let updateDownloadInFlight = false; let updaterConfigured = false; let updateState: DesktopUpdateState = initialUpdateState(); +const updateStateListeners = new Set<(state: DesktopUpdateState) => void>(); +updateStateListeners.add(() => emitUpdateState()); function resolveUpdaterErrorContext(): DesktopUpdateErrorContext { if (updateDownloadInFlight) return "download"; @@ -559,18 +562,69 @@ async function checkForUpdatesFromMenu(): Promise { } } +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DEFAULT = "Check for Updates..."; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_CHECKING = "Checking for Updates..."; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_IDLE = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DEFAULT; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DISABLED = "Updates unavailable"; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADING = "Downloading update..."; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADED = "Update downloaded"; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_AVAILABLE = "Update available"; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_UP_TO_DATE = "You're up to date!"; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_ERROR = "Update check failed"; +const checkForUpdatesMenuItem: MenuItem = new MenuItem({ + label: CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DEFAULT, + click: async () => await handleCheckForUpdatesMenuClick(), +}); + +// TODO: Only the enabled status is actually dynamic here. Wait for upstream to allow for dynamic label updates. +updateStateListeners.add((state) => { + switch (state.status) { + case "checking": + checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_CHECKING; + checkForUpdatesMenuItem.enabled = false; + break; + case "available": + checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_AVAILABLE; + checkForUpdatesMenuItem.enabled = false; + break; + case "downloading": + checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADING; + checkForUpdatesMenuItem.enabled = false; + break; + case "downloaded": + checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADED; + checkForUpdatesMenuItem.enabled = false; + break; + case "disabled": + checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DISABLED; + checkForUpdatesMenuItem.enabled = false; + break; + case "error": + checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_ERROR; + checkForUpdatesMenuItem.enabled = false; + break; + case "up-to-date": + checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_UP_TO_DATE; + checkForUpdatesMenuItem.enabled = false; + break; + case "idle": + checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_IDLE; + checkForUpdatesMenuItem.enabled = true; + break; + } +}); + +let applicationMenu: Menu | null = null; + function configureApplicationMenu(): void { - const template: MenuItemConstructorOptions[] = []; + const template: (MenuItemConstructorOptions | MenuItem)[] = []; if (process.platform === "darwin") { template.push({ label: app.name, - submenu: [ + submenu: Menu.buildFromTemplate([ { role: "about" }, - { - label: "Check for Updates...", - click: () => handleCheckForUpdatesMenuClick(), - }, + checkForUpdatesMenuItem, { type: "separator" }, { label: "Settings...", @@ -585,7 +639,7 @@ function configureApplicationMenu(): void { { role: "unhide" }, { type: "separator" }, { role: "quit" }, - ], + ]), }); } @@ -625,16 +679,13 @@ function configureApplicationMenu(): void { { role: "windowMenu" }, { role: "help", - submenu: [ - { - label: "Check for Updates...", - click: () => handleCheckForUpdatesMenuClick(), - }, - ], + // TODO: Is it safe to use the same menu item for both the root menu and the help menu? + submenu: Menu.buildFromTemplate([checkForUpdatesMenuItem]), }, ); - Menu.setApplicationMenu(Menu.buildFromTemplate(template)); + applicationMenu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(applicationMenu); } function resolveResourcePath(fileName: string): string | null { @@ -727,7 +778,9 @@ function emitUpdateState(): void { function setUpdateState(patch: Partial): void { updateState = { ...updateState, ...patch }; - emitUpdateState(); + for (const listener of updateStateListeners) { + listener(updateState); + } } function shouldEnableAutoUpdates(): boolean { From 1fefd647bbd5aa9292347deadd237e92060cf93a Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Tue, 17 Mar 2026 16:58:34 -0400 Subject: [PATCH 2/4] fix: bump electron version so this actually works --- apps/desktop/package.json | 2 +- bun.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0754c0d1c8..553d527971 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "effect": "catalog:", - "electron": "40.6.0", + "electron": "40.7.0", "electron-updater": "^6.6.2" }, "devDependencies": { diff --git a/bun.lock b/bun.lock index b8e36149f7..334eaeb79e 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ "version": "0.0.10", "dependencies": { "effect": "catalog:", - "electron": "40.6.0", + "electron": "40.7.0", "electron-updater": "^6.6.2", }, "devDependencies": { @@ -1012,7 +1012,7 @@ "effect": ["effect@https://pkg.pr.new/Effect-TS/effect-smol/effect@8881a9b", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }], - "electron": ["electron@40.6.0", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-ett8W+yOFGDuM0vhJMamYSkrbV3LoaffzJd9GfjI96zRAxyrNqUSKqBpf/WGbQCweDxX2pkUCUfrv4wwKpsFZA=="], + "electron": ["electron@40.7.0", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-oQe76S/3V1rcb0+i45hAxnCH8udkRZSaHUNwglzNAEKbB94LSJ1qwbFo8+uRc2UsYZgCqSIMRcyX40GyOkD+Xw=="], "electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="], From e27a67aba83c341ca9669a3866ce69f68b856d89 Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Tue, 17 Mar 2026 17:08:19 -0400 Subject: [PATCH 3/4] style: remove inaccurate todo --- apps/desktop/src/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 838e013c41..b7303c756b 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -576,7 +576,6 @@ const checkForUpdatesMenuItem: MenuItem = new MenuItem({ click: async () => await handleCheckForUpdatesMenuClick(), }); -// TODO: Only the enabled status is actually dynamic here. Wait for upstream to allow for dynamic label updates. updateStateListeners.add((state) => { switch (state.status) { case "checking": From 7b1d5e192a9f8e31645ba958d9d1e72a5eb77409 Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Tue, 17 Mar 2026 17:11:29 -0400 Subject: [PATCH 4/4] refactor: address remaining todo by separating out help and app menu update menu items --- apps/desktop/src/main.ts | 56 +++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index b7303c756b..2556fc3fc0 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -571,46 +571,55 @@ const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADED = "Update downloaded"; const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_AVAILABLE = "Update available"; const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_UP_TO_DATE = "You're up to date!"; const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_ERROR = "Update check failed"; -const checkForUpdatesMenuItem: MenuItem = new MenuItem({ - label: CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DEFAULT, - click: async () => await handleCheckForUpdatesMenuClick(), -}); +function makeCheckForUpdatesMenuItem(): MenuItem { + return new MenuItem({ + label: CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DEFAULT, + click: async () => await handleCheckForUpdatesMenuClick(), + }); +} +const checkForUpdatesMenuItemInAppMenu = makeCheckForUpdatesMenuItem(); +const checkForUpdatesMenuItemInHelpMenu = makeCheckForUpdatesMenuItem(); -updateStateListeners.add((state) => { +function updateCheckForUpdatesMenuItem(menuItem: MenuItem, state: DesktopUpdateState): void { switch (state.status) { case "checking": - checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_CHECKING; - checkForUpdatesMenuItem.enabled = false; + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_CHECKING; + menuItem.enabled = false; break; case "available": - checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_AVAILABLE; - checkForUpdatesMenuItem.enabled = false; + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_AVAILABLE; + menuItem.enabled = false; break; case "downloading": - checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADING; - checkForUpdatesMenuItem.enabled = false; + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADING; + menuItem.enabled = false; break; case "downloaded": - checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADED; - checkForUpdatesMenuItem.enabled = false; + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADED; + menuItem.enabled = false; break; case "disabled": - checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DISABLED; - checkForUpdatesMenuItem.enabled = false; + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DISABLED; + menuItem.enabled = false; break; case "error": - checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_ERROR; - checkForUpdatesMenuItem.enabled = false; + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_ERROR; + menuItem.enabled = false; break; case "up-to-date": - checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_UP_TO_DATE; - checkForUpdatesMenuItem.enabled = false; + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_UP_TO_DATE; + menuItem.enabled = false; break; case "idle": - checkForUpdatesMenuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_IDLE; - checkForUpdatesMenuItem.enabled = true; + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_IDLE; + menuItem.enabled = true; break; } +} + +updateStateListeners.add((state) => { + updateCheckForUpdatesMenuItem(checkForUpdatesMenuItemInAppMenu, state); + updateCheckForUpdatesMenuItem(checkForUpdatesMenuItemInHelpMenu, state); }); let applicationMenu: Menu | null = null; @@ -623,7 +632,7 @@ function configureApplicationMenu(): void { label: app.name, submenu: Menu.buildFromTemplate([ { role: "about" }, - checkForUpdatesMenuItem, + checkForUpdatesMenuItemInAppMenu, { type: "separator" }, { label: "Settings...", @@ -678,8 +687,7 @@ function configureApplicationMenu(): void { { role: "windowMenu" }, { role: "help", - // TODO: Is it safe to use the same menu item for both the root menu and the help menu? - submenu: Menu.buildFromTemplate([checkForUpdatesMenuItem]), + submenu: Menu.buildFromTemplate([checkForUpdatesMenuItemInHelpMenu]), }, );