From e02cf05bafccc3e1afa265533c097071bb47c7b1 Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Thu, 5 Feb 2026 23:36:12 +0800 Subject: [PATCH] feat(update): implement auto-update functionality with electron-updater - Add AppUpdater module with update lifecycle management - Create UpdateSettings UI component with progress display - Add Progress UI component based on Radix UI - Create update Zustand store for state management - Register update IPC handlers in main process - Auto-check for updates on production startup - Add commit documentation for commits 2-6 --- build_process/commit_2_gateway_refinements.md | 83 ++++++ build_process/commit_3_setup_wizard.md | 88 ++++++ .../commit_4_provider_configuration.md | 127 +++++++++ build_process/commit_5_channel_connection.md | 124 ++++++++ build_process/commit_6_auto_update.md | 171 ++++++++++++ build_process/process.md | 3 +- electron/main/index.ts | 13 + electron/main/updater.ts | 264 ++++++++++++++++++ electron/preload/index.ts | 17 +- src/components/settings/UpdateSettings.tsx | 207 ++++++++++++++ src/components/ui/progress.tsx | 26 ++ src/pages/Settings/index.tsx | 56 +--- src/stores/update.ts | 184 ++++++++++++ 13 files changed, 1313 insertions(+), 50 deletions(-) create mode 100644 build_process/commit_2_gateway_refinements.md create mode 100644 build_process/commit_3_setup_wizard.md create mode 100644 build_process/commit_4_provider_configuration.md create mode 100644 build_process/commit_5_channel_connection.md create mode 100644 build_process/commit_6_auto_update.md create mode 100644 electron/main/updater.ts create mode 100644 src/components/settings/UpdateSettings.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/stores/update.ts diff --git a/build_process/commit_2_gateway_refinements.md b/build_process/commit_2_gateway_refinements.md new file mode 100644 index 000000000..289f8b722 --- /dev/null +++ b/build_process/commit_2_gateway_refinements.md @@ -0,0 +1,83 @@ +# Commit 2: Gateway Refinements + +## Summary +Enhance Gateway process management with auto-reconnection, health checks, and improved state management for more robust WebSocket communication. + +## Changes + +### Electron Main Process + +#### `electron/gateway/manager.ts` +- Added `'reconnecting'` state to `GatewayStatus` +- Implemented `ReconnectConfig` with exponential backoff strategy +- Added `maxAttempts`, `baseDelay`, `maxDelay` configuration +- New methods: + - `isConnected()` - Check WebSocket connection status + - `restart()` - Stop and start Gateway + - `clearAllTimers()` - Clean up timers on shutdown + - `startHealthCheck()` - Periodic health monitoring + - `checkHealth()` - Manual health check +- Enhanced `handleMessage()` to dispatch JSON-RPC responses and notifications +- Expanded `GatewayManagerEvents` for notification forwarding + +#### `electron/gateway/client.ts` +- Extended with new interfaces: `SkillBundle`, `CronTask`, `ProviderConfig` +- Added cron task management methods: + - `listCronTasks()`, `createCronTask()`, `updateCronTask()`, `deleteCronTask()`, `runCronTask()` +- Added provider management methods: + - `listProviders()`, `setProvider()`, `removeProvider()`, `testProvider()` +- Enhanced system calls: + - `getHealth()` now includes `version` + - Added `getVersion()`, `getSkillBundles()`, `installBundle()` + +#### `electron/main/ipc-handlers.ts` +- Added `gateway:isConnected`, `gateway:health` IPC handlers +- Added `mainWindow.isDestroyed()` checks before sending events +- Forward new gateway events: `gateway:notification`, `gateway:channel-status`, `gateway:chat-message` + +#### `electron/preload/index.ts` +- Added new IPC channels for gateway operations +- Added notification event channels + +### React Renderer + +#### `src/stores/gateway.ts` +- Added `health: GatewayHealth | null`, `lastError: string | null` to state +- Added `checkHealth()`, `rpc()`, `clearError()` actions +- Enhanced `init()` to listen for error and notification events + +#### `src/types/gateway.ts` +- Added `'reconnecting'` to `GatewayStatus` state enum +- Added `version`, `reconnectAttempts` fields +- New interfaces: `GatewayHealth`, `GatewayNotification`, `ProviderConfig` + +#### `src/components/common/StatusBadge.tsx` +- Added `'reconnecting'` status with warning variant + +## Technical Details + +### Reconnection Strategy +- Exponential backoff: `delay = min(baseDelay * 2^attempt, maxDelay)` +- Default: 1s base delay, 30s max delay, 10 max attempts +- Automatic reconnection on unexpected disconnection +- Manual control via `shouldReconnect` flag + +### Health Check +- Periodic ping/pong via JSON-RPC `system.health` +- Returns status, uptime, version information +- Triggers reconnection on consecutive failures + +### Event Flow +``` +Gateway Process -> WebSocket -> GatewayManager -> IPC -> Renderer + | + v + Event Emitter + | + +---------------+---------------+ + | | | + status notification channel:status +``` + +## Version +v0.1.0-alpha (incremental) diff --git a/build_process/commit_3_setup_wizard.md b/build_process/commit_3_setup_wizard.md new file mode 100644 index 000000000..e69582f72 --- /dev/null +++ b/build_process/commit_3_setup_wizard.md @@ -0,0 +1,88 @@ +# Commit 3: Setup Wizard + +## Summary +Implement a functional multi-step setup wizard for first-run user onboarding, guiding users through environment checks, AI provider configuration, channel connection, and skill selection. + +## Changes + +### React Renderer + +#### `src/pages/Setup/index.tsx` +Complete rewrite with functional implementation: +- **Step 0: Welcome** - Introduction to ClawX +- **Step 1: Runtime Check** - Verify Node.js, OpenClaw, and Gateway status +- **Step 2: Provider Configuration** - Select AI provider and enter API key +- **Step 3: Channel Connection** - Choose messaging channel (WhatsApp, Telegram, etc.) +- **Step 4: Skill Selection** - Toggle predefined skill bundles +- **Step 5: Complete** - Summary of configuration + +New components: +- `WelcomeContent` - Welcome message and features overview +- `RuntimeContent` - Environment checks with real Gateway status +- `ProviderContent` - Provider selection with API key input and validation +- `ChannelContent` - Channel type selection with QR code placeholder +- `SkillsContent` - Skill bundle toggles +- `CompleteContent` - Configuration summary + +Features: +- Animated transitions using Framer Motion +- Progress indicator with step navigation +- Dynamic `canProceed` state based on step requirements +- API key visibility toggle +- Simulated API key validation + +#### `src/stores/settings.ts` +- Added `setupComplete: boolean` to state +- Added `markSetupComplete()` action +- Setup status persisted via Zustand persist middleware + +#### `src/App.tsx` +- Added `useLocation` for route tracking +- Added redirect logic: if `setupComplete` is false, navigate to `/setup` +- Fixed `handleNavigate` callback signature for IPC events + +## Technical Details + +### Setup Flow +``` +Welcome -> Runtime -> Provider -> Channel -> Skills -> Complete + | | | | | | + v v v v v v + [Skip] [Check] [Validate] [Select] [Toggle] [Finish] + | | | | | | + +---------+----------+----------+----------+----------+ + | + markSetupComplete() + | + Navigate to / +``` + +### Provider Types +- Anthropic (Claude) - API key starts with `sk-ant-` +- OpenAI (GPT) - API key starts with `sk-` +- Google (Gemini) - Length validation +- Ollama (Local) - No API key required +- Custom - Configurable base URL + +### Channel Types +- WhatsApp - QR code connection +- Telegram - Bot token +- Discord - Bot token +- Slack - OAuth/Bot token +- WeChat - QR code connection + +### Skill Bundles +- Productivity - Calendar, reminders, notes +- Developer - Code assistance, git operations +- Information - Web search, news, weather +- Entertainment - Music, games, trivia + +## UI/UX Features +- Gradient background with glassmorphism cards +- Step progress dots with completion indicators +- Animated page transitions +- Back/Skip/Next navigation +- Toast notifications on completion + +## Version +v0.1.0-alpha (incremental) diff --git a/build_process/commit_4_provider_configuration.md b/build_process/commit_4_provider_configuration.md new file mode 100644 index 000000000..0e5adde97 --- /dev/null +++ b/build_process/commit_4_provider_configuration.md @@ -0,0 +1,127 @@ +# Commit 4: Provider Configuration + +## Summary +Implement secure API key storage using Electron's safeStorage API and create a comprehensive provider management UI for configuring AI model providers. + +## Changes + +### Electron Main Process + +#### `electron/utils/secure-storage.ts` (New) +Secure storage utility using Electron's safeStorage: +- `isEncryptionAvailable()` - Check if system keychain is available +- `storeApiKey(providerId, apiKey)` - Encrypt and store API key +- `getApiKey(providerId)` - Decrypt and retrieve API key +- `deleteApiKey(providerId)` - Remove stored API key +- `hasApiKey(providerId)` - Check if key exists +- `listStoredKeyIds()` - List all stored provider IDs + +Provider configuration management: +- `saveProvider(config)` - Save provider configuration +- `getProvider(providerId)` - Get single provider +- `getAllProviders()` - Get all providers +- `deleteProvider(providerId)` - Delete provider and key +- `setDefaultProvider(providerId)` - Set default provider +- `getDefaultProvider()` - Get default provider ID +- `getProviderWithKeyInfo(providerId)` - Get provider with masked key +- `getAllProvidersWithKeyInfo()` - Get all providers with key info + +Note: Uses dynamic `import('electron-store')` for ESM compatibility. + +#### `electron/main/ipc-handlers.ts` +New `registerProviderHandlers()` function with IPC handlers: +- `provider:encryptionAvailable` +- `provider:list` / `provider:get` / `provider:save` / `provider:delete` +- `provider:setApiKey` / `provider:deleteApiKey` / `provider:hasApiKey` / `provider:getApiKey` +- `provider:setDefault` / `provider:getDefault` +- `provider:validateKey` - Basic format validation + +#### `electron/preload/index.ts` +Added all provider IPC channels to valid channels list. + +### React Renderer + +#### `src/stores/providers.ts` (New) +Zustand store for provider management: +- State: `providers`, `defaultProviderId`, `loading`, `error` +- Actions: `fetchProviders`, `addProvider`, `updateProvider`, `deleteProvider` +- Actions: `setApiKey`, `deleteApiKey`, `setDefaultProvider`, `validateApiKey`, `getApiKey` + +#### `src/components/settings/ProvidersSettings.tsx` (New) +Provider management UI component: +- `ProvidersSettings` - Main component orchestrating provider list +- `ProviderCard` - Individual provider display with actions +- `AddProviderDialog` - Modal for adding new providers + +Features: +- Provider type icons (Anthropic, OpenAI, Google, Ollama, Custom) +- Masked API key display (first 4 + last 4 characters) +- Set default provider +- Enable/disable providers +- Edit/delete functionality +- API key validation on add + +#### `src/pages/Settings/index.tsx` +- Added AI Providers section with `ProvidersSettings` component +- Added `Key` icon import + +## Technical Details + +### Security Architecture +``` +Renderer Process Main Process + | | + | provider:setApiKey | + |--------------------------------->| + | | safeStorage.encryptString() + | | | + | | v + | | System Keychain + | | | + | | electron-store + | | (encrypted base64) + | | + | provider:getApiKey | + |--------------------------------->| + | | safeStorage.decryptString() + |<---------------------------------| + | (plaintext) | +``` + +### Provider Configuration Schema +```typescript +interface ProviderConfig { + id: string; + name: string; + type: 'anthropic' | 'openai' | 'google' | 'ollama' | 'custom'; + baseUrl?: string; + model?: string; + enabled: boolean; + createdAt: string; + updatedAt: string; +} +``` + +### Key Validation Rules +- Anthropic: Must start with `sk-ant-` +- OpenAI: Must start with `sk-` +- Google: Minimum 20 characters +- Ollama: No key required +- Custom: No validation + +### ESM Compatibility +electron-store v10+ is ESM-only. Solution: dynamic imports in main process. + +```typescript +let store: any = null; +async function getStore() { + if (!store) { + const Store = (await import('electron-store')).default; + store = new Store({ /* config */ }); + } + return store; +} +``` + +## Version +v0.1.0-alpha (incremental) diff --git a/build_process/commit_5_channel_connection.md b/build_process/commit_5_channel_connection.md new file mode 100644 index 000000000..2182b0dd7 --- /dev/null +++ b/build_process/commit_5_channel_connection.md @@ -0,0 +1,124 @@ +# Commit 5: Channel Connection Flows + +## Summary +Implement comprehensive channel management UI with multi-platform support, including QR code-based connection for WhatsApp/WeChat and token-based connection for Telegram/Discord/Slack. + +## Changes + +### React Renderer + +#### `src/pages/Channels/index.tsx` +Complete rewrite with enhanced functionality: + +**Main Components:** +- `Channels` - Main page with channel list, stats, and add dialog +- `ChannelCard` - Individual channel display with connect/disconnect/delete +- `AddChannelDialog` - Multi-step channel addition flow + +**Features:** +- Channel statistics (Total, Connected, Disconnected) +- Gateway status warning when not running +- Supported channel type grid for quick add +- Connection-type specific flows: + - QR Code: WhatsApp, WeChat + - Token: Telegram, Discord, Slack + +**AddChannelDialog Flow:** +1. Select channel type +2. View connection instructions +3. For QR: Generate and display QR code +4. For Token: Enter bot token +5. Optionally name the channel +6. Confirm and add + +**Channel Info Configuration:** +```typescript +const channelInfo: Record +``` + +#### `src/stores/channels.ts` +Enhanced channel store with new actions: +- `addChannel(params)` - Add new channel with type, name, token +- `deleteChannel(channelId)` - Remove channel +- `requestQrCode(channelType)` - Request QR code from Gateway +- `clearError()` - Clear error state + +Improved error handling: +- Graceful fallback when Gateway unavailable +- Local channel creation for offline mode + +#### `src/types/channel.ts` +No changes - existing types sufficient. + +### Electron Main Process + +#### `electron/utils/store.ts` +- Converted to dynamic imports for ESM compatibility +- All functions now async + +#### `electron/main/window.ts` +- Converted to dynamic imports for ESM compatibility +- `getWindowState()` and `saveWindowState()` now async + +## Technical Details + +### Channel Connection Architecture +``` +AddChannelDialog + | + +-- QR Flow + | | + | v + | requestQrCode() --> Gateway --> WhatsApp/WeChat API + | | | + | v v + | Display QR <-- qrCode string <-- QR Generated + | | + | v + | User Scans --> Device Confirms --> channel:status event + | + +-- Token Flow + | + v + Enter Token --> addChannel() --> Gateway --> Bot API + | | + v v + Validate <-- success/error <-- Connection Attempt +``` + +### Channel Types Configuration +| Type | Connection | Token Label | Docs | +|------|------------|-------------|------| +| WhatsApp | QR Code | - | WhatsApp FAQ | +| Telegram | Token | Bot Token | BotFather Docs | +| Discord | Token | Bot Token | Developer Portal | +| Slack | Token | Bot Token (xoxb-) | Slack API | +| WeChat | QR Code | - | - | + +### Connection Instructions +Each channel type provides step-by-step instructions: +- WhatsApp: Open app > Settings > Linked Devices > Scan +- Telegram: @BotFather > /newbot > Copy token +- Discord: Developer Portal > Application > Bot > Token +- Slack: API Apps > Create App > OAuth > Install + +### UI Components Used +- `Card`, `Button`, `Input`, `Label` - Base components +- `Separator` - Visual dividers +- `StatusBadge` - Connection status +- `LoadingSpinner` - Loading states +- Lucide icons: Plus, Radio, RefreshCw, Power, QrCode, etc. + +### Error Handling +- Gateway offline: Create local channel, show warning +- Connection failed: Display error on ChannelCard +- Invalid token: Show validation error in dialog + +## Version +v0.1.0-alpha (incremental) diff --git a/build_process/commit_6_auto_update.md b/build_process/commit_6_auto_update.md new file mode 100644 index 000000000..2bc40b19c --- /dev/null +++ b/build_process/commit_6_auto_update.md @@ -0,0 +1,171 @@ +# Commit 6: Auto-Update Functionality + +## Summary +Integrate electron-updater for automatic application updates with a comprehensive UI for checking, downloading, and installing updates. + +## Changes + +### Electron Main Process + +#### `electron/main/updater.ts` (New) +Complete auto-update module: + +**AppUpdater Class:** +- Extends EventEmitter for event-based notifications +- Configures electron-updater settings +- Manages update lifecycle + +**Methods:** +- `setMainWindow(window)` - Set window for IPC +- `getStatus()` - Get current update status +- `checkForUpdates()` - Check for available updates +- `downloadUpdate()` - Download available update +- `quitAndInstall()` - Install update and restart +- `setChannel(channel)` - Set update channel (stable/beta/dev) +- `setAutoDownload(enable)` - Configure auto-download +- `getCurrentVersion()` - Get app version + +**Update Status:** +- `idle` - No update activity +- `checking` - Checking for updates +- `available` - Update available +- `not-available` - Already on latest +- `downloading` - Download in progress +- `downloaded` - Ready to install +- `error` - Update failed + +**IPC Handlers (registerUpdateHandlers):** +- `update:status` - Get current status +- `update:version` - Get app version +- `update:check` - Trigger update check +- `update:download` - Start download +- `update:install` - Install and restart +- `update:setChannel` - Change update channel +- `update:setAutoDownload` - Toggle auto-download + +**Events forwarded to renderer:** +- `update:status-changed` +- `update:checking` +- `update:available` +- `update:not-available` +- `update:progress` +- `update:downloaded` +- `update:error` + +#### `electron/main/index.ts` +- Import and register update handlers +- Auto-check for updates in production (10s delay) + +#### `electron/preload/index.ts` +- Added all update IPC channels + +### React Renderer + +#### `src/stores/update.ts` (New) +Zustand store for update state: +- State: `status`, `currentVersion`, `updateInfo`, `progress`, `error`, `isInitialized` +- Actions: `init`, `checkForUpdates`, `downloadUpdate`, `installUpdate`, `setChannel`, `setAutoDownload`, `clearError` +- Listens for all update events from main process + +#### `src/components/settings/UpdateSettings.tsx` (New) +Update UI component: +- Current version display +- Status indicator with icon +- Status text description +- Action buttons (Check/Download/Install/Retry) +- Download progress bar with transfer stats +- Update info card (version, date, release notes) +- Error details display + +**Progress Display:** +- Transferred / Total bytes +- Transfer speed (bytes/second) +- Progress percentage bar + +#### `src/components/ui/progress.tsx` (New) +Radix UI Progress component for download visualization. + +#### `src/pages/Settings/index.tsx` +- Replaced manual update section with `UpdateSettings` component +- Added `useUpdateStore` for version display +- Removed unused state and effect hooks + +## Technical Details + +### Update Flow +``` +App Start (Production) + | + v + 10s Delay + | + v +checkForUpdates() + | + +-- No Update --> status: 'not-available' + | + +-- Update Found --> status: 'available' + | + v + [User Action] + | + v + downloadUpdate() + | + v + status: 'downloading' + progress events + | + v + status: 'downloaded' + | + v + [User Action] + | + v + quitAndInstall() + | + v + App Restarts +``` + +### electron-updater Configuration +```typescript +autoUpdater.autoDownload = false; // Manual download trigger +autoUpdater.autoInstallOnAppQuit = true; // Install on quit +autoUpdater.logger = customLogger; // Console logging +``` + +### Update Channels +- `stable` - Production releases +- `beta` - Pre-release testing +- `dev` - Development builds + +### Progress Info Interface +```typescript +interface ProgressInfo { + total: number; // Total bytes + delta: number; // Bytes since last event + transferred: number; // Bytes downloaded + percent: number; // 0-100 + bytesPerSecond: number; // Transfer speed +} +``` + +### UI States +| Status | Icon | Action Button | +|--------|------|---------------| +| idle | RefreshCw (gray) | Check for Updates | +| checking | Loader2 (spin, blue) | Checking... (disabled) | +| available | Download (green) | Download Update | +| downloading | Download (pulse, blue) | Downloading... (disabled) | +| downloaded | CheckCircle2 (green) | Install & Restart | +| error | AlertCircle (red) | Retry | +| not-available | CheckCircle2 (green) | Check for Updates | + +## Dependencies +- electron-updater ^6.3.9 (already installed) +- @radix-ui/react-progress ^1.1.1 (already installed) + +## Version +v0.1.0-alpha (incremental) diff --git a/build_process/process.md b/build_process/process.md index 86660ef64..500ca49b4 100644 --- a/build_process/process.md +++ b/build_process/process.md @@ -11,6 +11,7 @@ * [commit_3] Setup wizard - Multi-step onboarding flow with provider, channel, skill selection * [commit_4] Provider configuration - Secure API key storage, provider management UI * [commit_5] Channel connection flows - Multi-channel support with QR/token connection UI +* [commit_6] Auto-update functionality - electron-updater integration with UI ### Plan: 1. ~~Initialize project structure~~ ✅ @@ -18,7 +19,7 @@ 3. ~~Implement Setup wizard with actual functionality~~ ✅ 4. ~~Add Provider configuration (API Key management)~~ ✅ 5. ~~Implement Channel connection flows~~ ✅ -6. Add auto-update functionality +6. ~~Add auto-update functionality~~ ✅ 7. Packaging and distribution setup ## Version Milestones diff --git a/electron/main/index.ts b/electron/main/index.ts index c3e3db90a..d80dd7534 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -9,6 +9,7 @@ import { registerIpcHandlers } from './ipc-handlers'; import { createTray } from './tray'; import { createMenu } from './menu'; import { PORTS } from '../utils/config'; +import { appUpdater, registerUpdateHandlers } from './updater'; // Disable GPU acceleration for better compatibility app.disableHardwareAcceleration(); @@ -76,6 +77,18 @@ async function initialize(): Promise { // Register IPC handlers registerIpcHandlers(gatewayManager, mainWindow); + // Register update handlers + registerUpdateHandlers(appUpdater, mainWindow); + + // Check for updates after a delay (only in production) + if (!process.env.VITE_DEV_SERVER_URL) { + setTimeout(() => { + appUpdater.checkForUpdates().catch((err) => { + console.error('Failed to check for updates:', err); + }); + }, 10000); // Check after 10 seconds + } + // Handle window close mainWindow.on('closed', () => { mainWindow = null; diff --git a/electron/main/updater.ts b/electron/main/updater.ts new file mode 100644 index 000000000..29f0177b6 --- /dev/null +++ b/electron/main/updater.ts @@ -0,0 +1,264 @@ +/** + * Auto-Updater Module + * Handles automatic application updates using electron-updater + */ +import { autoUpdater, UpdateInfo, ProgressInfo, UpdateDownloadedEvent } from 'electron-updater'; +import { BrowserWindow, app, ipcMain } from 'electron'; +import { EventEmitter } from 'events'; + +export interface UpdateStatus { + status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error'; + info?: UpdateInfo; + progress?: ProgressInfo; + error?: string; +} + +export interface UpdaterEvents { + 'status-changed': (status: UpdateStatus) => void; + 'checking-for-update': () => void; + 'update-available': (info: UpdateInfo) => void; + 'update-not-available': (info: UpdateInfo) => void; + 'download-progress': (progress: ProgressInfo) => void; + 'update-downloaded': (event: UpdateDownloadedEvent) => void; + 'error': (error: Error) => void; +} + +export class AppUpdater extends EventEmitter { + private mainWindow: BrowserWindow | null = null; + private status: UpdateStatus = { status: 'idle' }; + + constructor() { + super(); + + // Configure auto-updater + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + + // Use logger + autoUpdater.logger = { + info: (msg: string) => console.log('[Updater]', msg), + warn: (msg: string) => console.warn('[Updater]', msg), + error: (msg: string) => console.error('[Updater]', msg), + debug: (msg: string) => console.debug('[Updater]', msg), + }; + + this.setupListeners(); + } + + /** + * Set the main window for sending update events + */ + setMainWindow(window: BrowserWindow): void { + this.mainWindow = window; + } + + /** + * Get current update status + */ + getStatus(): UpdateStatus { + return this.status; + } + + /** + * Setup auto-updater event listeners + */ + private setupListeners(): void { + autoUpdater.on('checking-for-update', () => { + this.updateStatus({ status: 'checking' }); + this.emit('checking-for-update'); + }); + + autoUpdater.on('update-available', (info: UpdateInfo) => { + this.updateStatus({ status: 'available', info }); + this.emit('update-available', info); + }); + + autoUpdater.on('update-not-available', (info: UpdateInfo) => { + this.updateStatus({ status: 'not-available', info }); + this.emit('update-not-available', info); + }); + + autoUpdater.on('download-progress', (progress: ProgressInfo) => { + this.updateStatus({ status: 'downloading', progress }); + this.emit('download-progress', progress); + }); + + autoUpdater.on('update-downloaded', (event: UpdateDownloadedEvent) => { + this.updateStatus({ status: 'downloaded', info: event }); + this.emit('update-downloaded', event); + }); + + autoUpdater.on('error', (error: Error) => { + this.updateStatus({ status: 'error', error: error.message }); + this.emit('error', error); + }); + } + + /** + * Update status and notify renderer + */ + private updateStatus(newStatus: Partial): void { + this.status = { ...this.status, ...newStatus }; + this.sendToRenderer('update:status-changed', this.status); + } + + /** + * Send event to renderer process + */ + private sendToRenderer(channel: string, data: unknown): void { + if (this.mainWindow && !this.mainWindow.isDestroyed()) { + this.mainWindow.webContents.send(channel, data); + } + } + + /** + * Check for updates + */ + async checkForUpdates(): Promise { + try { + const result = await autoUpdater.checkForUpdates(); + return result?.updateInfo || null; + } catch (error) { + console.error('[Updater] Check for updates failed:', error); + return null; + } + } + + /** + * Download available update + */ + async downloadUpdate(): Promise { + try { + await autoUpdater.downloadUpdate(); + } catch (error) { + console.error('[Updater] Download update failed:', error); + throw error; + } + } + + /** + * Install update and restart app + */ + quitAndInstall(): void { + autoUpdater.quitAndInstall(); + } + + /** + * Set update channel (stable, beta, dev) + */ + setChannel(channel: 'stable' | 'beta' | 'dev'): void { + autoUpdater.channel = channel; + } + + /** + * Set auto-download preference + */ + setAutoDownload(enable: boolean): void { + autoUpdater.autoDownload = enable; + } + + /** + * Get current version + */ + getCurrentVersion(): string { + return app.getVersion(); + } +} + +/** + * Register IPC handlers for update operations + */ +export function registerUpdateHandlers( + updater: AppUpdater, + mainWindow: BrowserWindow +): void { + updater.setMainWindow(mainWindow); + + // Get current update status + ipcMain.handle('update:status', () => { + return updater.getStatus(); + }); + + // Get current version + ipcMain.handle('update:version', () => { + return updater.getCurrentVersion(); + }); + + // Check for updates + ipcMain.handle('update:check', async () => { + try { + const info = await updater.checkForUpdates(); + return { success: true, info }; + } catch (error) { + return { success: false, error: String(error) }; + } + }); + + // Download update + ipcMain.handle('update:download', async () => { + try { + await updater.downloadUpdate(); + return { success: true }; + } catch (error) { + return { success: false, error: String(error) }; + } + }); + + // Install update and restart + ipcMain.handle('update:install', () => { + updater.quitAndInstall(); + return { success: true }; + }); + + // Set update channel + ipcMain.handle('update:setChannel', (_, channel: 'stable' | 'beta' | 'dev') => { + updater.setChannel(channel); + return { success: true }; + }); + + // Set auto-download preference + ipcMain.handle('update:setAutoDownload', (_, enable: boolean) => { + updater.setAutoDownload(enable); + return { success: true }; + }); + + // Forward update events to renderer + updater.on('checking-for-update', () => { + if (!mainWindow.isDestroyed()) { + mainWindow.webContents.send('update:checking'); + } + }); + + updater.on('update-available', (info) => { + if (!mainWindow.isDestroyed()) { + mainWindow.webContents.send('update:available', info); + } + }); + + updater.on('update-not-available', (info) => { + if (!mainWindow.isDestroyed()) { + mainWindow.webContents.send('update:not-available', info); + } + }); + + updater.on('download-progress', (progress) => { + if (!mainWindow.isDestroyed()) { + mainWindow.webContents.send('update:progress', progress); + } + }); + + updater.on('update-downloaded', (event) => { + if (!mainWindow.isDestroyed()) { + mainWindow.webContents.send('update:downloaded', event); + } + }); + + updater.on('error', (error) => { + if (!mainWindow.isDestroyed()) { + mainWindow.webContents.send('update:error', error.message); + } + }); +} + +// Export singleton instance +export const appUpdater = new AppUpdater(); diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 81c7dc577..44b51d6c9 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -43,10 +43,13 @@ const electronAPI = { 'settings:getAll', 'settings:reset', // Update + 'update:status', + 'update:version', 'update:check', 'update:download', 'update:install', - 'update:getStatus', + 'update:setChannel', + 'update:setAutoDownload', // Env 'env:getConfig', 'env:setApiKey', @@ -93,9 +96,13 @@ const electronAPI = { 'gateway:exit', 'gateway:error', 'navigate', + 'update:status-changed', + 'update:checking', 'update:available', + 'update:not-available', + 'update:progress', 'update:downloaded', - 'update:status', + 'update:error', 'cron:updated', ]; @@ -128,9 +135,13 @@ const electronAPI = { 'gateway:exit', 'gateway:error', 'navigate', + 'update:status-changed', + 'update:checking', 'update:available', + 'update:not-available', + 'update:progress', 'update:downloaded', - 'update:status', + 'update:error', ]; if (validChannels.includes(channel)) { diff --git a/src/components/settings/UpdateSettings.tsx b/src/components/settings/UpdateSettings.tsx new file mode 100644 index 000000000..6facb37ef --- /dev/null +++ b/src/components/settings/UpdateSettings.tsx @@ -0,0 +1,207 @@ +/** + * Update Settings Component + * Displays update status and allows manual update checking/installation + */ +import { useEffect, useCallback } from 'react'; +import { Download, RefreshCw, CheckCircle2, AlertCircle, Loader2, Rocket } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import { useUpdateStore } from '@/stores/update'; + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +export function UpdateSettings() { + const { + status, + currentVersion, + updateInfo, + progress, + error, + isInitialized, + init, + checkForUpdates, + downloadUpdate, + installUpdate, + clearError, + } = useUpdateStore(); + + // Initialize on mount + useEffect(() => { + init(); + }, [init]); + + const handleCheckForUpdates = useCallback(async () => { + clearError(); + await checkForUpdates(); + }, [checkForUpdates, clearError]); + + const renderStatusIcon = () => { + switch (status) { + case 'checking': + return ; + case 'downloading': + return ; + case 'available': + return ; + case 'downloaded': + return ; + case 'error': + return ; + case 'not-available': + return ; + default: + return ; + } + }; + + const renderStatusText = () => { + switch (status) { + case 'checking': + return 'Checking for updates...'; + case 'downloading': + return 'Downloading update...'; + case 'available': + return `Update available: v${updateInfo?.version}`; + case 'downloaded': + return `Ready to install: v${updateInfo?.version}`; + case 'error': + return error || 'Update check failed'; + case 'not-available': + return 'You have the latest version'; + default: + return 'Check for updates to get the latest features'; + } + }; + + const renderAction = () => { + switch (status) { + case 'checking': + return ( + + ); + case 'downloading': + return ( + + ); + case 'available': + return ( + + ); + case 'downloaded': + return ( + + ); + case 'error': + return ( + + ); + default: + return ( + + ); + } + }; + + if (!isInitialized) { + return ( +
+ + Loading... +
+ ); + } + + return ( +
+ {/* Current Version */} +
+
+

Current Version

+

v{currentVersion}

+
+ {renderStatusIcon()} +
+ + {/* Status */} +
+

{renderStatusText()}

+ {renderAction()} +
+ + {/* Download Progress */} + {status === 'downloading' && progress && ( +
+
+ + {formatBytes(progress.transferred)} / {formatBytes(progress.total)} + + {formatBytes(progress.bytesPerSecond)}/s +
+ +

+ {Math.round(progress.percent)}% complete +

+
+ )} + + {/* Update Info */} + {updateInfo && (status === 'available' || status === 'downloaded') && ( +
+
+

Version {updateInfo.version}

+ {updateInfo.releaseDate && ( +

+ {new Date(updateInfo.releaseDate).toLocaleDateString()} +

+ )} +
+ {updateInfo.releaseNotes && ( +
+

What's New:

+

{updateInfo.releaseNotes}

+
+ )} +
+ )} + + {/* Error Details */} + {status === 'error' && error && ( +
+

Error Details:

+

{error}

+
+ )} + + {/* Help Text */} +

+ Updates are downloaded in the background and installed when you restart the app. +

+
+ ); +} + +export default UpdateSettings; diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 000000000..105fb6500 --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/src/pages/Settings/index.tsx b/src/pages/Settings/index.tsx index 0c8dd5421..7c1995ecd 100644 --- a/src/pages/Settings/index.tsx +++ b/src/pages/Settings/index.tsx @@ -2,16 +2,15 @@ * Settings Page * Application configuration */ -import { useState, useEffect } from 'react'; import { Sun, Moon, Monitor, RefreshCw, - Loader2, Terminal, ExternalLink, Key, + Download, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; @@ -21,7 +20,9 @@ import { Separator } from '@/components/ui/separator'; import { Badge } from '@/components/ui/badge'; import { useSettingsStore } from '@/stores/settings'; import { useGatewayStore } from '@/stores/gateway'; +import { useUpdateStore } from '@/stores/update'; import { ProvidersSettings } from '@/components/settings/ProvidersSettings'; +import { UpdateSettings } from '@/components/settings/UpdateSettings'; export function Settings() { const { @@ -37,26 +38,7 @@ export function Settings() { } = useSettingsStore(); const { status: gatewayStatus, restart: restartGateway } = useGatewayStore(); - - const [appVersion, setAppVersion] = useState('0.1.0'); - const [checkingUpdate, setCheckingUpdate] = useState(false); - - // Get app version - useEffect(() => { - window.electron.ipcRenderer.invoke('app:version').then((version) => { - setAppVersion(version as string); - }); - }, []); - - // Check for updates - const handleCheckUpdate = async () => { - setCheckingUpdate(true); - try { - await window.electron.ipcRenderer.invoke('update:check'); - } finally { - setCheckingUpdate(false); - } - }; + const currentVersion = useUpdateStore((state) => state.currentVersion); // Open developer console const openDevConsole = () => { @@ -178,32 +160,14 @@ export function Settings() { {/* Updates */} - Updates + + + Updates + Keep ClawX up to date -
-
-

ClawX

-

- Version {appVersion} -

-
- -
+ @@ -271,7 +235,7 @@ export function Settings() { ClawX - Graphical AI Assistant

Based on OpenClaw

-

Version {appVersion}

+

Version {currentVersion}