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_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
|
||||
|
||||
@@ -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<void> {
|
||||
// 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;
|
||||
|
||||
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: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)) {
|
||||
|
||||
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
|
||||
* 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 */}
|
||||
<Card>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border">
|
||||
<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>
|
||||
<UpdateSettings />
|
||||
|
||||
<Separator />
|
||||
|
||||
@@ -271,7 +235,7 @@ export function Settings() {
|
||||
<strong>ClawX</strong> - Graphical AI Assistant
|
||||
</p>
|
||||
<p>Based on OpenClaw</p>
|
||||
<p>Version {appVersion}</p>
|
||||
<p>Version {currentVersion}</p>
|
||||
<div className="flex gap-4 pt-2">
|
||||
<Button
|
||||
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