diff --git a/desktop/src/main/index.ts b/desktop/src/main/index.ts index d47052d4..5f9d1301 100644 --- a/desktop/src/main/index.ts +++ b/desktop/src/main/index.ts @@ -253,7 +253,14 @@ function setupIPC() { setUpdateLanguage(lang) startDownload() }) - ipcMain.handle('update-install', () => quitAndInstall()) + ipcMain.handle('update-install', () => { + // Let the window actually close so the app can fully quit — otherwise the + // close-to-tray handler preventDefault()s it, the process stays alive, and + // Squirrel.Mac can't swap the app bundle (the update silently no-ops and + // relaunching still shows the old version). + isQuitting = true + quitAndInstall() + }) // Synchronous OS locale lookup (e.g. "zh-CN", "en-US"). Used by the renderer // to pick a sensible default UI language on first run before any paint. diff --git a/desktop/src/main/updater.ts b/desktop/src/main/updater.ts index 048e165a..ff286e73 100644 --- a/desktop/src/main/updater.ts +++ b/desktop/src/main/updater.ts @@ -229,6 +229,16 @@ function attemptDownload(): void { export function quitAndInstall(): void { if (!app.isPackaged) return log('quitAndInstall: relaunching to install update') - // isSilent=false (show installer), isForceRunAfter=true (relaunch after). - autoUpdater.quitAndInstall(false, true) + // isSilent MUST be true on Windows: our NSIS installer is an "assisted" + // installer (oneClick:false + allowToChangeInstallationDirectory), so a + // non-silent update re-shows the install-dir / install-mode wizard instead of + // updating in place. Silent skips the wizard and updates directly. macOS + // (Squirrel.Mac) ignores isSilent, so false there is fine. + // isForceRunAfter=true relaunches the app once the install finishes. + // Drop window-all-closed handlers first: with an assisted NSIS installer a + // lingering handler can keep the process alive and stop the installer from + // replacing files / relaunching (a documented electron-updater gotcha). + app.removeAllListeners('window-all-closed') + const isSilent = process.platform === 'win32' + autoUpdater.quitAndInstall(isSilent, true) } diff --git a/desktop/src/renderer/src/components/UpdateBanner.tsx b/desktop/src/renderer/src/components/UpdateBanner.tsx index b51d1e8b..bdd45aff 100644 --- a/desktop/src/renderer/src/components/UpdateBanner.tsx +++ b/desktop/src/renderer/src/components/UpdateBanner.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Download, RefreshCw, X, Loader2 } from 'lucide-react' +import { Download, RefreshCw, X, Loader2, AlertTriangle } from 'lucide-react' import { t } from '../i18n' import { useUpdateStore, hasAvailableUpdate } from '../store/updateStore' @@ -12,9 +12,12 @@ const UpdateBanner: React.FC = () => { const available = hasAvailableUpdate(state) const status = state.status + const errored = status?.state === 'error' - // Render nothing when there's no update, or the panel is closed (dismissed). - if (!available || !open) return null + // Show the panel when it's open AND we either know of an update or hit an + // error. Keeping it up on error is important: a failed download must surface + // a visible message instead of silently doing nothing. + if (!open || (!available && !errored)) return null const version = state.version const downloading = status?.state === 'downloading' @@ -25,8 +28,12 @@ const UpdateBanner: React.FC = () => {
{t('update_available')}
- {version &&v{version}
} ++ {errored ? t('update_failed') : t('update_available')} +
+ {!errored && version && ( +v{version}
+ )}