Implement Electron app auto-updater (Phase 1 & 2)

Phase 1: Update Notifications & Download
- Install electron-updater dependency
- Add GitHub publish configuration to package.json

Phase 2: Auto-Download & Install
- Backend: Main process auto-updater with electron-updater
- Frontend: AppUpdateNotification UI component
- IPC: Full update workflow (check/download/install/events)

Backend Implementation:
- Created app-updater.ts with autoUpdater configuration
- Auto-download enabled, checks every 4 hours
- Events: update-available, update-downloaded, download-progress
- Created app-update-handlers.ts with IPC handlers
- Integrated into main/index.ts (production only)

Frontend Implementation:
- Created AppUpdateNotification.tsx with modal dialog
- Shows version, release notes, download progress
- Download workflow: Available → Downloading → Downloaded → Install
- Created app-update-api.ts preload bindings
- Integrated into App.tsx root level

Configuration:
- Added 8 IPC channels: APP_UPDATE_* operations & events
- Created AppUpdateInfo, AppUpdateProgress, AppUpdate*Event types
- GitHub release publishing: provider=github, repo=Auto-Claude

Features:
 Automatic update checking (3s after launch, every 4 hours)
 Auto-download updates in background with progress bar
 User notification with version & release notes
 One-click install and restart
 Dismissible with "Remind Me Later"
 Error handling & visual feedback
 Production-only (disabled in dev mode)

