diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 245422f..d70b51e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -39,16 +39,9 @@ jobs:
run: npm run electron:build
- name: Package for Windows
- run: npx electron-builder --win --config electron-builder.yml --publish never
-
- - name: Upload Windows artifacts
- uses: actions/upload-artifact@v4
- with:
- name: windows
- path: |
- release/*.exe
- release/*.zip
- if-no-files-found: warn
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: npx electron-builder --win --config electron-builder.yml --publish always
build-macos:
if: ${{ github.event_name == 'push' || inputs.platform == 'all' || inputs.platform == 'macos' }}
@@ -71,14 +64,9 @@ jobs:
run: npm run electron:build
- name: Package for macOS (${{ matrix.arch }})
- run: npx electron-builder --mac --${{ matrix.arch }} --config electron-builder.yml --publish never
-
- - name: Upload macOS artifacts
- uses: actions/upload-artifact@v4
- with:
- name: macos-${{ matrix.arch }}
- path: release/*.dmg
- if-no-files-found: warn
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: npx electron-builder --mac --${{ matrix.arch }} --config electron-builder.yml --publish always
build-linux:
if: ${{ github.event_name == 'push' || inputs.platform == 'all' || inputs.platform == 'linux' }}
@@ -101,17 +89,9 @@ jobs:
run: npm run electron:build
- name: Package for Linux (${{ matrix.arch }})
- run: npx electron-builder --linux --${{ matrix.arch }} --config electron-builder.yml --publish never
-
- - name: Upload Linux artifacts
- uses: actions/upload-artifact@v4
- with:
- name: linux-${{ matrix.arch }}
- path: |
- release/*.AppImage
- release/*.deb
- release/*.rpm
- if-no-files-found: warn
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: npx electron-builder --linux --${{ matrix.arch }} --config electron-builder.yml --publish always
release:
if: ${{ always() && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !contains(needs.*.result, 'cancelled') }}
@@ -122,11 +102,6 @@ jobs:
with:
fetch-depth: 0
- - name: Download all artifacts
- uses: actions/download-artifact@v4
- with:
- path: artifacts
-
- name: Get version and previous tag
id: meta
run: |
@@ -151,13 +126,7 @@ jobs:
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"
- - name: Collect release files
- run: |
- mkdir -p release-files
- find artifacts -type f \( -name '*.exe' -o -name '*.dmg' -o -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' -o -name '*.zip' \) -exec cp {} release-files/ \;
- ls -lh release-files/
-
- - name: Create GitHub Release
+ - name: Update release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
@@ -179,21 +148,21 @@ jobs:
## Installation
- **Windows**: 下载 exe 安装包,双击安装,按提示完成即可。
+ **Windows**: Download the exe installer, double-click to install.
**macOS**:
- 1. 下载对应芯片架构的 DMG 文件
- 2. 打开 DMG,将 CodePilot 拖入 Applications 文件夹
- 3. 首次打开时如遇安全提示,前往 **系统设置 → 隐私与安全性** 点击"仍要打开"
- 4. 在 Settings 页面配置 Anthropic API Key 或环境变量
+ 1. Download the DMG for your chip architecture
+ 2. Open the DMG, drag CodePilot into Applications
+ 3. If you see a security prompt on first launch, go to **System Settings > Privacy & Security** and click "Open Anyway"
+ 4. Configure your Anthropic API Key in Settings
- **Linux**: 使用 AppImage 直接运行,或通过 deb/rpm 包安装。
+ **Linux**: Run the AppImage directly, or install via deb/rpm package.
## Requirements
- Windows 10+ / macOS 12.0+ / Linux (glibc 2.31+)
- - Anthropic API Key 或已配置 `ANTHROPIC_API_KEY` 环境变量
- - 如需使用代码相关功能,建议安装 Claude Code CLI
+ - Anthropic API Key or `ANTHROPIC_API_KEY` environment variable
+ - Claude Code CLI recommended for code features
## Changelog
NOTES_EOF
@@ -207,8 +176,9 @@ jobs:
CHANGELOG_INLINE_EOF
fi
- gh release create "${GITHUB_REF_NAME}" \
+ # Update the draft release created by electron-builder with full notes
+ gh release edit "${GITHUB_REF_NAME}" \
--title "CodePilot v${VERSION}" \
--notes-file release-notes.md \
--latest \
- release-files/*
+ --draft=false
diff --git a/electron-builder.yml b/electron-builder.yml
index 9b8c54a..75e0a58 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -1,5 +1,9 @@
appId: com.codepilot.app
productName: CodePilot
+publish:
+ provider: github
+ owner: op7418
+ repo: CodePilot
directories:
output: release
files:
diff --git a/electron/main.ts b/electron/main.ts
index 819f279..b33888e 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -4,6 +4,7 @@ import { execFileSync, spawn, ChildProcess } from 'child_process';
import fs from 'fs';
import net from 'net';
import os from 'os';
+import { initAutoUpdater, setUpdaterWindow } from './updater';
let mainWindow: BrowserWindow | null = null;
let serverProcess: Electron.UtilityProcess | null = null;
@@ -739,6 +740,11 @@ app.whenReady().then(async () => {
serverPort = port;
createWindow(port);
+
+ // Initialize auto-updater in packaged mode only
+ if (!isDev && mainWindow) {
+ initAutoUpdater(mainWindow);
+ }
} catch (err) {
console.error('Failed to start:', err);
dialog.showErrorBox(
@@ -766,6 +772,11 @@ app.on('activate', async () => {
serverPort = port;
}
createWindow(serverPort || 3000);
+
+ // Re-attach updater to the new window
+ if (!isDev && mainWindow) {
+ setUpdaterWindow(mainWindow);
+ }
} catch (err) {
console.error('Failed to restart server:', err);
}
diff --git a/electron/preload.ts b/electron/preload.ts
index 6de761a..1cd1a89 100644
--- a/electron/preload.ts
+++ b/electron/preload.ts
@@ -25,4 +25,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
return () => { ipcRenderer.removeListener('install:progress', listener); };
},
},
+ updater: {
+ checkForUpdates: () => ipcRenderer.invoke('updater:check'),
+ quitAndInstall: () => ipcRenderer.invoke('updater:quit-and-install'),
+ onStatus: (callback: (data: unknown) => void) => {
+ const listener = (_event: unknown, data: unknown) => callback(data);
+ ipcRenderer.on('updater:status', listener);
+ return () => { ipcRenderer.removeListener('updater:status', listener); };
+ },
+ },
});
diff --git a/electron/updater.ts b/electron/updater.ts
new file mode 100644
index 0000000..220b4bf
--- /dev/null
+++ b/electron/updater.ts
@@ -0,0 +1,93 @@
+import { autoUpdater } from 'electron-updater';
+import type { BrowserWindow } from 'electron';
+import { ipcMain } from 'electron';
+
+let mainWindow: BrowserWindow | null = null;
+
+function sendStatus(data: Record) {
+ mainWindow?.webContents.send('updater:status', data);
+}
+
+export function initAutoUpdater(win: BrowserWindow) {
+ mainWindow = win;
+
+ // Configuration
+ autoUpdater.autoDownload = true;
+ autoUpdater.autoInstallOnAppQuit = true;
+
+ // --- Events ---
+ autoUpdater.on('checking-for-update', () => {
+ sendStatus({ status: 'checking' });
+ });
+
+ autoUpdater.on('update-available', (info) => {
+ sendStatus({
+ status: 'available',
+ info: {
+ version: info.version,
+ releaseNotes: info.releaseNotes,
+ releaseName: info.releaseName,
+ releaseDate: info.releaseDate,
+ },
+ });
+ });
+
+ autoUpdater.on('update-not-available', () => {
+ sendStatus({ status: 'not-available' });
+ });
+
+ autoUpdater.on('download-progress', (progress) => {
+ sendStatus({
+ status: 'downloading',
+ progress: {
+ percent: progress.percent,
+ bytesPerSecond: progress.bytesPerSecond,
+ transferred: progress.transferred,
+ total: progress.total,
+ },
+ });
+ });
+
+ autoUpdater.on('update-downloaded', (info) => {
+ sendStatus({
+ status: 'downloaded',
+ info: {
+ version: info.version,
+ releaseNotes: info.releaseNotes,
+ releaseName: info.releaseName,
+ releaseDate: info.releaseDate,
+ },
+ });
+ });
+
+ autoUpdater.on('error', (err) => {
+ sendStatus({ status: 'error', error: err.message });
+ });
+
+ // --- IPC handlers ---
+ ipcMain.handle('updater:check', async () => {
+ return autoUpdater.checkForUpdates();
+ });
+
+ ipcMain.handle('updater:quit-and-install', () => {
+ autoUpdater.quitAndInstall();
+ });
+
+ // Initial check after 10 seconds
+ setTimeout(() => {
+ autoUpdater.checkForUpdates().catch((err) => {
+ console.warn('[updater] Initial check failed:', err.message);
+ });
+ }, 10_000);
+
+ // Periodic check every 4 hours
+ setInterval(() => {
+ autoUpdater.checkForUpdates().catch((err) => {
+ console.warn('[updater] Periodic check failed:', err.message);
+ });
+ }, 4 * 60 * 60 * 1000);
+}
+
+export function setUpdaterWindow(win: BrowserWindow) {
+ mainWindow = win;
+}
diff --git a/package-lock.json b/package-lock.json
index 98d4815..6ffa3f2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,6 +23,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+ "electron-updater": "^6.8.3",
"lucide-react": "^0.563.0",
"motion": "^12.33.0",
"nanoid": "^5.1.6",
@@ -8206,7 +8207,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
"license": "Python-2.0"
},
"node_modules/aria-hidden": {
@@ -8774,7 +8774,6 @@
"version": "9.5.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4",
@@ -10633,6 +10632,69 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/electron-updater": {
+ "version": "6.8.3",
+ "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz",
+ "integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "builder-util-runtime": "9.5.1",
+ "fs-extra": "^10.1.0",
+ "js-yaml": "^4.1.0",
+ "lazy-val": "^1.0.5",
+ "lodash.escaperegexp": "^4.1.2",
+ "lodash.isequal": "^4.5.0",
+ "semver": "~7.7.3",
+ "tiny-typed-emitter": "^2.1.0"
+ }
+ },
+ "node_modules/electron-updater/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-updater/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-updater/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/electron-updater/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
"node_modules/electron-winstaller": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
@@ -12443,7 +12505,6 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
"license": "ISC"
},
"node_modules/hachure-fill": {
@@ -13814,7 +13875,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -14002,7 +14062,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/leva": {
@@ -14381,6 +14440,19 @@
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
"license": "MIT"
},
+ "node_modules/lodash.escaperegexp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
+ "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -18833,7 +18905,6 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
- "dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=11.0.0"
@@ -20035,6 +20106,12 @@
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
+ "node_modules/tiny-typed-emitter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
+ "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
+ "license": "MIT"
+ },
"node_modules/tinyexec": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
diff --git a/package.json b/package.json
index e4a6c75..cd6f82f 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+ "electron-updater": "^6.8.3",
"lucide-react": "^0.563.0",
"motion": "^12.33.0",
"nanoid": "^5.1.6",
diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx
index 4c79fb9..5a2c9c4 100644
--- a/src/components/layout/AppShell.tsx
+++ b/src/components/layout/AppShell.tsx
@@ -8,6 +8,7 @@ import { ChatListPanel } from "./ChatListPanel";
import { RightPanel } from "./RightPanel";
import { ResizeHandle } from "./ResizeHandle";
import { UpdateDialog } from "./UpdateDialog";
+import { UpdateBanner } from "./UpdateBanner";
import { DocPreview } from "./DocPreview";
import { PanelContext, type PanelContent, type PreviewViewMode } from "@/hooks/usePanel";
import { UpdateContext, type UpdateInfo } from "@/hooks/useUpdate";
@@ -172,17 +173,26 @@ export function AppShell({ children }: { children: React.ReactNode }) {
const [checking, setChecking] = useState(false);
const [showDialog, setShowDialog] = useState(false);
- const checkForUpdates = useCallback(async () => {
+ const isNativeUpdater = typeof window !== "undefined" && !!window.electronAPI?.updater;
+
+ // --- Browser-mode update check (fallback) ---
+ const checkForUpdatesBrowser = useCallback(async () => {
setChecking(true);
try {
const res = await fetch("/api/app/updates");
if (!res.ok) return;
- const data: UpdateInfo = await res.json();
- setUpdateInfo(data);
+ const data = await res.json();
+ const info: UpdateInfo = {
+ ...data,
+ downloadProgress: null,
+ readyToInstall: false,
+ isNativeUpdate: false,
+ };
+ setUpdateInfo(info);
- if (data.updateAvailable) {
+ if (info.updateAvailable) {
const dismissed = localStorage.getItem(DISMISSED_VERSION_KEY);
- if (dismissed !== data.latestVersion) {
+ if (dismissed !== info.latestVersion) {
setShowDialog(true);
}
}
@@ -193,6 +203,97 @@ export function AppShell({ children }: { children: React.ReactNode }) {
}
}, []);
+ // --- Electron native updater check ---
+ const checkForUpdatesNative = useCallback(async () => {
+ setChecking(true);
+ try {
+ await window.electronAPI?.updater?.checkForUpdates();
+ } catch {
+ setChecking(false);
+ }
+ }, []);
+
+ const checkForUpdates = isNativeUpdater ? checkForUpdatesNative : checkForUpdatesBrowser;
+
+ // Subscribe to native updater IPC events
+ useEffect(() => {
+ if (!isNativeUpdater) return;
+
+ const unsubscribe = window.electronAPI!.updater!.onStatus((event) => {
+ switch (event.status) {
+ case 'checking':
+ setChecking(true);
+ break;
+
+ case 'available':
+ setChecking(false);
+ setUpdateInfo((prev) => {
+ const releaseNotes = typeof event.info?.releaseNotes === 'string'
+ ? event.info.releaseNotes
+ : '';
+ const newInfo: UpdateInfo = {
+ updateAvailable: true,
+ latestVersion: event.info?.version ?? '',
+ currentVersion: prev?.currentVersion ?? (process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0'),
+ releaseName: event.info?.releaseName ?? `v${event.info?.version}`,
+ releaseNotes,
+ releaseUrl: `https://github.com/op7418/CodePilot/releases/tag/v${event.info?.version}`,
+ publishedAt: event.info?.releaseDate ?? '',
+ downloadProgress: prev?.downloadProgress ?? null,
+ readyToInstall: prev?.readyToInstall ?? false,
+ isNativeUpdate: true,
+ };
+ return newInfo;
+ });
+ // Show dialog if not dismissed
+ if (event.info?.version) {
+ const dismissed = localStorage.getItem(DISMISSED_VERSION_KEY);
+ if (dismissed !== event.info.version) {
+ setShowDialog(true);
+ }
+ }
+ break;
+
+ case 'not-available':
+ setChecking(false);
+ break;
+
+ case 'downloading':
+ setUpdateInfo((prev) => prev ? {
+ ...prev,
+ downloadProgress: event.progress?.percent ?? null,
+ } : prev);
+ break;
+
+ case 'downloaded':
+ setUpdateInfo((prev) => {
+ if (!prev) return prev;
+ return {
+ ...prev,
+ readyToInstall: true,
+ downloadProgress: 100,
+ };
+ });
+ break;
+
+ case 'error':
+ setChecking(false);
+ console.warn('[updater] Error:', event.error);
+ break;
+ }
+ });
+
+ return unsubscribe;
+ }, [isNativeUpdater]);
+
+ // Browser mode: check on mount + every 8 hours
+ useEffect(() => {
+ if (isNativeUpdater) return;
+ checkForUpdatesBrowser();
+ const id = setInterval(checkForUpdatesBrowser, CHECK_INTERVAL);
+ return () => clearInterval(id);
+ }, [isNativeUpdater, checkForUpdatesBrowser]);
+
const dismissUpdate = useCallback(() => {
setShowDialog(false);
if (updateInfo?.latestVersion) {
@@ -200,12 +301,9 @@ export function AppShell({ children }: { children: React.ReactNode }) {
}
}, [updateInfo]);
- // Check on mount + every 8 hours
- useEffect(() => {
- checkForUpdates();
- const id = setInterval(checkForUpdates, CHECK_INTERVAL);
- return () => clearInterval(id);
- }, [checkForUpdates]);
+ const quitAndInstall = useCallback(() => {
+ window.electronAPI?.updater?.quitAndInstall();
+ }, []);
const updateContextValue = useMemo(
() => ({
@@ -215,8 +313,9 @@ export function AppShell({ children }: { children: React.ReactNode }) {
dismissUpdate,
showDialog,
setShowDialog,
+ quitAndInstall,
}),
- [updateInfo, checking, checkForUpdates, dismissUpdate, showDialog]
+ [updateInfo, checking, checkForUpdates, dismissUpdate, showDialog, quitAndInstall]
);
const panelContextValue = useMemo(
@@ -252,6 +351,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
chatListOpen={chatListOpen}
onToggleChatList={() => setChatListOpen(!chatListOpen)}
hasUpdate={updateInfo?.updateAvailable ?? false}
+ readyToInstall={updateInfo?.readyToInstall ?? false}
skipPermissionsActive={skipPermissionsActive}
/>
@@ -266,6 +366,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
className="h-5 w-full shrink-0"
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
/>
+
{children}
diff --git a/src/components/layout/NavRail.tsx b/src/components/layout/NavRail.tsx
index 112eb0d..d56ff79 100644
--- a/src/components/layout/NavRail.tsx
+++ b/src/components/layout/NavRail.tsx
@@ -27,6 +27,7 @@ interface NavRailProps {
chatListOpen: boolean;
onToggleChatList: () => void;
hasUpdate?: boolean;
+ readyToInstall?: boolean;
skipPermissionsActive?: boolean;
}
@@ -36,7 +37,7 @@ const navItems = [
{ href: "/settings", label: "Settings", icon: Settings02Icon },
] as const;
-export function NavRail({ onToggleChatList, hasUpdate, skipPermissionsActive }: NavRailProps) {
+export function NavRail({ onToggleChatList, hasUpdate, readyToInstall, skipPermissionsActive }: NavRailProps) {
const pathname = usePathname();
const router = useRouter();
const { theme, setTheme } = useTheme();
@@ -103,7 +104,10 @@ export function NavRail({ onToggleChatList, hasUpdate, skipPermissionsActive }:
{item.href === "/settings" && hasUpdate && (
-
+
)}
)}
diff --git a/src/components/layout/UpdateBanner.tsx b/src/components/layout/UpdateBanner.tsx
new file mode 100644
index 0000000..9351344
--- /dev/null
+++ b/src/components/layout/UpdateBanner.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { useUpdate } from "@/hooks/useUpdate";
+import { useTranslation } from "@/hooks/useTranslation";
+
+export function UpdateBanner() {
+ const { updateInfo, quitAndInstall } = useUpdate();
+ const { t } = useTranslation();
+
+ if (!updateInfo?.isNativeUpdate || !updateInfo.readyToInstall) return null;
+
+ return (
+
+ {t('update.readyToInstall', { version: updateInfo.latestVersion })}
+
+
+ );
+}
diff --git a/src/components/layout/UpdateDialog.tsx b/src/components/layout/UpdateDialog.tsx
index c867209..1bebc2c 100644
--- a/src/components/layout/UpdateDialog.tsx
+++ b/src/components/layout/UpdateDialog.tsx
@@ -11,19 +11,23 @@ import {
DialogFooter,
} from "@/components/ui/dialog";
import { useUpdate } from "@/hooks/useUpdate";
+import { useTranslation } from "@/hooks/useTranslation";
export function UpdateDialog() {
- const { updateInfo, showDialog, setShowDialog, dismissUpdate } = useUpdate();
+ const { updateInfo, showDialog, dismissUpdate, quitAndInstall } = useUpdate();
+ const { t } = useTranslation();
if (!updateInfo?.updateAvailable) return null;
+ const { isNativeUpdate, readyToInstall, downloadProgress } = updateInfo;
+
return (
+ {/* Download progress bar (native update, downloading) */}
+ {isNativeUpdate && !readyToInstall && downloadProgress != null && downloadProgress > 0 && (
+
+
+
+ {t('update.downloading')} {Math.round(downloadProgress)}%
+
+
+ )}
+
-
+ {!isNativeUpdate ? (
+ // Browser mode: open release page
+
+ ) : readyToInstall ? (
+ // Electron: downloaded, ready to install
+
+ ) : (
+ // Electron: still downloading
+
+ )}
diff --git a/src/components/settings/GeneralSection.tsx b/src/components/settings/GeneralSection.tsx
index a8d6d4e..334b893 100644
--- a/src/components/settings/GeneralSection.tsx
+++ b/src/components/settings/GeneralSection.tsx
@@ -21,7 +21,7 @@ import { SUPPORTED_LOCALES, type Locale } from "@/i18n";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
function UpdateCard() {
- const { updateInfo, checking, checkForUpdates } = useUpdate();
+ const { updateInfo, checking, checkForUpdates, quitAndInstall } = useUpdate();
const { t } = useTranslation();
const currentVersion = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0";
@@ -52,18 +52,33 @@ function UpdateCard() {
{updateInfo.updateAvailable ? (
-
+
- {t('settings.updateAvailable', { version: updateInfo.latestVersion })}
+ {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 })}
-
+ {updateInfo.readyToInstall ? (
+
+ ) : !updateInfo.isNativeUpdate ? (
+
+ ) : null}
) : (
{t('settings.latestVersion')}
diff --git a/src/hooks/useUpdate.ts b/src/hooks/useUpdate.ts
index 65f4039..bf9cc1f 100644
--- a/src/hooks/useUpdate.ts
+++ b/src/hooks/useUpdate.ts
@@ -10,6 +10,9 @@ export interface UpdateInfo {
releaseNotes: string;
releaseUrl: string;
publishedAt: string;
+ downloadProgress: number | null;
+ readyToInstall: boolean;
+ isNativeUpdate: boolean;
}
export interface UpdateContextValue {
@@ -19,6 +22,7 @@ export interface UpdateContextValue {
dismissUpdate: () => void;
showDialog: boolean;
setShowDialog: (v: boolean) => void;
+ quitAndInstall: () => void;
}
export const UpdateContext = createContext
(null);
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index a921863..8ad9cc4 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -299,6 +299,14 @@ const en = {
'error.tryAgain': 'Try Again',
'error.reloadApp': 'Reload App',
+ // ── Update ─────────────────────────────────────────────────
+ 'update.newVersionAvailable': 'New Version Available',
+ 'update.downloading': 'Downloading',
+ 'update.restartToUpdate': 'Restart to Update',
+ 'update.restartNow': 'Restart Now',
+ 'update.readyToInstall': 'CodePilot v{version} is ready — restart to update',
+ 'update.later': 'Later',
+
// ── CLI dynamic field labels ──────────────────────────────
'cli.loadingSettings': 'Loading settings...',
'cli.field.skipDangerousModePermissionPrompt': 'Skip Dangerous Mode Permission Prompt',
diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts
index 7e2433d..0f4a1c7 100644
--- a/src/i18n/zh.ts
+++ b/src/i18n/zh.ts
@@ -296,6 +296,14 @@ const zh: Record = {
'error.tryAgain': '重试',
'error.reloadApp': '重新加载',
+ // ── Update ─────────────────────────────────────────────────
+ 'update.newVersionAvailable': '有新版本可用',
+ 'update.downloading': '下载中',
+ 'update.restartToUpdate': '重启以更新',
+ 'update.restartNow': '立即重启',
+ 'update.readyToInstall': 'CodePilot v{version} 已就绪 — 重启以完成更新',
+ 'update.later': '稍后',
+
// ── CLI dynamic field labels ──────────────────────────────
'cli.loadingSettings': '加载设置中...',
'cli.field.skipDangerousModePermissionPrompt': '跳过危险模式权限提示',
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts
index 204b7e5..ce94b02 100644
--- a/src/types/electron.d.ts
+++ b/src/types/electron.d.ts
@@ -17,6 +17,29 @@ interface ElectronInstallAPI {
onProgress: (callback: (data: any) => void) => () => void;
}
+interface UpdateStatusEvent {
+ status: 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
+ info?: {
+ version: string;
+ releaseNotes?: string | { version: string; note: string }[] | null;
+ releaseName?: string | null;
+ releaseDate?: string;
+ };
+ progress?: {
+ percent: number;
+ bytesPerSecond: number;
+ transferred: number;
+ total: number;
+ };
+ error?: string;
+}
+
+interface ElectronUpdaterAPI {
+ checkForUpdates: () => Promise;
+ quitAndInstall: () => Promise;
+ onStatus: (callback: (data: UpdateStatusEvent) => void) => () => void;
+}
+
interface ElectronAPI {
versions: {
electron: string;
@@ -33,6 +56,7 @@ interface ElectronAPI {
}) => Promise<{ canceled: boolean; filePaths: string[] }>;
};
install: ElectronInstallAPI;
+ updater?: ElectronUpdaterAPI;
}
declare global {