From 7f2d0280806a0d1411bf3fd5aa5a6ddf614f0407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 16 Apr 2026 22:52:16 -0400 Subject: [PATCH] fix(updater): only toast 'Up to Date' on manual user-initiated checks --- src/main/ipc-core-common.ts | 2 +- src/main/ipc-core-read-handlers.ts | 4 ++- src/main/updater-service.ts | 54 ++++++++++++++++++++++++++---- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/main/ipc-core-common.ts b/src/main/ipc-core-common.ts index dbc7fad7..131491f7 100644 --- a/src/main/ipc-core-common.ts +++ b/src/main/ipc-core-common.ts @@ -28,7 +28,7 @@ export interface RegisterCoreHandlersDependencies { checkConnector: (id: ConnectorId) => Promise; openConnectorAuthTerminal: (id: ConnectorId) => Promise; openGenericTerminal: (input: { commandKey: string }) => Promise; - checkForUpdates: () => Promise; + checkForUpdates: (options?: { manual?: boolean }) => Promise; getWorkerProcess: () => { postMessage: (message: unknown) => void } | null; resetAfterRestore: () => void; getApiPort: () => number; diff --git a/src/main/ipc-core-read-handlers.ts b/src/main/ipc-core-read-handlers.ts index a8ac4d81..d6739da6 100644 --- a/src/main/ipc-core-read-handlers.ts +++ b/src/main/ipc-core-read-handlers.ts @@ -91,7 +91,9 @@ export function registerCoreReadHandlers({ }); registerHandle(IPC_CHANNELS.checkForUpdates, async () => { - return ok(await checkForUpdates()); + // Invocation via IPC only happens when the user opens Settings and clicks + // "Check for updates". Pass manual:true so the Up-to-Date toast fires. + return ok(await checkForUpdates({ manual: true })); }); registerHandle(IPC_CHANNELS.getSidebarBadges, async (_event, companyId) => { diff --git a/src/main/updater-service.ts b/src/main/updater-service.ts index 52c22f91..1d449bfd 100644 --- a/src/main/updater-service.ts +++ b/src/main/updater-service.ts @@ -14,12 +14,38 @@ interface UpdaterServiceDependencies { notify: (options: NotificationOptions) => void; } +type SkipUpdateReason = "updates_disabled" | "insecure_url" | "dev_without_real_url"; + +function shouldSkipUpdateCheck(input: { + isPackaged: boolean; + updatesEnabled: boolean; + updateUrl: string | undefined; + updatesDisabledEnv: boolean; +}): SkipUpdateReason | null { + if (!input.updatesEnabled || input.updatesDisabledEnv) { + return "updates_disabled"; + } + if (input.updateUrl && !input.updateUrl.startsWith("https://")) { + return "insecure_url"; + } + if (!input.isPackaged && (!input.updateUrl || input.updateUrl.includes("example.invalid"))) { + return "dev_without_real_url"; + } + return null; +} + export function createUpdaterService({ db, logger, notify, }: UpdaterServiceDependencies) { let listenersAttached = false; + /** + * Set by checkForUpdates({ manual: true }) from the UI. Gates the + * "Up to Date" toast so the auto-every-24h check stays silent when + * nothing happened. Cleared inside the update-not-available handler. + */ + let manualCheckPending = false; function attachListeners() { if (listenersAttached) { @@ -31,6 +57,7 @@ export function createUpdaterService({ }); autoUpdater.on("update-available", (info) => { logger.info("[updater] update available", info.version); + manualCheckPending = false; notify({ title: "Update Available", body: `Version ${info.version} is downloading in the background`, @@ -55,6 +82,10 @@ export function createUpdaterService({ }); }); autoUpdater.on("update-not-available", () => { + if (!manualCheckPending) { + return; + } + manualCheckPending = false; notify({ title: "Up to Date", body: "You are running the latest version", @@ -64,6 +95,7 @@ export function createUpdaterService({ }); autoUpdater.on("error", (error) => { logger.error("[updater] error", error); + manualCheckPending = false; notify({ title: "Update Check Failed", body: "Could not check for updates. Try again later.", @@ -73,20 +105,29 @@ export function createUpdaterService({ }); } - async function checkForUpdates() { + async function checkForUpdates(options: { manual?: boolean } = {}) { const snapshot = db.listSnapshot(); - if (!snapshot.updatesEnabled || process.env.AGENTCOMPANY_DISABLE_UPDATES === "true") { - return false; - } const updateUrl = process.env.AGENTCOMPANY_UPDATE_URL?.trim(); - if (updateUrl && !updateUrl.startsWith("https://")) { + const skipReason = shouldSkipUpdateCheck({ + isPackaged: app.isPackaged, + updatesEnabled: snapshot.updatesEnabled, + updateUrl, + updatesDisabledEnv: process.env.AGENTCOMPANY_DISABLE_UPDATES === "true", + }); + if (skipReason === "insecure_url") { logger.error("[updater] AGENTCOMPANY_UPDATE_URL must use HTTPS — refusing insecure update source."); return false; } - if (!app.isPackaged && (!updateUrl || updateUrl.includes("example.invalid"))) { + if (skipReason === "dev_without_real_url") { logger.info("[updater] Skipping update check because no real update URL is configured."); return false; } + if (skipReason) { + return false; + } + if (options.manual) { + manualCheckPending = true; + } try { autoUpdater.logger = { info: (...args: unknown[]) => logger.info(...args), @@ -100,6 +141,7 @@ export function createUpdaterService({ return Boolean(result?.updateInfo); } catch (error) { logger.error(error); + manualCheckPending = false; return false; } }