diff --git a/src/main/updater.ts b/src/main/updater.ts index 84726b012ab..0f6f625304c 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -9,6 +9,7 @@ let currentStatus: UpdateStatus = { state: 'idle' } let userInitiatedCheck = false let onBeforeQuitCleanup: (() => void) | null = null let autoUpdaterInitialized = false +let availableVersion: string | null = null function sendStatus(status: UpdateStatus): void { currentStatus = status @@ -79,7 +80,7 @@ export function setupAutoUpdater( return } - autoUpdater.autoDownload = true + autoUpdater.autoDownload = false autoUpdater.autoInstallOnAppQuit = true // Use allowPrerelease to bypass broken /releases/latest endpoint (returns 406) // and instead parse the version directly from the atom feed which works reliably. @@ -105,6 +106,7 @@ export function setupAutoUpdater( sendStatus({ state: 'not-available', userInitiated: wasUserInitiated || undefined }) return } + availableVersion = info.version sendStatus({ state: 'available', version: info.version }) }) @@ -115,7 +117,11 @@ export function setupAutoUpdater( }) autoUpdater.on('download-progress', (progress) => { - sendStatus({ state: 'downloading', percent: Math.round(progress.percent) }) + sendStatus({ + state: 'downloading', + percent: Math.round(progress.percent), + version: availableVersion ?? '' + }) }) autoUpdater.on('update-downloaded', (info) => { @@ -137,5 +143,17 @@ export function setupAutoUpdater( }) }) - autoUpdater.checkForUpdatesAndNotify() + autoUpdater.checkForUpdates().catch((err) => { + // Startup check — don't bother the user, but log for diagnostics + console.error('[updater] startup check failed:', err?.message ?? err) + }) +} + +export function downloadUpdate(): void { + if (currentStatus.state !== 'available') { + return + } + autoUpdater.downloadUpdate().catch((err) => { + sendStatus({ state: 'error', message: String(err?.message ?? err) }) + }) } diff --git a/src/main/window/attach-main-window-services.ts b/src/main/window/attach-main-window-services.ts index f9b6aaad921..ce5ae82e8fd 100644 --- a/src/main/window/attach-main-window-services.ts +++ b/src/main/window/attach-main-window-services.ts @@ -4,7 +4,13 @@ import type { Store } from '../persistence' import { registerRepoHandlers } from '../ipc/repos' import { registerWorktreeHandlers } from '../ipc/worktrees' import { registerPtyHandlers } from '../ipc/pty' -import { checkForUpdates, getUpdateStatus, quitAndInstall, setupAutoUpdater } from '../updater' +import { + checkForUpdates, + downloadUpdate, + getUpdateStatus, + quitAndInstall, + setupAutoUpdater +} from '../updater' export function attachMainWindowServices(mainWindow: BrowserWindow, store: Store): void { registerRepoHandlers(mainWindow, store) @@ -46,10 +52,12 @@ export function registerUpdaterHandlers(): void { ipcMain.removeHandler('updater:getStatus') ipcMain.removeHandler('updater:getVersion') ipcMain.removeHandler('updater:check') + ipcMain.removeHandler('updater:download') ipcMain.removeHandler('updater:quitAndInstall') ipcMain.handle('updater:getStatus', () => getUpdateStatus()) ipcMain.handle('updater:getVersion', () => app.getVersion()) ipcMain.handle('updater:check', () => checkForUpdates()) + ipcMain.handle('updater:download', () => downloadUpdate()) ipcMain.handle('updater:quitAndInstall', () => quitAndInstall()) } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 1cc5721268a..61112022630 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -99,6 +99,7 @@ type UpdaterApi = { getVersion: () => Promise getStatus: () => Promise check: () => Promise + download: () => Promise quitAndInstall: () => Promise onStatus: (callback: (status: UpdateStatus) => void) => () => void } diff --git a/src/preload/index.ts b/src/preload/index.ts index 0b3a5412a22..4375968f394 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -192,6 +192,7 @@ const api = { getStatus: (): Promise => ipcRenderer.invoke('updater:getStatus'), getVersion: (): Promise => ipcRenderer.invoke('updater:getVersion'), check: (): Promise => ipcRenderer.invoke('updater:check'), + download: (): Promise => ipcRenderer.invoke('updater:download'), quitAndInstall: (): Promise => ipcRenderer.invoke('updater:quitAndInstall'), onStatus: (callback: (status: unknown) => void): (() => void) => { const listener = (_event: Electron.IpcRendererEvent, status: unknown) => callback(status) diff --git a/src/renderer/src/components/settings/GeneralPane.tsx b/src/renderer/src/components/settings/GeneralPane.tsx index 08c86af8361..3f21f72d6c1 100644 --- a/src/renderer/src/components/settings/GeneralPane.tsx +++ b/src/renderer/src/components/settings/GeneralPane.tsx @@ -161,7 +161,17 @@ export function GeneralPane({ Check for Updates - {updateStatus.state === 'downloaded' ? ( + {updateStatus.state === 'available' ? ( + + ) : updateStatus.state === 'downloaded' ? ( diff --git a/src/renderer/src/hooks/useIpcEvents.ts b/src/renderer/src/hooks/useIpcEvents.ts index 0af1fd2d3c6..fd61a183b4b 100644 --- a/src/renderer/src/hooks/useIpcEvents.ts +++ b/src/renderer/src/hooks/useIpcEvents.ts @@ -34,6 +34,7 @@ export function useIpcEvents(): void { }) let checkingToastId: string | number | undefined + let availableToastId: string | number | undefined unsubs.push( window.api.updater.onStatus((raw) => { const status = raw as UpdateStatus @@ -52,7 +53,23 @@ export function useIpcEvents(): void { toast.dismiss(checkingToastId) } checkingToastId = undefined + availableToastId = toast.info(`Version ${status.version} is available.`, { + duration: Infinity, + action: { + label: 'Install', + onClick: () => window.api.updater.download() + } + }) + } else if (status.state === 'downloading') { + if (availableToastId) { + toast.dismiss(availableToastId) + availableToastId = undefined + } } else if (status.state === 'downloaded') { + if (availableToastId) { + toast.dismiss(availableToastId) + availableToastId = undefined + } toast.success(`Version ${status.version} is ready to install.`, { duration: Infinity, action: { diff --git a/src/shared/types.ts b/src/shared/types.ts index f01c74126a0..3126ef88714 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -145,7 +145,7 @@ export type UpdateStatus = | { state: 'checking'; userInitiated?: boolean } | { state: 'available'; version: string } | { state: 'not-available'; userInitiated?: boolean } - | { state: 'downloading'; percent: number } + | { state: 'downloading'; percent: number; version: string } | { state: 'downloaded'; version: string } | { state: 'error'; message: string; userInitiated?: boolean }