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
This commit is contained in:
83
build_process/commit_2_gateway_refinements.md
Normal file
83
build_process/commit_2_gateway_refinements.md
Normal file
@@ -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)
|
||||||
88
build_process/commit_3_setup_wizard.md
Normal file
88
build_process/commit_3_setup_wizard.md
Normal file
@@ -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)
|
||||||
127
build_process/commit_4_provider_configuration.md
Normal file
127
build_process/commit_4_provider_configuration.md
Normal file
@@ -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)
|
||||||
124
build_process/commit_5_channel_connection.md
Normal file
124
build_process/commit_5_channel_connection.md
Normal file
@@ -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<ChannelType, {
|
||||||
|
description: string;
|
||||||
|
connectionType: 'qr' | 'token' | 'oauth';
|
||||||
|
instructions: string[];
|
||||||
|
tokenLabel?: string;
|
||||||
|
docsUrl?: string;
|
||||||
|
}>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `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)
|
||||||
171
build_process/commit_6_auto_update.md
Normal file
171
build_process/commit_6_auto_update.md
Normal file
@@ -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)
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
* [commit_3] Setup wizard - Multi-step onboarding flow with provider, channel, skill selection
|
* [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_4] Provider configuration - Secure API key storage, provider management UI
|
||||||
* [commit_5] Channel connection flows - Multi-channel support with QR/token connection 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:
|
### Plan:
|
||||||
1. ~~Initialize project structure~~ ✅
|
1. ~~Initialize project structure~~ ✅
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
3. ~~Implement Setup wizard with actual functionality~~ ✅
|
3. ~~Implement Setup wizard with actual functionality~~ ✅
|
||||||
4. ~~Add Provider configuration (API Key management)~~ ✅
|
4. ~~Add Provider configuration (API Key management)~~ ✅
|
||||||
5. ~~Implement Channel connection flows~~ ✅
|
5. ~~Implement Channel connection flows~~ ✅
|
||||||
6. Add auto-update functionality
|
6. ~~Add auto-update functionality~~ ✅
|
||||||
7. Packaging and distribution setup
|
7. Packaging and distribution setup
|
||||||
|
|
||||||
## Version Milestones
|
## Version Milestones
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { registerIpcHandlers } from './ipc-handlers';
|
|||||||
import { createTray } from './tray';
|
import { createTray } from './tray';
|
||||||
import { createMenu } from './menu';
|
import { createMenu } from './menu';
|
||||||
import { PORTS } from '../utils/config';
|
import { PORTS } from '../utils/config';
|
||||||
|
import { appUpdater, registerUpdateHandlers } from './updater';
|
||||||
|
|
||||||
// Disable GPU acceleration for better compatibility
|
// Disable GPU acceleration for better compatibility
|
||||||
app.disableHardwareAcceleration();
|
app.disableHardwareAcceleration();
|
||||||
@@ -76,6 +77,18 @@ async function initialize(): Promise<void> {
|
|||||||
// Register IPC handlers
|
// Register IPC handlers
|
||||||
registerIpcHandlers(gatewayManager, mainWindow);
|
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
|
// Handle window close
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
|
|||||||
264
electron/main/updater.ts
Normal file
264
electron/main/updater.ts
Normal file
@@ -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<UpdateStatus>): 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<UpdateInfo | null> {
|
||||||
|
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<void> {
|
||||||
|
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();
|
||||||
@@ -43,10 +43,13 @@ const electronAPI = {
|
|||||||
'settings:getAll',
|
'settings:getAll',
|
||||||
'settings:reset',
|
'settings:reset',
|
||||||
// Update
|
// Update
|
||||||
|
'update:status',
|
||||||
|
'update:version',
|
||||||
'update:check',
|
'update:check',
|
||||||
'update:download',
|
'update:download',
|
||||||
'update:install',
|
'update:install',
|
||||||
'update:getStatus',
|
'update:setChannel',
|
||||||
|
'update:setAutoDownload',
|
||||||
// Env
|
// Env
|
||||||
'env:getConfig',
|
'env:getConfig',
|
||||||
'env:setApiKey',
|
'env:setApiKey',
|
||||||
@@ -93,9 +96,13 @@ const electronAPI = {
|
|||||||
'gateway:exit',
|
'gateway:exit',
|
||||||
'gateway:error',
|
'gateway:error',
|
||||||
'navigate',
|
'navigate',
|
||||||
|
'update:status-changed',
|
||||||
|
'update:checking',
|
||||||
'update:available',
|
'update:available',
|
||||||
|
'update:not-available',
|
||||||
|
'update:progress',
|
||||||
'update:downloaded',
|
'update:downloaded',
|
||||||
'update:status',
|
'update:error',
|
||||||
'cron:updated',
|
'cron:updated',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -128,9 +135,13 @@ const electronAPI = {
|
|||||||
'gateway:exit',
|
'gateway:exit',
|
||||||
'gateway:error',
|
'gateway:error',
|
||||||
'navigate',
|
'navigate',
|
||||||
|
'update:status-changed',
|
||||||
|
'update:checking',
|
||||||
'update:available',
|
'update:available',
|
||||||
|
'update:not-available',
|
||||||
|
'update:progress',
|
||||||
'update:downloaded',
|
'update:downloaded',
|
||||||
'update:status',
|
'update:error',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (validChannels.includes(channel)) {
|
if (validChannels.includes(channel)) {
|
||||||
|
|||||||
207
src/components/settings/UpdateSettings.tsx
Normal file
207
src/components/settings/UpdateSettings.tsx
Normal file
@@ -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 <Loader2 className="h-5 w-5 animate-spin text-blue-500" />;
|
||||||
|
case 'downloading':
|
||||||
|
return <Download className="h-5 w-5 text-blue-500 animate-pulse" />;
|
||||||
|
case 'available':
|
||||||
|
return <Download className="h-5 w-5 text-green-500" />;
|
||||||
|
case 'downloaded':
|
||||||
|
return <CheckCircle2 className="h-5 w-5 text-green-500" />;
|
||||||
|
case 'error':
|
||||||
|
return <AlertCircle className="h-5 w-5 text-red-500" />;
|
||||||
|
case 'not-available':
|
||||||
|
return <CheckCircle2 className="h-5 w-5 text-green-500" />;
|
||||||
|
default:
|
||||||
|
return <RefreshCw className="h-5 w-5 text-muted-foreground" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Button disabled variant="outline" size="sm">
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Checking...
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
case 'downloading':
|
||||||
|
return (
|
||||||
|
<Button disabled variant="outline" size="sm">
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Downloading...
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
case 'available':
|
||||||
|
return (
|
||||||
|
<Button onClick={downloadUpdate} size="sm">
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Download Update
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
case 'downloaded':
|
||||||
|
return (
|
||||||
|
<Button onClick={installUpdate} size="sm" variant="default">
|
||||||
|
<Rocket className="h-4 w-4 mr-2" />
|
||||||
|
Install & Restart
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
case 'error':
|
||||||
|
return (
|
||||||
|
<Button onClick={handleCheckForUpdates} variant="outline" size="sm">
|
||||||
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Button onClick={handleCheckForUpdates} variant="outline" size="sm">
|
||||||
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
|
Check for Updates
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isInitialized) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Current Version */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium">Current Version</p>
|
||||||
|
<p className="text-2xl font-bold">v{currentVersion}</p>
|
||||||
|
</div>
|
||||||
|
{renderStatusIcon()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div className="flex items-center justify-between py-3 border-t border-b">
|
||||||
|
<p className="text-sm text-muted-foreground">{renderStatusText()}</p>
|
||||||
|
{renderAction()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Download Progress */}
|
||||||
|
{status === 'downloading' && progress && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span>
|
||||||
|
{formatBytes(progress.transferred)} / {formatBytes(progress.total)}
|
||||||
|
</span>
|
||||||
|
<span>{formatBytes(progress.bytesPerSecond)}/s</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={progress.percent} className="h-2" />
|
||||||
|
<p className="text-xs text-muted-foreground text-center">
|
||||||
|
{Math.round(progress.percent)}% complete
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Update Info */}
|
||||||
|
{updateInfo && (status === 'available' || status === 'downloaded') && (
|
||||||
|
<div className="rounded-lg bg-muted p-4 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="font-medium">Version {updateInfo.version}</p>
|
||||||
|
{updateInfo.releaseDate && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{new Date(updateInfo.releaseDate).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{updateInfo.releaseNotes && (
|
||||||
|
<div className="text-sm text-muted-foreground prose prose-sm max-w-none">
|
||||||
|
<p className="font-medium text-foreground mb-1">What's New:</p>
|
||||||
|
<p className="whitespace-pre-wrap">{updateInfo.releaseNotes}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error Details */}
|
||||||
|
{status === 'error' && error && (
|
||||||
|
<div className="rounded-lg bg-red-50 dark:bg-red-900/10 p-4 text-red-600 dark:text-red-400 text-sm">
|
||||||
|
<p className="font-medium mb-1">Error Details:</p>
|
||||||
|
<p>{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Help Text */}
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Updates are downloaded in the background and installed when you restart the app.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateSettings;
|
||||||
26
src/components/ui/progress.tsx
Normal file
26
src/components/ui/progress.tsx
Normal file
@@ -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<typeof ProgressPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||||
|
>(({ className, value, ...props }, ref) => (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
className="h-full w-full flex-1 bg-primary transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
))
|
||||||
|
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Progress }
|
||||||
@@ -2,16 +2,15 @@
|
|||||||
* Settings Page
|
* Settings Page
|
||||||
* Application configuration
|
* Application configuration
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import {
|
import {
|
||||||
Sun,
|
Sun,
|
||||||
Moon,
|
Moon,
|
||||||
Monitor,
|
Monitor,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Loader2,
|
|
||||||
Terminal,
|
Terminal,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Key,
|
Key,
|
||||||
|
Download,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
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 { Badge } from '@/components/ui/badge';
|
||||||
import { useSettingsStore } from '@/stores/settings';
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
import { useGatewayStore } from '@/stores/gateway';
|
import { useGatewayStore } from '@/stores/gateway';
|
||||||
|
import { useUpdateStore } from '@/stores/update';
|
||||||
import { ProvidersSettings } from '@/components/settings/ProvidersSettings';
|
import { ProvidersSettings } from '@/components/settings/ProvidersSettings';
|
||||||
|
import { UpdateSettings } from '@/components/settings/UpdateSettings';
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
const {
|
const {
|
||||||
@@ -37,26 +38,7 @@ export function Settings() {
|
|||||||
} = useSettingsStore();
|
} = useSettingsStore();
|
||||||
|
|
||||||
const { status: gatewayStatus, restart: restartGateway } = useGatewayStore();
|
const { status: gatewayStatus, restart: restartGateway } = useGatewayStore();
|
||||||
|
const currentVersion = useUpdateStore((state) => state.currentVersion);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Open developer console
|
// Open developer console
|
||||||
const openDevConsole = () => {
|
const openDevConsole = () => {
|
||||||
@@ -178,32 +160,14 @@ export function Settings() {
|
|||||||
{/* Updates */}
|
{/* Updates */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Updates</CardTitle>
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Download className="h-5 w-5" />
|
||||||
|
Updates
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>Keep ClawX up to date</CardDescription>
|
<CardDescription>Keep ClawX up to date</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg border">
|
<UpdateSettings />
|
||||||
<div>
|
|
||||||
<p className="font-medium">ClawX</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Version {appVersion}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleCheckUpdate}
|
|
||||||
disabled={checkingUpdate}
|
|
||||||
>
|
|
||||||
{checkingUpdate ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
||||||
Checking...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'Check for Updates'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
@@ -271,7 +235,7 @@ export function Settings() {
|
|||||||
<strong>ClawX</strong> - Graphical AI Assistant
|
<strong>ClawX</strong> - Graphical AI Assistant
|
||||||
</p>
|
</p>
|
||||||
<p>Based on OpenClaw</p>
|
<p>Based on OpenClaw</p>
|
||||||
<p>Version {appVersion}</p>
|
<p>Version {currentVersion}</p>
|
||||||
<div className="flex gap-4 pt-2">
|
<div className="flex gap-4 pt-2">
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
|
|||||||
184
src/stores/update.ts
Normal file
184
src/stores/update.ts
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* Update State Store
|
||||||
|
* Manages application update state
|
||||||
|
*/
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export interface UpdateInfo {
|
||||||
|
version: string;
|
||||||
|
releaseDate?: string;
|
||||||
|
releaseNotes?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgressInfo {
|
||||||
|
total: number;
|
||||||
|
delta: number;
|
||||||
|
transferred: number;
|
||||||
|
percent: number;
|
||||||
|
bytesPerSecond: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateStatus =
|
||||||
|
| 'idle'
|
||||||
|
| 'checking'
|
||||||
|
| 'available'
|
||||||
|
| 'not-available'
|
||||||
|
| 'downloading'
|
||||||
|
| 'downloaded'
|
||||||
|
| 'error';
|
||||||
|
|
||||||
|
interface UpdateState {
|
||||||
|
status: UpdateStatus;
|
||||||
|
currentVersion: string;
|
||||||
|
updateInfo: UpdateInfo | null;
|
||||||
|
progress: ProgressInfo | null;
|
||||||
|
error: string | null;
|
||||||
|
isInitialized: boolean;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
init: () => Promise<void>;
|
||||||
|
checkForUpdates: () => Promise<void>;
|
||||||
|
downloadUpdate: () => Promise<void>;
|
||||||
|
installUpdate: () => void;
|
||||||
|
setChannel: (channel: 'stable' | 'beta' | 'dev') => Promise<void>;
|
||||||
|
setAutoDownload: (enable: boolean) => Promise<void>;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateStore = create<UpdateState>((set, get) => ({
|
||||||
|
status: 'idle',
|
||||||
|
currentVersion: '0.0.0',
|
||||||
|
updateInfo: null,
|
||||||
|
progress: null,
|
||||||
|
error: null,
|
||||||
|
isInitialized: false,
|
||||||
|
|
||||||
|
init: async () => {
|
||||||
|
if (get().isInitialized) return;
|
||||||
|
|
||||||
|
// Get current version
|
||||||
|
try {
|
||||||
|
const version = await window.electron.ipcRenderer.invoke('update:version');
|
||||||
|
set({ currentVersion: version as string });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get version:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current status
|
||||||
|
try {
|
||||||
|
const status = await window.electron.ipcRenderer.invoke('update:status') as {
|
||||||
|
status: UpdateStatus;
|
||||||
|
info?: UpdateInfo;
|
||||||
|
progress?: ProgressInfo;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
set({
|
||||||
|
status: status.status,
|
||||||
|
updateInfo: status.info || null,
|
||||||
|
progress: status.progress || null,
|
||||||
|
error: status.error || null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get update status:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for update events
|
||||||
|
window.electron.ipcRenderer.on('update:status-changed', (data) => {
|
||||||
|
const status = data as {
|
||||||
|
status: UpdateStatus;
|
||||||
|
info?: UpdateInfo;
|
||||||
|
progress?: ProgressInfo;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
set({
|
||||||
|
status: status.status,
|
||||||
|
updateInfo: status.info || null,
|
||||||
|
progress: status.progress || null,
|
||||||
|
error: status.error || null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.on('update:checking', () => {
|
||||||
|
set({ status: 'checking', error: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.on('update:available', (info) => {
|
||||||
|
set({ status: 'available', updateInfo: info as UpdateInfo });
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.on('update:not-available', () => {
|
||||||
|
set({ status: 'not-available' });
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.on('update:progress', (progress) => {
|
||||||
|
set({ status: 'downloading', progress: progress as ProgressInfo });
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.on('update:downloaded', (info) => {
|
||||||
|
set({ status: 'downloaded', updateInfo: info as UpdateInfo, progress: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.on('update:error', (error) => {
|
||||||
|
set({ status: 'error', error: error as string, progress: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
set({ isInitialized: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
checkForUpdates: async () => {
|
||||||
|
set({ status: 'checking', error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electron.ipcRenderer.invoke('update:check') as {
|
||||||
|
success: boolean;
|
||||||
|
info?: UpdateInfo;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
set({ status: 'error', error: result.error || 'Failed to check for updates' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
set({ status: 'error', error: String(error) });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadUpdate: async () => {
|
||||||
|
set({ status: 'downloading', error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electron.ipcRenderer.invoke('update:download') as {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
set({ status: 'error', error: result.error || 'Failed to download update' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
set({ status: 'error', error: String(error) });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
installUpdate: () => {
|
||||||
|
window.electron.ipcRenderer.invoke('update:install');
|
||||||
|
},
|
||||||
|
|
||||||
|
setChannel: async (channel) => {
|
||||||
|
try {
|
||||||
|
await window.electron.ipcRenderer.invoke('update:setChannel', channel);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set update channel:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setAutoDownload: async (enable) => {
|
||||||
|
try {
|
||||||
|
await window.electron.ipcRenderer.invoke('update:setAutoDownload', enable);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set auto-download:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearError: () => set({ error: null, status: 'idle' }),
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user