Non-technical users can now update the Electron app with one click!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
AndyMik90
2025-12-18 09:37:22 +01:00
parent d33a0aaff6
commit efc112a313
14 changed files with 2546 additions and 27 deletions
+312
View File
@@ -0,0 +1,312 @@
# Auto Claude Update System Analysis
## Current State
The app has **TWO separate update systems** for different components:
### 1. ✅ Auto Claude Framework Updates (WORKING)
**What it updates:** The Python framework source code (`auto-claude/` directory)
**How it works:**
- Checks GitHub Releases API for new versions
- Downloads release tarball
- Extracts and applies update to the bundled source
- Preserves user configuration files (.env, etc.)
**User Experience:**
- **Settings > Advanced > Updates** section
- Visual update checker with version display
- Release notes rendered in UI
- Progress bar during download
- One-click update button
- Works across all platforms (macOS, Windows, Linux)
**Files:**
- `auto-claude-ui/src/main/auto-claude-updater.ts` - Main updater module
- `auto-claude-ui/src/main/updater/update-checker.ts` - Update checking
- `auto-claude-ui/src/main/updater/update-installer.ts` - Download & install
- `auto-claude-ui/src/main/ipc-handlers/autobuild-source-handlers.ts` - IPC handlers
- `auto-claude-ui/src/renderer/components/settings/AdvancedSettings.tsx` - UI
**Status:****FULLY FUNCTIONAL** - Non-technical users can update the framework with one click!
---
### 2. ❌ Electron App Updates (NOT IMPLEMENTED)
**What it updates:** The Electron application itself (Auto Claude UI)
**Current State:**
- ❌ No `electron-updater` dependency installed
- ❌ No auto-update configuration in electron-builder
- ❌ No update checking in main process
- ❌ No UI for app update notifications
- ❌ Users must manually download new releases from GitHub
**What users currently need to do:**
1. Go to GitHub Releases page
2. Download the appropriate installer (.dmg, .exe, .AppImage, etc.)
3. Run the installer
4. Manually replace the old app
---
## What Needs to Be Implemented
To enable automatic Electron app updates for non-technical users, we need to add:
### 1. Install electron-updater
```bash
npm install electron-updater
```
### 2. Configure electron-builder for Publishing
Add to `package.json` build config:
```json
{
"build": {
"publish": [
{
"provider": "github",
"owner": "AndyMik90",
"repo": "Auto-Claude"
}
]
}
}
```
### 3. Implement Auto-Update Logic in Main Process
Create `auto-claude-ui/src/main/app-updater.ts`:
```typescript
import { autoUpdater } from 'electron-updater';
import { app, BrowserWindow } from 'electron';
export function initializeAppUpdater(mainWindow: BrowserWindow) {
// Configure update checking
autoUpdater.autoDownload = false; // Let user decide
autoUpdater.autoInstallOnAppQuit = true;
// Check for updates on launch (after 3 seconds)
setTimeout(() => {
autoUpdater.checkForUpdates();
}, 3000);
// Check periodically (every 4 hours)
setInterval(() => {
autoUpdater.checkForUpdates();
}, 4 * 60 * 60 * 1000);
// Event handlers
autoUpdater.on('update-available', (info) => {
mainWindow.webContents.send('app-update-available', {
version: info.version,
releaseNotes: info.releaseNotes,
releaseDate: info.releaseDate
});
});
autoUpdater.on('update-downloaded', (info) => {
mainWindow.webContents.send('app-update-downloaded', {
version: info.version
});
});
autoUpdater.on('error', (error) => {
console.error('App update error:', error);
});
autoUpdater.on('download-progress', (progress) => {
mainWindow.webContents.send('app-update-progress', {
percent: progress.percent,
transferred: progress.transferred,
total: progress.total
});
});
}
// IPC handlers
export function registerAppUpdateHandlers() {
ipcMain.handle('app-update-download', async () => {
await autoUpdater.downloadUpdate();
});
ipcMain.handle('app-update-install', () => {
autoUpdater.quitAndInstall();
});
ipcMain.handle('app-update-check', async () => {
return await autoUpdater.checkForUpdates();
});
}
```
### 4. Add UI Notification Component
Create an update banner or modal in the renderer that:
- Shows when app update is available
- Displays version and release notes
- Has "Download Update" button
- Shows download progress
- Has "Install and Restart" button after download
### 5. Update GitHub Release Workflow
Ensure GitHub releases are created with proper assets:
- macOS: `.dmg` and `.zip` files + `latest-mac.yml`
- Windows: `.exe` installer + `latest.yml`
- Linux: `.AppImage` and `.deb` + `latest-linux.yml`
The `latest-*.yml` files are auto-generated by electron-builder and contain update metadata.
---
## Implementation Strategy
### Option A: Full Auto-Update (Recommended)
**Pros:**
- Best user experience
- Automatic background downloads
- One-click install
- Industry standard
**Cons:**
- Requires code signing certificates for production (macOS, Windows)
- Without signing, users get security warnings
### Option B: Update Notification Only
**Pros:**
- Simpler implementation
- No code signing required
- User downloads from GitHub (trusted source)
**Cons:**
- Users still need to manually download and install
- Less convenient than auto-update
**Implementation:**
```typescript
// Just notify, don't auto-download
autoUpdater.autoDownload = false;
autoUpdater.on('update-available', (info) => {
// Show notification with link to GitHub Releases
mainWindow.webContents.send('app-update-available', {
version: info.version,
downloadUrl: `https://github.com/AndyMik90/Auto-Claude/releases/tag/v${info.version}`
});
});
```
---
## Development vs Production
**Important:** `electron-updater` only works in **packaged apps**, not in development mode.
During development:
```typescript
if (app.isPackaged) {
initializeAppUpdater(mainWindow);
} else {
console.log('[Dev] Auto-updater disabled in development mode');
}
```
---
## Code Signing Requirements
For production auto-updates without security warnings:
### macOS
- Requires Apple Developer account ($99/year)
- Code signing certificate
- Notarization with Apple
### Windows
- Requires code signing certificate (~$200-400/year)
- Without: Windows SmartScreen warnings
### Linux
- No code signing required
- Users may need to mark `.AppImage` as executable
---
## Testing Auto-Updates
1. **Local Testing:**
- Build and package: `npm run package`
- Create local update server or use GitHub Releases
- Test with different versions
2. **GitHub Releases Testing:**
- Create a draft release on GitHub
- Publish with version tag (e.g., `v2.4.0`)
- electron-builder automatically uploads assets
- Test with previous version installed
---
## Recommended Next Steps
1. **Phase 1: Add Update Notification** (Quick win)
- Install `electron-updater`
- Add basic update checking
- Show notification with link to GitHub Releases
- No auto-download, users download manually
2. **Phase 2: Enable Auto-Download** (Better UX)
- Add download progress UI
- Enable auto-download of updates
- Add "Install and Restart" button
3. **Phase 3: Code Signing** (Production ready)
- Acquire code signing certificates
- Configure signing in electron-builder
- Notarize macOS builds
- Sign Windows builds
---
## Current Framework Update Flow (Already Working!)
For reference, here's how the existing Auto Claude framework updater works:
1. User opens **Settings > Advanced > Updates**
2. App checks GitHub Releases API
3. If update available, shows version + release notes
4. User clicks "Download Update"
5. Progress bar shows download status
6. Update is extracted and applied to bundled source
7. User configuration (.env) is preserved
8. Done - no restart needed!
**This same UX could be replicated for app updates!**
---
## Summary
| Component | Status | User Experience |
|-----------|--------|-----------------|
| Auto Claude Framework | ✅ Working | One-click update in Settings |
| Electron App (UI) | ❌ Missing | Must download from GitHub manually |
**Recommendation:** Implement electron-updater with update notifications (Phase 1) as a quick win. This will enable non-technical users to update the app without using git or terminal commands.
**Estimated effort:**
- Phase 1 (Notifications): 2-4 hours
- Phase 2 (Auto-download): 2-3 hours
- Phase 3 (Code signing): Varies by platform
The existing framework updater code provides an excellent reference for the UI implementation!
+1541 -21
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -55,6 +55,7 @@
"chokidar": "^5.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"electron-updater": "^6.6.2",
"ioredis": "^5.8.2",
"lucide-react": "^0.560.0",
"motion": "^12.23.26",
@@ -114,6 +115,13 @@
"build": {
"appId": "com.autoclaude.ui",
"productName": "Auto Claude",
"publish": [
{
"provider": "github",
"owner": "AndyMik90",
"repo": "Auto-Claude"
}
],
"directories": {
"output": "dist",
"buildResources": "resources"
+191
View File
@@ -0,0 +1,191 @@
/**
* Electron App Auto-Updater
*
* Manages automatic updates for the packaged Electron application using electron-updater.
* Updates are published through GitHub Releases and automatically downloaded and installed.
*
* Update flow:
* 1. Check for updates 3 seconds after app launch
* 2. Download updates automatically when available
* 3. Notify user when update is downloaded
* 4. Install and restart when user confirms
*
* Events sent to renderer:
* - APP_UPDATE_AVAILABLE: New update available (with version info)
* - APP_UPDATE_DOWNLOADED: Update downloaded and ready to install
* - APP_UPDATE_PROGRESS: Download progress updates
* - APP_UPDATE_ERROR: Error during update process
*/
import { autoUpdater } from 'electron-updater';
import type { BrowserWindow } from 'electron';
import { IPC_CHANNELS } from '../shared/constants';
import type { AppUpdateInfo } from '../shared/types';
// Configure electron-updater
autoUpdater.autoDownload = true; // Automatically download updates when available
autoUpdater.autoInstallOnAppQuit = true; // Automatically install on app quit
let mainWindow: BrowserWindow | null = null;
/**
* Initialize the app updater system
*
* Sets up event handlers and starts periodic update checks.
* Should only be called in production (app.isPackaged).
*
* @param window - The main BrowserWindow for sending update events
*/
export function initializeAppUpdater(window: BrowserWindow): void {
mainWindow = window;
// Log updater configuration
console.log('[app-updater] Initializing app auto-updater');
console.log('[app-updater] Current version:', autoUpdater.currentVersion.version);
console.log('[app-updater] Auto-download enabled:', autoUpdater.autoDownload);
// ============================================
// Event Handlers
// ============================================
// Update available - new version found
autoUpdater.on('update-available', (info) => {
console.log('[app-updater] Update available:', info.version);
if (mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_AVAILABLE, {
version: info.version,
releaseNotes: info.releaseNotes,
releaseDate: info.releaseDate
});
}
});
// Update downloaded - ready to install
autoUpdater.on('update-downloaded', (info) => {
console.log('[app-updater] Update downloaded:', info.version);
if (mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_DOWNLOADED, {
version: info.version,
releaseNotes: info.releaseNotes,
releaseDate: info.releaseDate
});
}
});
// Download progress
autoUpdater.on('download-progress', (progress) => {
console.log(`[app-updater] Download progress: ${progress.percent.toFixed(2)}%`);
if (mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_PROGRESS, {
percent: progress.percent,
bytesPerSecond: progress.bytesPerSecond,
transferred: progress.transferred,
total: progress.total
});
}
});
// Error handling
autoUpdater.on('error', (error) => {
console.error('[app-updater] Update error:', error);
if (mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_ERROR, {
message: error.message,
stack: error.stack
});
}
});
// No update available
autoUpdater.on('update-not-available', (info) => {
console.log('[app-updater] No updates available. Current version:', info.version);
});
// Checking for updates
autoUpdater.on('checking-for-update', () => {
console.log('[app-updater] Checking for updates...');
});
// ============================================
// Update Check Schedule
// ============================================
// Check for updates 3 seconds after launch
setTimeout(() => {
console.log('[app-updater] Performing initial update check');
autoUpdater.checkForUpdates().catch((error) => {
console.error('[app-updater] Initial update check failed:', error);
});
}, 3000);
// Check for updates every 4 hours
const FOUR_HOURS = 4 * 60 * 60 * 1000;
setInterval(() => {
console.log('[app-updater] Performing periodic update check');
autoUpdater.checkForUpdates().catch((error) => {
console.error('[app-updater] Periodic update check failed:', error);
});
}, FOUR_HOURS);
console.log('[app-updater] Auto-updater initialized successfully');
}
/**
* Manually check for updates
* Called from IPC handler when user requests manual check
*/
export async function checkForUpdates(): Promise<AppUpdateInfo | null> {
try {
console.log('[app-updater] Manual update check requested');
const result = await autoUpdater.checkForUpdates();
if (!result) {
return null;
}
const updateAvailable = result.updateInfo.version !== autoUpdater.currentVersion.version;
if (!updateAvailable) {
return null;
}
return {
version: result.updateInfo.version,
releaseNotes: result.updateInfo.releaseNotes as string | undefined,
releaseDate: result.updateInfo.releaseDate
};
} catch (error) {
console.error('[app-updater] Manual update check failed:', error);
throw error;
}
}
/**
* Manually download update
* Called from IPC handler when user requests manual download
*/
export async function downloadUpdate(): Promise<void> {
try {
console.log('[app-updater] Manual update download requested');
await autoUpdater.downloadUpdate();
} catch (error) {
console.error('[app-updater] Manual update download failed:', error);
throw error;
}
}
/**
* Quit and install update
* Called from IPC handler when user confirms installation
*/
export function quitAndInstall(): void {
console.log('[app-updater] Quitting and installing update');
autoUpdater.quitAndInstall(false, true);
}
/**
* Get current app version
*/
export function getCurrentVersion(): string {
return autoUpdater.currentVersion.version;
}
+9
View File
@@ -7,6 +7,7 @@ import { TerminalManager } from './terminal-manager';
import { pythonEnvManager } from './python-env-manager';
import { getUsageMonitor } from './claude-profile/usage-monitor';
import { initializeUsageMonitorForwarding } from './ipc-handlers/terminal-handlers';
import { initializeAppUpdater } from './app-updater';
// Get icon path based on platform
function getIconPath(): string {
@@ -137,6 +138,14 @@ app.whenReady().then(() => {
const usageMonitor = getUsageMonitor();
usageMonitor.start();
console.log('[main] Usage monitor initialized and started');
// Initialize app auto-updater (only in production)
if (app.isPackaged) {
initializeAppUpdater(mainWindow);
console.log('[main] App auto-updater initialized');
} else {
console.log('[main] App auto-updater disabled in development mode');
}
}
// macOS: re-create window when dock icon is clicked
@@ -0,0 +1,106 @@
/**
* App Update IPC Handlers
*
* Handles IPC communication for Electron app auto-updates.
* Provides manual controls for checking, downloading, and installing updates.
*/
import { ipcMain } from 'electron';
import { IPC_CHANNELS } from '../../shared/constants';
import type { IPCResult, AppUpdateInfo } from '../../shared/types';
import {
checkForUpdates,
downloadUpdate,
quitAndInstall,
getCurrentVersion
} from '../app-updater';
/**
* Register all app-update-related IPC handlers
*/
export function registerAppUpdateHandlers(): void {
console.log('[IPC] Registering app update handlers');
// ============================================
// App Update Operations
// ============================================
/**
* APP_UPDATE_CHECK: Manually check for updates
* Returns update availability and version information
*/
ipcMain.handle(
IPC_CHANNELS.APP_UPDATE_CHECK,
async (): Promise<IPCResult<AppUpdateInfo | null>> => {
try {
const result = await checkForUpdates();
return { success: true, data: result };
} catch (error) {
console.error('[app-update-handlers] Check for updates failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to check for updates'
};
}
}
);
/**
* APP_UPDATE_DOWNLOAD: Manually download update
* Triggers download of available update
*/
ipcMain.handle(
IPC_CHANNELS.APP_UPDATE_DOWNLOAD,
async (): Promise<IPCResult> => {
try {
await downloadUpdate();
return { success: true };
} catch (error) {
console.error('[app-update-handlers] Download update failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to download update'
};
}
}
);
/**
* APP_UPDATE_INSTALL: Quit and install update
* Quits the app and installs the downloaded update
*/
ipcMain.handle(
IPC_CHANNELS.APP_UPDATE_INSTALL,
async (): Promise<IPCResult> => {
try {
quitAndInstall();
return { success: true };
} catch (error) {
console.error('[app-update-handlers] Install update failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to install update'
};
}
}
);
/**
* APP_UPDATE_GET_VERSION: Get current app version
* Returns the current application version
*/
ipcMain.handle(
IPC_CHANNELS.APP_UPDATE_GET_VERSION,
async (): Promise<string> => {
try {
const version = getCurrentVersion();
return version;
} catch (error) {
console.error('[app-update-handlers] Get version failed:', error);
throw error;
}
}
);
console.log('[IPC] App update handlers registered successfully');
}
@@ -27,6 +27,7 @@ import { registerIdeationHandlers } from './ideation-handlers';
import { registerChangelogHandlers } from './changelog-handlers';
import { registerInsightsHandlers } from './insights-handlers';
import { registerDockerHandlers } from './docker-handlers';
import { registerAppUpdateHandlers } from './app-update-handlers';
import { notificationService } from '../notification-service';
/**
@@ -94,6 +95,9 @@ export function setupIpcHandlers(
// Docker & infrastructure handlers (for Graphiti/FalkorDB)
registerDockerHandlers();
// App auto-update handlers
registerAppUpdateHandlers();
console.log('[IPC] All handler modules registered successfully');
}
@@ -114,5 +118,6 @@ export {
registerIdeationHandlers,
registerChangelogHandlers,
registerInsightsHandlers,
registerDockerHandlers
registerDockerHandlers,
registerAppUpdateHandlers
};
@@ -0,0 +1,67 @@
import { IPC_CHANNELS } from '../../shared/constants';
import type {
AppUpdateInfo,
AppUpdateProgress,
AppUpdateAvailableEvent,
AppUpdateDownloadedEvent,
IPCResult
} from '../../shared/types';
import { createIpcListener, invokeIpc, IpcListenerCleanup } from './modules/ipc-utils';
/**
* App Auto-Update API operations
* Handles Electron app updates using electron-updater
*/
export interface AppUpdateAPI {
// Operations
checkAppUpdate: () => Promise<IPCResult<AppUpdateInfo | null>>;
downloadAppUpdate: () => Promise<IPCResult>;
installAppUpdate: () => void;
getAppVersion: () => Promise<string>;
// Event Listeners
onAppUpdateAvailable: (
callback: (info: AppUpdateAvailableEvent) => void
) => IpcListenerCleanup;
onAppUpdateDownloaded: (
callback: (info: AppUpdateDownloadedEvent) => void
) => IpcListenerCleanup;
onAppUpdateProgress: (
callback: (progress: AppUpdateProgress) => void
) => IpcListenerCleanup;
}
/**
* Creates the App Auto-Update API implementation
*/
export const createAppUpdateAPI = (): AppUpdateAPI => ({
// Operations
checkAppUpdate: (): Promise<IPCResult<AppUpdateInfo | null>> =>
invokeIpc(IPC_CHANNELS.APP_UPDATE_CHECK),
downloadAppUpdate: (): Promise<IPCResult> =>
invokeIpc(IPC_CHANNELS.APP_UPDATE_DOWNLOAD),
installAppUpdate: (): void => {
invokeIpc(IPC_CHANNELS.APP_UPDATE_INSTALL);
},
getAppVersion: (): Promise<string> =>
invokeIpc(IPC_CHANNELS.APP_UPDATE_GET_VERSION),
// Event Listeners
onAppUpdateAvailable: (
callback: (info: AppUpdateAvailableEvent) => void
): IpcListenerCleanup =>
createIpcListener(IPC_CHANNELS.APP_UPDATE_AVAILABLE, callback),
onAppUpdateDownloaded: (
callback: (info: AppUpdateDownloadedEvent) => void
): IpcListenerCleanup =>
createIpcListener(IPC_CHANNELS.APP_UPDATE_DOWNLOADED, callback),
onAppUpdateProgress: (
callback: (progress: AppUpdateProgress) => void
): IpcListenerCleanup =>
createIpcListener(IPC_CHANNELS.APP_UPDATE_PROGRESS, callback)
});
+9 -4
View File
@@ -5,6 +5,7 @@ import { SettingsAPI, createSettingsAPI } from './settings-api';
import { FileAPI, createFileAPI } from './file-api';
import { AgentAPI, createAgentAPI } from './agent-api';
import { IdeationAPI, createIdeationAPI } from './modules/ideation-api';
import { AppUpdateAPI, createAppUpdateAPI } from './app-update-api';
export interface ElectronAPI extends
ProjectAPI,
@@ -13,7 +14,8 @@ export interface ElectronAPI extends
SettingsAPI,
FileAPI,
AgentAPI,
IdeationAPI {}
IdeationAPI,
AppUpdateAPI {}
export const createElectronAPI = (): ElectronAPI => ({
...createProjectAPI(),
@@ -22,7 +24,8 @@ export const createElectronAPI = (): ElectronAPI => ({
...createSettingsAPI(),
...createFileAPI(),
...createAgentAPI(),
...createIdeationAPI()
...createIdeationAPI(),
...createAppUpdateAPI()
});
// Export individual API creators for potential use in tests or specialized contexts
@@ -33,7 +36,8 @@ export {
createSettingsAPI,
createFileAPI,
createAgentAPI,
createIdeationAPI
createIdeationAPI,
createAppUpdateAPI
};
export type {
@@ -43,5 +47,6 @@ export type {
SettingsAPI,
FileAPI,
AgentAPI,
IdeationAPI
IdeationAPI,
AppUpdateAPI
};
+4
View File
@@ -34,6 +34,7 @@ import { WelcomeScreen } from './components/WelcomeScreen';
import { RateLimitModal } from './components/RateLimitModal';
import { SDKRateLimitModal } from './components/SDKRateLimitModal';
import { OnboardingWizard } from './components/onboarding';
import { AppUpdateNotification } from './components/AppUpdateNotification';
import { UsageIndicator } from './components/UsageIndicator';
import { ProactiveSwapListener } from './components/ProactiveSwapListener';
import { useProjectStore, loadProjects, addProject, initializeProject } from './stores/project-store';
@@ -479,6 +480,9 @@ export function App() {
setIsSettingsDialogOpen(true);
}}
/>
{/* App Update Notification - shows when new app version is available */}
<AppUpdateNotification />
</div>
</TooltipProvider>
);
@@ -0,0 +1,252 @@
import { useState, useEffect, useMemo } from 'react';
import {
Download,
RefreshCw,
CheckCircle2,
AlertCircle,
ExternalLink
} from 'lucide-react';
import { Button } from './ui/button';
import { Progress } from './ui/progress';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from './ui/dialog';
import type {
AppUpdateAvailableEvent,
AppUpdateDownloadedEvent,
AppUpdateProgress
} from '../../shared/types';
/**
* Simple markdown renderer for release notes
* Handles: headers, bold, lists, line breaks
*/
function ReleaseNotesRenderer({ markdown }: { markdown: string }) {
const html = useMemo(() => {
const result = markdown
// Escape HTML
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// Headers (### Header -> <h3>)
.replace(/^### (.+)$/gm, '<h4 class="text-sm font-semibold text-foreground mt-3 mb-1.5 first:mt-0">$1</h4>')
.replace(/^## (.+)$/gm, '<h3 class="text-sm font-semibold text-foreground mt-3 mb-1.5 first:mt-0">$1</h3>')
// Bold (**text** -> <strong>)
.replace(/\*\*([^*]+)\*\*/g, '<strong class="text-foreground font-medium">$1</strong>')
// Inline code (`code` -> <code>)
.replace(/`([^`]+)`/g, '<code class="px-1 py-0.5 bg-muted rounded text-xs">$1</code>')
// List items (- item -> <li>)
.replace(/^- (.+)$/gm, '<li class="ml-4 text-muted-foreground before:content-[\'•\'] before:mr-2 before:text-muted-foreground/60">$1</li>')
// Wrap consecutive list items
.replace(/(<li[^>]*>.*?<\/li>\n?)+/g, '<ul class="space-y-1 my-2">$&</ul>')
// Line breaks for remaining lines
.replace(/\n\n/g, '<div class="h-2"></div>')
.replace(/\n/g, '<br/>');
return result;
}, [markdown]);
return (
<div
className="text-sm text-muted-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
/**
* App Update Notification Dialog
* Shows when a new app version is available and handles download/install workflow
*/
export function AppUpdateNotification() {
const [isOpen, setIsOpen] = useState(false);
const [updateInfo, setUpdateInfo] = useState<AppUpdateAvailableEvent | null>(null);
const [downloadProgress, setDownloadProgress] = useState<AppUpdateProgress | null>(null);
const [isDownloading, setIsDownloading] = useState(false);
const [isDownloaded, setIsDownloaded] = useState(false);
const [downloadError, setDownloadError] = useState<string | null>(null);
// Listen for update available event
useEffect(() => {
const cleanup = window.electronAPI.onAppUpdateAvailable((info) => {
setUpdateInfo(info);
setIsOpen(true);
setIsDownloading(false);
setIsDownloaded(false);
setDownloadProgress(null);
setDownloadError(null);
});
return cleanup;
}, []);
// Listen for update downloaded event
useEffect(() => {
const cleanup = window.electronAPI.onAppUpdateDownloaded((info) => {
setIsDownloading(false);
setIsDownloaded(true);
setDownloadProgress(null);
});
return cleanup;
}, []);
// Listen for download progress
useEffect(() => {
const cleanup = window.electronAPI.onAppUpdateProgress((progress) => {
setDownloadProgress(progress);
});
return cleanup;
}, []);
const handleDownload = async () => {
setIsDownloading(true);
setDownloadError(null);
try {
const result = await window.electronAPI.downloadAppUpdate();
if (!result.success) {
setDownloadError(result.error || 'Failed to download update');
setIsDownloading(false);
}
} catch (error) {
console.error('Failed to download app update:', error);
setDownloadError('Failed to download update');
setIsDownloading(false);
}
};
const handleInstall = () => {
window.electronAPI.installAppUpdate();
};
const handleDismiss = () => {
setIsOpen(false);
};
if (!updateInfo) {
return null;
}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Download className="h-5 w-5" />
App Update Available
</DialogTitle>
<DialogDescription>
A new version of Auto Claude is ready to download
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Version Info */}
<div className="rounded-lg border border-border bg-muted/50 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
New Version
</p>
<p className="text-base font-medium text-foreground">
{updateInfo.version}
</p>
{updateInfo.releaseDate && (
<p className="text-xs text-muted-foreground mt-1">
Released {new Date(updateInfo.releaseDate).toLocaleDateString()}
</p>
)}
</div>
{isDownloaded ? (
<CheckCircle2 className="h-6 w-6 text-success" />
) : isDownloading ? (
<RefreshCw className="h-6 w-6 animate-spin text-info" />
) : (
<Download className="h-6 w-6 text-info" />
)}
</div>
</div>
{/* Release Notes */}
{updateInfo.releaseNotes && (
<div className="bg-background rounded-lg p-4 max-h-64 overflow-y-auto border border-border/50">
<ReleaseNotesRenderer markdown={updateInfo.releaseNotes} />
</div>
)}
{/* Download Progress */}
{isDownloading && downloadProgress && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Downloading...</span>
<span className="text-foreground font-medium">
{Math.round(downloadProgress.percent)}%
</span>
</div>
<Progress value={downloadProgress.percent} className="h-2" />
<p className="text-xs text-muted-foreground text-right">
{(downloadProgress.transferred / 1024 / 1024).toFixed(2)} MB / {(downloadProgress.total / 1024 / 1024).toFixed(2)} MB
</p>
</div>
)}
{/* Download Error */}
{downloadError && (
<div className="flex items-center gap-3 text-sm text-destructive bg-destructive/10 border border-destructive/30 rounded-lg p-3">
<AlertCircle className="h-5 w-5 shrink-0" />
<span>{downloadError}</span>
</div>
)}
{/* Downloaded Success */}
{isDownloaded && (
<div className="flex items-center gap-3 text-sm text-success bg-success/10 border border-success/30 rounded-lg p-3">
<CheckCircle2 className="h-5 w-5 shrink-0" />
<span>Update downloaded successfully! Click Install to restart and apply the update.</span>
</div>
)}
</div>
<DialogFooter className="flex flex-col sm:flex-row gap-3">
<Button
variant="outline"
onClick={handleDismiss}
disabled={isDownloading}
>
{isDownloaded ? 'Install Later' : 'Remind Me Later'}
</Button>
{isDownloaded ? (
<Button onClick={handleInstall}>
<RefreshCw className="mr-2 h-4 w-4" />
Install and Restart
</Button>
) : (
<Button
onClick={handleDownload}
disabled={isDownloading}
>
{isDownloading ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Downloading...
</>
) : (
<>
<Download className="mr-2 h-4 w-4" />
Download Update
</>
)}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+13 -1
View File
@@ -257,5 +257,17 @@ export const IPC_CHANNELS = {
// Git operations
GIT_GET_BRANCHES: 'git:getBranches',
GIT_GET_CURRENT_BRANCH: 'git:getCurrentBranch',
GIT_DETECT_MAIN_BRANCH: 'git:detectMainBranch'
GIT_DETECT_MAIN_BRANCH: 'git:detectMainBranch',
// App auto-update operations
APP_UPDATE_CHECK: 'app-update:check',
APP_UPDATE_DOWNLOAD: 'app-update:download',
APP_UPDATE_INSTALL: 'app-update:install',
APP_UPDATE_GET_VERSION: 'app-update:get-version',
// App auto-update events (main -> renderer)
APP_UPDATE_AVAILABLE: 'app-update:available',
APP_UPDATE_DOWNLOADED: 'app-update:downloaded',
APP_UPDATE_PROGRESS: 'app-update:progress',
APP_UPDATE_ERROR: 'app-update:error'
} as const;
@@ -0,0 +1,25 @@
/**
* Types for Electron app auto-update functionality
*/
export interface AppUpdateInfo {
version: string;
releaseNotes?: string;
releaseDate?: string;
}
export interface AppUpdateProgress {
percent: number;
transferred: number;
total: number;
}
export interface AppUpdateAvailableEvent {
version: string;
releaseNotes?: string;
releaseDate?: string;
}
export interface AppUpdateDownloadedEvent {
version: string;
}
+3
View File
@@ -15,6 +15,9 @@ export * from './changelog';
export * from './insights';
export * from './roadmap';
export * from './integrations';
export * from './glossary';
export * from './status';
export * from './app-update';
// IPC types (must be last to use types from other modules)
export * from './ipc';