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}