Skip to content

Commit fd03d17

Browse files
7418claude
andcommitted
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>
1 parent a5e2ced commit fd03d17

File tree

11 files changed

+126
-62
lines changed

11 files changed

+126
-62
lines changed

electron/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
2727
},
2828
updater: {
2929
checkForUpdates: () => ipcRenderer.invoke('updater:check'),
30+
downloadUpdate: () => ipcRenderer.invoke('updater:download'),
3031
quitAndInstall: () => ipcRenderer.invoke('updater:quit-and-install'),
3132
onStatus: (callback: (data: unknown) => void) => {
3233
const listener = (_event: unknown, data: unknown) => callback(data);

electron/updater.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,48 @@
11
import { autoUpdater } from 'electron-updater';
22
import type { BrowserWindow } from 'electron';
3-
import { ipcMain } from 'electron';
3+
import { ipcMain, session } from 'electron';
44

55
let mainWindow: BrowserWindow | null = null;
66

77
function sendStatus(data: Record<string, unknown>) {
88
mainWindow?.webContents.send('updater:status', data);
99
}
1010

11+
/**
12+
* Resolve system proxy for GitHub and inject into electron-updater
13+
* so that VPN / proxy tools are respected during update downloads.
14+
*/
15+
async function configureProxy() {
16+
try {
17+
const proxy = await session.defaultSession.resolveProxy('https://github.com');
18+
// proxy returns "DIRECT" or "PROXY host:port" / "SOCKS5 host:port" etc.
19+
if (proxy && proxy !== 'DIRECT') {
20+
const match = proxy.match(/^(?:PROXY|HTTPS)\s+(.+)/i);
21+
if (match) {
22+
process.env.HTTPS_PROXY = `http://${match[1]}`;
23+
console.log('[updater] Using system proxy:', process.env.HTTPS_PROXY);
24+
}
25+
const socksMatch = proxy.match(/^SOCKS5?\s+(.+)/i);
26+
if (socksMatch) {
27+
process.env.HTTPS_PROXY = `socks5://${socksMatch[1]}`;
28+
console.log('[updater] Using system SOCKS proxy:', process.env.HTTPS_PROXY);
29+
}
30+
}
31+
} catch (err) {
32+
console.warn('[updater] Failed to resolve proxy:', err);
33+
}
34+
}
35+
1136
export function initAutoUpdater(win: BrowserWindow) {
1237
mainWindow = win;
1338

14-
// Configuration
15-
autoUpdater.autoDownload = true;
39+
// Configuration — don't auto-download, let user trigger manually
40+
autoUpdater.autoDownload = false;
1641
autoUpdater.autoInstallOnAppQuit = true;
1742

43+
// Resolve and apply system proxy for update downloads
44+
configureProxy();
45+
1846
// --- Events ---
1947
autoUpdater.on('checking-for-update', () => {
2048
sendStatus({ status: 'checking' });
@@ -69,6 +97,10 @@ export function initAutoUpdater(win: BrowserWindow) {
6997
return autoUpdater.checkForUpdates();
7098
});
7199

100+
ipcMain.handle('updater:download', async () => {
101+
return autoUpdater.downloadUpdate();
102+
});
103+
72104
ipcMain.handle('updater:quit-and-install', () => {
73105
autoUpdater.quitAndInstall();
74106
});

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codepilot",
3-
"version": "0.17.0",
3+
"version": "0.17.1",
44
"private": true,
55
"author": {
66
"name": "op7418",

src/components/layout/AppShell.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,11 @@ export function AppShell({ children }: { children: React.ReactNode }) {
298298

299299
const dismissUpdate = useCallback(() => {
300300
setShowDialog(false);
301-
if (updateInfo?.latestVersion) {
302-
localStorage.setItem(DISMISSED_VERSION_KEY, updateInfo.latestVersion);
303-
}
304-
}, [updateInfo]);
301+
}, []);
302+
303+
const downloadUpdate = useCallback(() => {
304+
window.electronAPI?.updater?.downloadUpdate();
305+
}, []);
305306

306307
const quitAndInstall = useCallback(() => {
307308
window.electronAPI?.updater?.quitAndInstall();
@@ -312,12 +313,13 @@ export function AppShell({ children }: { children: React.ReactNode }) {
312313
updateInfo,
313314
checking,
314315
checkForUpdates,
316+
downloadUpdate,
315317
dismissUpdate,
316318
showDialog,
317319
setShowDialog,
318320
quitAndInstall,
319321
}),
320-
[updateInfo, checking, checkForUpdates, dismissUpdate, showDialog, quitAndInstall]
322+
[updateInfo, checking, checkForUpdates, downloadUpdate, dismissUpdate, showDialog, quitAndInstall]
321323
);
322324

323325
const panelContextValue = useMemo(

src/components/layout/UpdateDialog.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ import { useUpdate } from "@/hooks/useUpdate";
1414
import { useTranslation } from "@/hooks/useTranslation";
1515

1616
export function UpdateDialog() {
17-
const { updateInfo, showDialog, dismissUpdate, quitAndInstall } = useUpdate();
17+
const { updateInfo, showDialog, dismissUpdate, downloadUpdate, quitAndInstall } = useUpdate();
1818
const { t } = useTranslation();
1919

2020
if (!updateInfo?.updateAvailable) return null;
2121

2222
const { isNativeUpdate, readyToInstall, downloadProgress } = updateInfo;
23+
const isDownloading = isNativeUpdate && !readyToInstall && downloadProgress != null && downloadProgress > 0;
2324

2425
return (
2526
<Dialog open={showDialog} onOpenChange={(open) => {
@@ -68,17 +69,17 @@ export function UpdateDialog() {
6869
Current: v{updateInfo.currentVersion} &rarr; Latest: v{updateInfo.latestVersion}
6970
</p>
7071

71-
{/* Download progress bar (native update, downloading) */}
72-
{isNativeUpdate && !readyToInstall && downloadProgress != null && downloadProgress > 0 && (
72+
{/* Download progress bar */}
73+
{isDownloading && (
7374
<div className="space-y-1">
7475
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
7576
<div
7677
className="h-full rounded-full bg-blue-500 transition-all"
77-
style={{ width: `${Math.min(downloadProgress, 100)}%` }}
78+
style={{ width: `${Math.min(downloadProgress!, 100)}%` }}
7879
/>
7980
</div>
8081
<p className="text-xs text-muted-foreground">
81-
{t('update.downloading')} {Math.round(downloadProgress)}%
82+
{t('update.downloading')} {Math.round(downloadProgress!)}%
8283
</p>
8384
</div>
8485
)}
@@ -88,7 +89,6 @@ export function UpdateDialog() {
8889
{t('update.later')}
8990
</Button>
9091
{!isNativeUpdate ? (
91-
// Browser mode: open release page
9292
<Button
9393
onClick={() => {
9494
window.open(updateInfo.releaseUrl, "_blank");
@@ -97,15 +97,17 @@ export function UpdateDialog() {
9797
{t('settings.viewRelease')}
9898
</Button>
9999
) : readyToInstall ? (
100-
// Electron: downloaded, ready to install
101100
<Button onClick={quitAndInstall}>
102101
{t('update.restartToUpdate')}
103102
</Button>
104-
) : (
105-
// Electron: still downloading
103+
) : isDownloading ? (
106104
<Button disabled>
107105
{t('update.downloading')}...
108106
</Button>
107+
) : (
108+
<Button onClick={downloadUpdate}>
109+
{t('update.installUpdate')}
110+
</Button>
109111
)}
110112
</DialogFooter>
111113
</DialogContent>

src/components/settings/GeneralSection.tsx

Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,64 +21,87 @@ import { SUPPORTED_LOCALES, type Locale } from "@/i18n";
2121
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
2222

2323
function UpdateCard() {
24-
const { updateInfo, checking, checkForUpdates, quitAndInstall } = useUpdate();
24+
const { updateInfo, checking, checkForUpdates, downloadUpdate, quitAndInstall, setShowDialog } = useUpdate();
2525
const { t } = useTranslation();
2626
const currentVersion = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0";
2727

28+
const isDownloading = updateInfo?.isNativeUpdate && !updateInfo.readyToInstall
29+
&& updateInfo.downloadProgress != null && updateInfo.downloadProgress > 0;
30+
2831
return (
2932
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
3033
<div className="flex items-center justify-between">
3134
<div>
3235
<h2 className="text-sm font-medium">{t('settings.codepilot')}</h2>
3336
<p className="text-xs text-muted-foreground">{t('settings.version', { version: currentVersion })}</p>
3437
</div>
35-
<Button
36-
variant="outline"
37-
size="sm"
38-
onClick={checkForUpdates}
39-
disabled={checking}
40-
className="gap-2"
41-
>
42-
{checking ? (
43-
<HugeiconsIcon icon={Loading02Icon} className="h-3.5 w-3.5 animate-spin" />
44-
) : (
45-
<HugeiconsIcon icon={ReloadIcon} className="h-3.5 w-3.5" />
38+
<div className="flex items-center gap-2">
39+
{/* Show install/restart button when update available */}
40+
{updateInfo?.updateAvailable && !checking && (
41+
updateInfo.readyToInstall ? (
42+
<Button size="sm" onClick={quitAndInstall}>
43+
{t('update.restartToUpdate')}
44+
</Button>
45+
) : updateInfo.isNativeUpdate && !isDownloading ? (
46+
<Button size="sm" onClick={downloadUpdate}>
47+
{t('update.installUpdate')}
48+
</Button>
49+
) : !updateInfo.isNativeUpdate ? (
50+
<Button size="sm" variant="outline" onClick={() => window.open(updateInfo.releaseUrl, "_blank")}>
51+
{t('settings.viewRelease')}
52+
</Button>
53+
) : null
4654
)}
47-
{checking ? t('settings.checking') : t('settings.checkForUpdates')}
48-
</Button>
55+
<Button
56+
variant="outline"
57+
size="sm"
58+
onClick={checkForUpdates}
59+
disabled={checking}
60+
className="gap-2"
61+
>
62+
{checking ? (
63+
<HugeiconsIcon icon={Loading02Icon} className="h-3.5 w-3.5 animate-spin" />
64+
) : (
65+
<HugeiconsIcon icon={ReloadIcon} className="h-3.5 w-3.5" />
66+
)}
67+
{checking ? t('settings.checking') : t('settings.checkForUpdates')}
68+
</Button>
69+
</div>
4970
</div>
5071

5172
{updateInfo && !checking && (
5273
<div className="mt-3">
5374
{updateInfo.updateAvailable ? (
54-
<div className="flex items-center gap-2">
55-
<span className={`h-2 w-2 rounded-full ${updateInfo.readyToInstall ? 'bg-green-500' : 'bg-blue-500'}`} />
56-
<span className="text-sm">
57-
{updateInfo.readyToInstall
58-
? t('update.readyToInstall', { version: updateInfo.latestVersion })
59-
: updateInfo.downloadProgress != null && updateInfo.downloadProgress > 0 && !updateInfo.readyToInstall
60-
? `${t('update.downloading')} ${Math.round(updateInfo.downloadProgress)}%`
61-
: t('settings.updateAvailable', { version: updateInfo.latestVersion })}
62-
</span>
63-
{updateInfo.readyToInstall ? (
64-
<Button
65-
variant="link"
66-
size="sm"
67-
className="h-auto p-0 text-sm"
68-
onClick={quitAndInstall}
69-
>
70-
{t('update.restartToUpdate')}
71-
</Button>
72-
) : !updateInfo.isNativeUpdate ? (
73-
<Button
74-
variant="link"
75-
size="sm"
76-
className="h-auto p-0 text-sm"
77-
onClick={() => window.open(updateInfo.releaseUrl, "_blank")}
78-
>
79-
{t('settings.viewRelease')}
80-
</Button>
81-
) : null}
75+
<div className="space-y-2">
76+
<div className="flex items-center gap-2">
77+
<span className={`h-2 w-2 rounded-full ${updateInfo.readyToInstall ? 'bg-green-500' : isDownloading ? 'bg-yellow-500 animate-pulse' : 'bg-blue-500'}`} />
78+
<span className="text-sm">
79+
{updateInfo.readyToInstall
80+
? t('update.readyToInstall', { version: updateInfo.latestVersion })
81+
: isDownloading
82+
? `${t('update.downloading')} ${Math.round(updateInfo.downloadProgress!)}%`
83+
: t('settings.updateAvailable', { version: updateInfo.latestVersion })}
84+
</span>
85+
{updateInfo.releaseNotes && (
86+
<Button
87+
variant="link"
88+
size="sm"
89+
className="h-auto p-0 text-xs text-muted-foreground"
90+
onClick={() => setShowDialog(true)}
91+
>
92+
{t('gallery.viewDetails')}
93+
</Button>
94+
)}
95+
</div>
96+
{/* Download progress bar */}
97+
{isDownloading && (
98+
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
99+
<div
100+
className="h-full rounded-full bg-blue-500 transition-all"
101+
style={{ width: `${Math.min(updateInfo.downloadProgress!, 100)}%` }}
102+
/>
103+
</div>
104+
)}
82105
</div>
83106
) : (
84107
<p className="text-sm text-muted-foreground">{t('settings.latestVersion')}</p>

src/hooks/useUpdate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface UpdateContextValue {
1919
updateInfo: UpdateInfo | null;
2020
checking: boolean;
2121
checkForUpdates: () => Promise<void>;
22+
downloadUpdate: () => void;
2223
dismissUpdate: () => void;
2324
showDialog: boolean;
2425
setShowDialog: (v: boolean) => void;

src/i18n/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ const en = {
305305
'update.restartToUpdate': 'Restart to Update',
306306
'update.restartNow': 'Restart Now',
307307
'update.readyToInstall': 'CodePilot v{version} is ready — restart to update',
308+
'update.installUpdate': 'Download & Install',
308309
'update.later': 'Later',
309310

310311
// ── Image Generation ──────────────────────────────────────

src/i18n/zh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ const zh: Record<TranslationKey, string> = {
302302
'update.restartToUpdate': '重启以更新',
303303
'update.restartNow': '立即重启',
304304
'update.readyToInstall': 'CodePilot v{version} 已就绪 — 重启以完成更新',
305+
'update.installUpdate': '下载并安装',
305306
'update.later': '稍后',
306307

307308
// ── Image Generation ──────────────────────────────────────

0 commit comments

Comments
 (0)