fix: address review findings in updater flow (#114)

- Add console.error logging to startup update check catch block
- Add state guard to downloadUpdate() to prevent calls outside 'available' state
- Track and dismiss 'available' toast when download starts or completes
- Include version number in downloading status text

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jinjing
2026-03-26 07:35:52 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 6650289dcb
commit 149acf26bb
8 changed files with 83 additions and 14 deletions
+21 -3
View File
@@ -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) })
})
}
@@ -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())
}
+1
View File
@@ -99,6 +99,7 @@ type UpdaterApi = {
getVersion: () => Promise<string>
getStatus: () => Promise<UpdateStatus>
check: () => Promise<void>
download: () => Promise<void>
quitAndInstall: () => Promise<void>
onStatus: (callback: (status: UpdateStatus) => void) => () => void
}
+1
View File
@@ -192,6 +192,7 @@ const api = {
getStatus: (): Promise<unknown> => ipcRenderer.invoke('updater:getStatus'),
getVersion: (): Promise<string> => ipcRenderer.invoke('updater:getVersion'),
check: (): Promise<void> => ipcRenderer.invoke('updater:check'),
download: (): Promise<void> => ipcRenderer.invoke('updater:download'),
quitAndInstall: (): Promise<void> => ipcRenderer.invoke('updater:quitAndInstall'),
onStatus: (callback: (status: unknown) => void): (() => void) => {
const listener = (_event: Electron.IpcRendererEvent, status: unknown) => callback(status)
@@ -161,7 +161,17 @@ export function GeneralPane({
Check for Updates
</Button>
{updateStatus.state === 'downloaded' ? (
{updateStatus.state === 'available' ? (
<Button
variant="default"
size="sm"
onClick={() => window.api.updater.download()}
className="gap-2"
>
<Download className="size-3.5" />
Install Update ({updateStatus.version})
</Button>
) : updateStatus.state === 'downloaded' ? (
<Button
variant="default"
size="sm"
@@ -178,9 +188,10 @@ export function GeneralPane({
{updateStatus.state === 'idle' && 'Updates are checked automatically on launch.'}
{updateStatus.state === 'checking' && 'Checking for updates...'}
{updateStatus.state === 'available' &&
`Version ${updateStatus.version} is available. Downloading...`}
`Version ${updateStatus.version} is available. Click "Install Update" to download.`}
{updateStatus.state === 'not-available' && 'You\u2019re on the latest version.'}
{updateStatus.state === 'downloading' && `Downloading update... ${updateStatus.percent}%`}
{updateStatus.state === 'downloading' &&
`Downloading v${updateStatus.version}... ${updateStatus.percent}%`}
{updateStatus.state === 'downloaded' &&
`Version ${updateStatus.version} is ready to install.`}
{updateStatus.state === 'error' && `Update error: ${updateStatus.message}`}
@@ -13,7 +13,9 @@ const SidebarToolbar = React.memo(function SidebarToolbar() {
const updateVersion = 'version' in updateStatus ? updateStatus.version : null
const showUpdateBanner =
(updateStatus.state === 'downloaded' || updateStatus.state === 'available') &&
(updateStatus.state === 'downloaded' ||
updateStatus.state === 'available' ||
updateStatus.state === 'downloading') &&
updateVersion !== dismissedUpdateVersion
return (
@@ -21,17 +23,28 @@ const SidebarToolbar = React.memo(function SidebarToolbar() {
{showUpdateBanner && (
<div className="flex items-center border-t border-sidebar-border bg-primary/10">
<button
onClick={() =>
updateStatus.state === 'downloaded' ? window.api.updater.quitAndInstall() : undefined
}
className="flex items-center gap-1.5 flex-1 min-w-0 px-2 py-1.5 text-[11px] font-medium text-primary hover:bg-primary/15 transition-colors cursor-pointer"
onClick={() => {
if (updateStatus.state === 'downloaded') {
window.api.updater.quitAndInstall()
} else if (updateStatus.state === 'available') {
window.api.updater.download()
}
}}
disabled={updateStatus.state === 'downloading'}
className="flex items-center gap-1.5 flex-1 min-w-0 px-2 py-1.5 text-[11px] font-medium text-primary hover:bg-primary/15 transition-colors cursor-pointer disabled:opacity-60 disabled:cursor-default"
>
<Download className="size-3.5 shrink-0" />
{updateStatus.state === 'downloaded' ? (
<span>Restart now (update)</span>
) : updateStatus.state === 'downloading' ? (
<span>
Downloading <span className="font-semibold">v{updateVersion}</span>{' '}
{updateStatus.percent}%
</span>
) : (
<span>
Downloading <span className="font-semibold">v{updateStatus.version}</span>
Update <span className="font-semibold">v{updateStatus.version}</span> available
Install
</span>
)}
</button>
+17
View File
@@ -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: {
+1 -1
View File
@@ -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 }