fix: improve auto-update UX and proxy support, bump v0.17.1

Auto-update flow rework:
- updater.ts: disable autoDownload, add manual `updater:download` IPC handler
  so user explicitly triggers download via "Download & Install" button
- updater.ts: resolve system proxy via session.defaultSession.resolveProxy()
  and inject into HTTPS_PROXY env var, fixing zero download speed behind VPN/proxy
- preload.ts + electron.d.ts: expose `downloadUpdate` method to renderer

UI improvements:
- UpdateDialog: add "Download & Install" button when update available but not
  yet downloading; show progress bar during download; "Restart to Update" when ready
- GeneralSection UpdateCard: show "Download & Install" / "Restart to Update" button
  next to "Check for Updates"; show download progress bar inline; add "View Details"
  link to reopen update dialog
- AppShell: dismissUpdate no longer permanently hides update for that version
  (removed localStorage persistence), just closes the dialog
- useUpdate: add downloadUpdate to context

i18n:
- Added `update.installUpdate` key (en: "Download & Install", zh: "下载并安装")

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-27 11:19:48 +08:00
1 parent a5e2cedc12
commit fd03d179e3
11 files changed
+126 -62

No files matched your search

+1
View File
@@ -27,6 +27,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
},
updater: {
checkForUpdates: () => ipcRenderer.invoke('updater:check'),
downloadUpdate: () => ipcRenderer.invoke('updater:download'),
quitAndInstall: () => ipcRenderer.invoke('updater:quit-and-install'),
onStatus: (callback: (data: unknown) => void) => {
const listener = (_event: unknown, data: unknown) => callback(data);
+35 -3
View File
@@ -1,6 +1,6 @@
import { autoUpdater } from 'electron-updater';
import type { BrowserWindow } from 'electron';
import { ipcMain } from 'electron';
import { ipcMain, session } from 'electron';
let mainWindow: BrowserWindow | null = null;
@@ -8,13 +8,41 @@ function sendStatus(data: Record<string, unknown>) {
mainWindow?.webContents.send('updater:status', data);
}
/**
* Resolve system proxy for GitHub and inject into electron-updater
* so that VPN / proxy tools are respected during update downloads.
*/
async function configureProxy() {
try {
const proxy = await session.defaultSession.resolveProxy('https://github.com');
// proxy returns "DIRECT" or "PROXY host:port" / "SOCKS5 host:port" etc.
if (proxy && proxy !== 'DIRECT') {
const match = proxy.match(/^(?:PROXY|HTTPS)\s+(.+)/i);
if (match) {
process.env.HTTPS_PROXY = `http://${match[1]}`;
console.log('[updater] Using system proxy:', process.env.HTTPS_PROXY);
}
const socksMatch = proxy.match(/^SOCKS5?\s+(.+)/i);
if (socksMatch) {
process.env.HTTPS_PROXY = `socks5://${socksMatch[1]}`;
console.log('[updater] Using system SOCKS proxy:', process.env.HTTPS_PROXY);
}
}
} catch (err) {
console.warn('[updater] Failed to resolve proxy:', err);
}
}
export function initAutoUpdater(win: BrowserWindow) {
mainWindow = win;
// Configuration
autoUpdater.autoDownload = true;
// Configuration — don't auto-download, let user trigger manually
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
// Resolve and apply system proxy for update downloads
configureProxy();
// --- Events ---
autoUpdater.on('checking-for-update', () => {
sendStatus({ status: 'checking' });
@@ -69,6 +97,10 @@ export function initAutoUpdater(win: BrowserWindow) {
return autoUpdater.checkForUpdates();
});
ipcMain.handle('updater:download', async () => {
return autoUpdater.downloadUpdate();
});
ipcMain.handle('updater:quit-and-install', () => {
autoUpdater.quitAndInstall();
});
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "codepilot",
"version": "0.17.0",
"version": "0.17.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codepilot",
"version": "0.17.0",
"version": "0.17.1",
"dependencies": {
"@ai-sdk/anthropic": "^3.0.47",
"@ai-sdk/google": "^3.0.31",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "codepilot",
"version": "0.17.0",
"version": "0.17.1",
"private": true,
"author": {
"name": "op7418",
+7 -5
View File
@@ -298,10 +298,11 @@ export function AppShell({ children }: { children: React.ReactNode }) {
const dismissUpdate = useCallback(() => {
setShowDialog(false);
if (updateInfo?.latestVersion) {
localStorage.setItem(DISMISSED_VERSION_KEY, updateInfo.latestVersion);
}
}, [updateInfo]);
}, []);
const downloadUpdate = useCallback(() => {
window.electronAPI?.updater?.downloadUpdate();
}, []);
const quitAndInstall = useCallback(() => {
window.electronAPI?.updater?.quitAndInstall();
@@ -312,12 +313,13 @@ export function AppShell({ children }: { children: React.ReactNode }) {
updateInfo,
checking,
checkForUpdates,
downloadUpdate,
dismissUpdate,
showDialog,
setShowDialog,
quitAndInstall,
}),
[updateInfo, checking, checkForUpdates, dismissUpdate, showDialog, quitAndInstall]
[updateInfo, checking, checkForUpdates, downloadUpdate, dismissUpdate, showDialog, quitAndInstall]
);
const panelContextValue = useMemo(
+11 -9
View File
@@ -14,12 +14,13 @@ import { useUpdate } from "@/hooks/useUpdate";
import { useTranslation } from "@/hooks/useTranslation";
export function UpdateDialog() {
const { updateInfo, showDialog, dismissUpdate, quitAndInstall } = useUpdate();
const { updateInfo, showDialog, dismissUpdate, downloadUpdate, quitAndInstall } = useUpdate();
const { t } = useTranslation();
if (!updateInfo?.updateAvailable) return null;
const { isNativeUpdate, readyToInstall, downloadProgress } = updateInfo;
const isDownloading = isNativeUpdate && !readyToInstall && downloadProgress != null && downloadProgress > 0;
return (
<Dialog open={showDialog} onOpenChange={(open) => {
@@ -68,17 +69,17 @@ export function UpdateDialog() {
Current: v{updateInfo.currentVersion} &rarr; Latest: v{updateInfo.latestVersion}
</p>
{/* Download progress bar (native update, downloading) */}
{isNativeUpdate && !readyToInstall && downloadProgress != null && downloadProgress > 0 && (
{/* Download progress bar */}
{isDownloading && (
<div className="space-y-1">
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-blue-500 transition-all"
style={{ width: `${Math.min(downloadProgress, 100)}%` }}
style={{ width: `${Math.min(downloadProgress!, 100)}%` }}
/>
</div>
<p className="text-xs text-muted-foreground">
{t('update.downloading')} {Math.round(downloadProgress)}%
{t('update.downloading')} {Math.round(downloadProgress!)}%
</p>
</div>
)}
@@ -88,7 +89,6 @@ export function UpdateDialog() {
{t('update.later')}
</Button>
{!isNativeUpdate ? (
// Browser mode: open release page
<Button
onClick={() => {
window.open(updateInfo.releaseUrl, "_blank");
@@ -97,15 +97,17 @@ export function UpdateDialog() {
{t('settings.viewRelease')}
</Button>
) : readyToInstall ? (
// Electron: downloaded, ready to install
<Button onClick={quitAndInstall}>
{t('update.restartToUpdate')}
</Button>
) : (
// Electron: still downloading
) : isDownloading ? (
<Button disabled>
{t('update.downloading')}...
</Button>
) : (
<Button onClick={downloadUpdate}>
{t('update.installUpdate')}
</Button>
)}
</DialogFooter>
</DialogContent>
+65 -42
View File
@@ -21,10 +21,13 @@ import { SUPPORTED_LOCALES, type Locale } from "@/i18n";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
function UpdateCard() {
const { updateInfo, checking, checkForUpdates, quitAndInstall } = useUpdate();
const { updateInfo, checking, checkForUpdates, downloadUpdate, quitAndInstall, setShowDialog } = useUpdate();
const { t } = useTranslation();
const currentVersion = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0";
const isDownloading = updateInfo?.isNativeUpdate && !updateInfo.readyToInstall
&& updateInfo.downloadProgress != null && updateInfo.downloadProgress > 0;
return (
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
<div className="flex items-center justify-between">
@@ -32,53 +35,73 @@ function UpdateCard() {
<h2 className="text-sm font-medium">{t('settings.codepilot')}</h2>
<p className="text-xs text-muted-foreground">{t('settings.version', { version: currentVersion })}</p>
</div>
<Button
variant="outline"
size="sm"
onClick={checkForUpdates}
disabled={checking}
className="gap-2"
>
{checking ? (
<HugeiconsIcon icon={Loading02Icon} className="h-3.5 w-3.5 animate-spin" />
) : (
<HugeiconsIcon icon={ReloadIcon} className="h-3.5 w-3.5" />
<div className="flex items-center gap-2">
{/* Show install/restart button when update available */}
{updateInfo?.updateAvailable && !checking && (
updateInfo.readyToInstall ? (
<Button size="sm" onClick={quitAndInstall}>
{t('update.restartToUpdate')}
</Button>
) : updateInfo.isNativeUpdate && !isDownloading ? (
<Button size="sm" onClick={downloadUpdate}>
{t('update.installUpdate')}
</Button>
) : !updateInfo.isNativeUpdate ? (
<Button size="sm" variant="outline" onClick={() => window.open(updateInfo.releaseUrl, "_blank")}>
{t('settings.viewRelease')}
</Button>
) : null
)}
{checking ? t('settings.checking') : t('settings.checkForUpdates')}
</Button>
<Button
variant="outline"
size="sm"
onClick={checkForUpdates}
disabled={checking}
className="gap-2"
>
{checking ? (
<HugeiconsIcon icon={Loading02Icon} className="h-3.5 w-3.5 animate-spin" />
) : (
<HugeiconsIcon icon={ReloadIcon} className="h-3.5 w-3.5" />
)}
{checking ? t('settings.checking') : t('settings.checkForUpdates')}
</Button>
</div>
</div>
{updateInfo && !checking && (
<div className="mt-3">
{updateInfo.updateAvailable ? (
<div className="flex items-center gap-2">
<span className={`h-2 w-2 rounded-full ${updateInfo.readyToInstall ? 'bg-green-500' : 'bg-blue-500'}`} />
<span className="text-sm">
{updateInfo.readyToInstall
? t('update.readyToInstall', { version: updateInfo.latestVersion })
: updateInfo.downloadProgress != null && updateInfo.downloadProgress > 0 && !updateInfo.readyToInstall
? `${t('update.downloading')} ${Math.round(updateInfo.downloadProgress)}%`
: t('settings.updateAvailable', { version: updateInfo.latestVersion })}
</span>
{updateInfo.readyToInstall ? (
<Button
variant="link"
size="sm"
className="h-auto p-0 text-sm"
onClick={quitAndInstall}
>
{t('update.restartToUpdate')}
</Button>
) : !updateInfo.isNativeUpdate ? (
<Button
variant="link"
size="sm"
className="h-auto p-0 text-sm"
onClick={() => window.open(updateInfo.releaseUrl, "_blank")}
>
{t('settings.viewRelease')}
</Button>
) : null}
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className={`h-2 w-2 rounded-full ${updateInfo.readyToInstall ? 'bg-green-500' : isDownloading ? 'bg-yellow-500 animate-pulse' : 'bg-blue-500'}`} />
<span className="text-sm">
{updateInfo.readyToInstall
? t('update.readyToInstall', { version: updateInfo.latestVersion })
: isDownloading
? `${t('update.downloading')} ${Math.round(updateInfo.downloadProgress!)}%`
: t('settings.updateAvailable', { version: updateInfo.latestVersion })}
</span>
{updateInfo.releaseNotes && (
<Button
variant="link"
size="sm"
className="h-auto p-0 text-xs text-muted-foreground"
onClick={() => setShowDialog(true)}
>
{t('gallery.viewDetails')}
</Button>
)}
</div>
{/* Download progress bar */}
{isDownloading && (
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-blue-500 transition-all"
style={{ width: `${Math.min(updateInfo.downloadProgress!, 100)}%` }}
/>
</div>
)}
</div>
) : (
<p className="text-sm text-muted-foreground">{t('settings.latestVersion')}</p>
+1
View File
@@ -19,6 +19,7 @@ export interface UpdateContextValue {
updateInfo: UpdateInfo | null;
checking: boolean;
checkForUpdates: () => Promise<void>;
downloadUpdate: () => void;
dismissUpdate: () => void;
showDialog: boolean;
setShowDialog: (v: boolean) => void;
+1
View File
@@ -305,6 +305,7 @@ const en = {
'update.restartToUpdate': 'Restart to Update',
'update.restartNow': 'Restart Now',
'update.readyToInstall': 'CodePilot v{version} is ready — restart to update',
'update.installUpdate': 'Download & Install',
'update.later': 'Later',
// ── Image Generation ──────────────────────────────────────
+1
View File
@@ -302,6 +302,7 @@ const zh: Record<TranslationKey, string> = {
'update.restartToUpdate': '重启以更新',
'update.restartNow': '立即重启',
'update.readyToInstall': 'CodePilot v{version} 已就绪 — 重启以完成更新',
'update.installUpdate': '下载并安装',
'update.later': '稍后',
// ── Image Generation ──────────────────────────────────────
+1
View File
@@ -36,6 +36,7 @@ interface UpdateStatusEvent {
interface ElectronUpdaterAPI {
checkForUpdates: () => Promise<unknown>;
downloadUpdate: () => Promise<unknown>;
quitAndInstall: () => Promise<void>;
onStatus: (callback: (data: UpdateStatusEvent) => void) => () => void;
}