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:
1 parent
a5e2cedc12
commit
fd03d179e3
11 files changed
+126
-62
No files matched your search
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codepilot",
|
||||
"version": "0.17.0",
|
||||
"version": "0.17.1",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "op7418",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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} → 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ──────────────────────────────────────
|
||||
|
||||
@@ -302,6 +302,7 @@ const zh: Record<TranslationKey, string> = {
|
||||
'update.restartToUpdate': '重启以更新',
|
||||
'update.restartNow': '立即重启',
|
||||
'update.readyToInstall': 'CodePilot v{version} 已就绪 — 重启以完成更新',
|
||||
'update.installUpdate': '下载并安装',
|
||||
'update.later': '稍后',
|
||||
|
||||
// ── Image Generation ──────────────────────────────────────
|
||||
|
||||
Vendored
+1
@@ -36,6 +36,7 @@ interface UpdateStatusEvent {
|
||||
|
||||
interface ElectronUpdaterAPI {
|
||||
checkForUpdates: () => Promise<unknown>;
|
||||
downloadUpdate: () => Promise<unknown>;
|
||||
quitAndInstall: () => Promise<void>;
|
||||
onStatus: (callback: (data: UpdateStatusEvent) => void) => () => void;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user