Merge pull request #659 from wuleihenbang/feat/prevent-sleep

feat: 添加防休眠功能 & 实现 OpenClaw 助手消息与工具卡片交替显示
This commit is contained in:
btc69m979y-dotcom
2026-03-23 14:03:49 +08:00
committed by GitHub
8 changed files with 602 additions and 22 deletions
@@ -0,0 +1,280 @@
# Prevent Sleep Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a "prevent sleep" toggle in Settings > General that keeps the system awake while LobsterAI is running.
**Architecture:** Follows the existing autoLaunch pattern — independent IPC channel, store key, and immediate toggle (no save button). Main process uses Electron's `powerSaveBlocker` API.
**Tech Stack:** Electron `powerSaveBlocker`, React, IPC, SQLite kv store
**Spec:** `docs/superpowers/specs/2026-03-21-prevent-sleep-design.md`
---
## File Map
| File | Action | Responsibility |
|------|--------|---------------|
| `src/main/main.ts` | Modify | Add `powerSaveBlocker` import, IPC handlers, startup activation |
| `src/main/preload.ts` | Modify | Expose `preventSleep.get/set` API |
| `src/renderer/types/electron.d.ts` | Modify | Add `preventSleep` type declaration |
| `src/renderer/services/i18n.ts` | Modify | Add zh/en translation keys |
| `src/renderer/components/Settings.tsx` | Modify | Add toggle UI in general tab |
---
### Task 1: Main Process — IPC Handlers and powerSaveBlocker
**Files:**
- Modify: `src/main/main.ts:1` (add `powerSaveBlocker` to electron import)
- Modify: `src/main/main.ts:1676` (add IPC handlers after autoLaunch handlers)
- [ ] **Step 1: Add `powerSaveBlocker` to electron import (line 1)**
Change:
```typescript
import { app, BrowserWindow, ipcMain, session, nativeTheme, dialog, shell, nativeImage, systemPreferences, Menu, protocol, net, powerMonitor } from 'electron';
```
To:
```typescript
import { app, BrowserWindow, ipcMain, session, nativeTheme, dialog, shell, nativeImage, systemPreferences, Menu, protocol, net, powerMonitor, powerSaveBlocker } from 'electron';
```
- [ ] **Step 2: Add blocker state variable**
Add after the electron import section (near other module-level variables):
```typescript
let preventSleepBlockerId: number | null = null;
```
- [ ] **Step 3: Add IPC handlers after autoLaunch handlers (after line 1676)**
```typescript
ipcMain.handle('app:getPreventSleep', () => {
const enabled = getStore().get<boolean>('prevent_sleep_enabled') ?? false;
return { enabled };
});
ipcMain.handle('app:setPreventSleep', (_event, enabled: unknown) => {
if (typeof enabled !== 'boolean') {
return { success: false, error: 'Invalid parameter: enabled must be boolean' };
}
try {
if (enabled) {
if (preventSleepBlockerId === null || !powerSaveBlocker.isStarted(preventSleepBlockerId)) {
preventSleepBlockerId = powerSaveBlocker.start('prevent-display-sleep');
}
} else {
if (preventSleepBlockerId !== null && powerSaveBlocker.isStarted(preventSleepBlockerId)) {
powerSaveBlocker.stop(preventSleepBlockerId);
preventSleepBlockerId = null;
}
}
getStore().set('prevent_sleep_enabled', enabled);
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to set prevent-sleep',
};
}
});
```
- [ ] **Step 4: Add startup activation**
Find the `initApp` function where autoLaunch store read happens (search for `auto_launch_enabled` near line 3932). Add nearby:
```typescript
// Restore prevent-sleep setting
const preventSleepEnabled = getStore().get<boolean>('prevent_sleep_enabled');
if (preventSleepEnabled) {
try {
preventSleepBlockerId = powerSaveBlocker.start('prevent-display-sleep');
} catch (err) {
console.error('[Main] Failed to start prevent-sleep blocker:', err);
}
}
```
- [ ] **Step 5: Compile and verify**
Run: `npm run compile:electron`
Expected: No errors
- [ ] **Step 6: Commit**
```bash
git add src/main/main.ts
git commit -m "feat: add prevent-sleep IPC handlers and powerSaveBlocker logic"
```
---
### Task 2: Preload and Type Declaration
**Files:**
- Modify: `src/main/preload.ts:262` (add after autoLaunch block)
- Modify: `src/renderer/types/electron.d.ts:358` (add after autoLaunch type)
- [ ] **Step 1: Add preload API (after autoLaunch block, line 262)**
```typescript
preventSleep: {
get: () => ipcRenderer.invoke('app:getPreventSleep'),
set: (enabled: boolean) => ipcRenderer.invoke('app:setPreventSleep', enabled),
},
```
- [ ] **Step 2: Add type declaration (after autoLaunch type, line 358)**
```typescript
preventSleep: {
get: () => Promise<{ enabled: boolean }>;
set: (enabled: boolean) => Promise<{ success: boolean; error?: string }>;
};
```
- [ ] **Step 3: Compile and verify**
Run: `npm run compile:electron`
Expected: No errors
- [ ] **Step 4: Commit**
```bash
git add src/main/preload.ts src/renderer/types/electron.d.ts
git commit -m "feat: expose preventSleep API in preload and type declaration"
```
---
### Task 3: i18n Translations
**Files:**
- Modify: `src/renderer/services/i18n.ts:860` (add after useSystemProxyDescription, zh section)
- Modify: `src/renderer/services/i18n.ts:1904` (add after useSystemProxyDescription, en section)
- [ ] **Step 1: Add Chinese translations (after line 860)**
```typescript
preventSleep: '防止休眠',
preventSleepDescription: '防止系统在应用运行时进入睡眠模式',
```
- [ ] **Step 2: Add English translations (after line 1904)**
```typescript
preventSleep: 'Prevent Sleep',
preventSleepDescription: 'Prevent the system from sleeping while the app is running',
```
- [ ] **Step 3: Commit**
```bash
git add src/renderer/services/i18n.ts
git commit -m "feat(i18n): add prevent-sleep translation keys"
```
---
### Task 4: Settings UI Toggle
**Files:**
- Modify: `src/renderer/components/Settings.tsx:383` (add state)
- Modify: `src/renderer/components/Settings.tsx:601` (add init load)
- Modify: `src/renderer/components/Settings.tsx:1949` (add toggle after system proxy section)
- [ ] **Step 1: Add state variables (near line 383, after autoLaunch states)**
```typescript
const [preventSleep, setPreventSleepState] = useState(false);
const [isUpdatingPreventSleep, setIsUpdatingPreventSleep] = useState(false);
```
- [ ] **Step 2: Add init load (after autoLaunch.get() block, near line 605)**
```typescript
// Load prevent-sleep setting
window.electron.preventSleep.get().then(({ enabled }) => {
setPreventSleepState(enabled);
}).catch(err => {
console.error('Failed to load prevent-sleep setting:', err);
});
```
- [ ] **Step 3: Add toggle UI (after system proxy section closing `</div>`, line 1949)**
```tsx
{/* Prevent Sleep Section */}
<div>
<h4 className="text-sm font-medium dark:text-claude-darkText text-claude-text mb-3">
{i18nService.t('preventSleep')}
</h4>
<label className="flex items-center justify-between cursor-pointer">
<span className="text-sm dark:text-claude-darkSecondaryText text-claude-secondaryText">
{i18nService.t('preventSleepDescription')}
</span>
<button
type="button"
role="switch"
aria-checked={preventSleep}
onClick={async () => {
if (isUpdatingPreventSleep) return;
const next = !preventSleep;
setIsUpdatingPreventSleep(true);
try {
const result = await window.electron.preventSleep.set(next);
if (result.success) {
setPreventSleepState(next);
} else {
setError(result.error || 'Failed to update prevent-sleep setting');
}
} catch (err) {
console.error('Failed to set prevent-sleep:', err);
setError('Failed to update prevent-sleep setting');
} finally {
setIsUpdatingPreventSleep(false);
}
}}
disabled={isUpdatingPreventSleep}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${
isUpdatingPreventSleep ? 'opacity-50 cursor-not-allowed' : ''
} ${
preventSleep
? 'bg-claude-accent'
: 'bg-gray-300 dark:bg-gray-600'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
preventSleep ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</label>
</div>
```
- [ ] **Step 4: Compile and verify**
Run: `npm run compile:electron`
Expected: No errors
- [ ] **Step 5: Manual test**
Run: `npm run electron:dev`
1. Open Settings > General
2. Verify "防止休眠" toggle appears below system proxy
3. Toggle on → system should not sleep
4. Toggle off → normal sleep behavior
5. Toggle on → close and reopen Settings → toggle should still be on
6. Restart app → toggle should still be on
- [ ] **Step 6: Commit**
```bash
git add src/renderer/components/Settings.tsx
git commit -m "feat(renderer): add prevent-sleep toggle in Settings general tab"
```
@@ -0,0 +1,104 @@
# Prevent Sleep Feature Design
## Overview
Add a "prevent sleep" toggle to Settings > General tab, preventing the system from sleeping while LobsterAI is running. Follows the existing autoLaunch pattern.
## User Story
As a user running long AI tasks or IM bots, I want LobsterAI to prevent my computer from sleeping, so tasks aren't interrupted by the system going to sleep.
## Design
### UI
- **Location**: Settings > General tab, below the system proxy toggle
- **Component**: Toggle switch (same style as autoLaunch / useSystemProxy)
- **Behavior**: Toggle takes effect immediately (no save button needed)
- **Label**: 防止休眠 / Prevent Sleep
- **Description**: 防止系统在应用运行时进入睡眠模式 / Prevent the system from sleeping while the app is running
### Architecture
Follows the autoLaunch pattern exactly:
```
Settings.tsx (toggle)
→ window.electron.preventSleep.set(true/false)
→ IPC: app:setPreventSleep
→ main.ts: powerSaveBlocker.start/stop()
→ store.set('prevent_sleep_enabled', boolean)
```
### Main Process (`main.ts`)
- Import `powerSaveBlocker` from `electron`
- Track blocker ID: `let preventSleepBlockerId: number | null = null`
- IPC `app:getPreventSleep`: read from store, return `{ enabled: boolean }`
- IPC `app:setPreventSleep`: start/stop blocker, persist to store
- On app startup: if `prevent_sleep_enabled` is true in store, auto-start blocker
- On app quit: stop blocker if active (cleanup)
### Preload (`preload.ts`)
```typescript
preventSleep: {
get: () => ipcRenderer.invoke('app:getPreventSleep'),
set: (enabled: boolean) => ipcRenderer.invoke('app:setPreventSleep', enabled),
},
```
### Type Declaration (`electron.d.ts`)
```typescript
preventSleep: {
get: () => Promise<{ enabled: boolean }>;
set: (enabled: boolean) => Promise<{ success: boolean; error?: string }>;
};
```
### Settings UI (`Settings.tsx`)
- State: `const [preventSleep, setPreventSleep] = useState(false)`
- Loading state: `isUpdatingPreventSleep`
- Init: load from `window.electron.preventSleep.get()` alongside autoLaunch
- Toggle handler: call `window.electron.preventSleep.set(next)`, update state
### i18n (`i18n.ts`)
| Key | zh | en |
|-----|----|----|
| `preventSleep` | 防止休眠 | Prevent Sleep |
| `preventSleepDescription` | 防止系统在应用运行时进入睡眠模式 | Prevent the system from sleeping while the app is running |
### Store
- Key: `prevent_sleep_enabled`
- Type: `boolean`
- Default: `false` (off by default)
### Electron API
- `powerSaveBlocker.start('prevent-display-sleep')` — prevents display sleep (also prevents system sleep)
- `powerSaveBlocker.stop(id)` — releases the blocker
- `powerSaveBlocker.isStarted(id)` — check if active
### Files to Modify
1. `src/main/main.ts` — add `powerSaveBlocker` import, IPC handlers, startup logic
2. `src/main/preload.ts` — add `preventSleep` API
3. `src/renderer/types/electron.d.ts` — add type declaration
4. `src/renderer/components/Settings.tsx` — add toggle in general tab
5. `src/renderer/services/i18n.ts` — add zh/en translations
### Edge Cases
- App quit while blocker active → blocker auto-released by OS when process exits
- Multiple toggles rapidly → loading state prevents double-click
- Store value missing → default to false (off)
### Not In Scope
- Per-session prevent sleep (only during active cowork sessions)
- Tray menu toggle
- System tray indicator showing sleep prevention status
@@ -104,6 +104,8 @@ type ActiveTurn = {
committedAssistantText: string;
currentAssistantSegmentText: string;
currentText: string;
/** Highest text length from agent assistant events (immune to chat delta noise). */
agentAssistantTextLength: number;
currentContentText: string;
currentContentBlocks: string[];
sawNonTextContentBlocks: boolean;
@@ -986,6 +988,7 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
committedAssistantText: '',
currentAssistantSegmentText: '',
currentText: '',
agentAssistantTextLength: 0,
currentContentText: '',
currentContentBlocks: [],
sawNonTextContentBlocks: false,
@@ -1590,6 +1593,9 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
}
if (event.event === 'agent') {
// Process assistant text updates here (before handleAgentEvent) because
// handleAgentEvent may enqueue events when sessionId mapping isn't ready.
this.processAgentAssistantText(event.payload);
this.handleAgentEvent(event.payload, event.seq);
return;
}
@@ -1895,10 +1901,6 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
this.probeBrowserControlService(toolCallId, phase);
}
if (phase === 'start') {
this.splitAssistantSegmentBeforeTool(turn);
}
if (!turn.toolUseMessageIdByToolCallId.has(toolCallId)) {
const toolUseMessage = this.store.addMessage(sessionId, {
type: 'tool_use',
@@ -2135,25 +2137,91 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
return normalizedFullText;
}
private splitAssistantSegmentBeforeTool(turn: ActiveTurn): void {
if (!turn.assistantMessageId) {
/**
* Process agent assistant-stream text directly from handleGatewayEvent.
* This bypasses handleAgentEvent's session resolution (which may enqueue events),
* ensuring text updates and reset detection always work.
*/
private processAgentAssistantText(payload: unknown): void {
if (!isRecord(payload)) return;
const p = payload as Record<string, unknown>;
if (p.stream !== 'assistant') return;
const dataField = isRecord(p.data) ? p.data as Record<string, unknown> : p;
const text = typeof dataField.text === 'string' ? dataField.text : '';
const runId = typeof p.runId === 'string' ? p.runId.trim() : '';
const sessionId = runId ? this.sessionIdByRunId.get(runId) : undefined;
const turn = sessionId ? this.activeTurns.get(sessionId) : undefined;
if (!text || !turn || !sessionId) {
if (text) {
console.debug('[Debug:processAssistant] skipped: text.len:', text.length, 'runId:', runId.slice(0, 8), 'sid:', !!sessionId, 'turn:', !!turn);
}
return;
}
const segmentText = turn.currentAssistantSegmentText.trim();
if (segmentText) {
const committedCandidate = `${turn.committedAssistantText}${segmentText}`;
const fullText = turn.currentText.trim();
if (fullText && fullText.startsWith(committedCandidate)) {
turn.committedAssistantText = committedCandidate;
} else {
turn.committedAssistantText = committedCandidate;
}
} else {
const fullText = turn.currentText.trim();
if (fullText && fullText.length > turn.committedAssistantText.length) {
turn.committedAssistantText = fullText;
}
// Detect text reset: new model call starts → text length drops significantly.
// Only trigger when hwm is meaningful (> 5 chars) to avoid false positives
// from early chat delta / agent event interleaving.
if (text.length < turn.agentAssistantTextLength
&& turn.agentAssistantTextLength > 5
&& turn.assistantMessageId) {
console.debug('[Debug:textReset] detected:', turn.agentAssistantTextLength, '->',
text.length, 'splitting. prevText:', turn.currentText.slice(0, 80));
this.splitAssistantSegmentBeforeTool(sessionId, turn);
turn.agentAssistantTextLength = 0;
}
// Track high-water mark.
turn.agentAssistantTextLength = Math.max(turn.agentAssistantTextLength, text.length);
// Update turn text state and push to store.
turn.currentText = text;
turn.currentAssistantSegmentText = this.resolveAssistantSegmentText(turn, text);
if (!turn.assistantMessageId && turn.currentAssistantSegmentText) {
// Create a new message for the new text segment (after split).
const assistantMessage = this.store.addMessage(sessionId, {
type: 'assistant',
content: turn.currentAssistantSegmentText,
metadata: { isStreaming: true, isFinal: false },
});
turn.assistantMessageId = assistantMessage.id;
this.emit('message', sessionId, assistantMessage);
} else if (turn.assistantMessageId && turn.currentAssistantSegmentText) {
this.store.updateMessage(sessionId, turn.assistantMessageId, {
content: turn.currentAssistantSegmentText,
metadata: { isStreaming: true, isFinal: false },
});
this.throttledEmitMessageUpdate(sessionId, turn.assistantMessageId, turn.currentAssistantSegmentText);
}
}
private splitAssistantSegmentBeforeTool(sessionId: string, turn: ActiveTurn): void {
if (!turn.assistantMessageId) return;
const messageId = turn.assistantMessageId;
// Committed text: use agentAssistantTextLength as the reliable segment length,
// since currentText/currentAssistantSegmentText may be overwritten by chat deltas.
// Read the actual content from the store (which was updated by processAgentAssistantText).
const session = this.store.getSession(sessionId);
const currentMsg = session?.messages.find((m) => m.id === messageId);
const storeContent = currentMsg?.content?.trim() || '';
if (storeContent) {
turn.committedAssistantText = `${turn.committedAssistantText}${storeContent}`;
}
// Flush pending throttled update and mark the message as final.
// Don't overwrite the content — the store already has the correct text
// from processAgentAssistantText's real-time updates.
this.clearPendingMessageUpdate(messageId);
this.store.updateMessage(sessionId, messageId, {
metadata: { isStreaming: false, isFinal: true },
});
if (storeContent) {
this.emit('messageUpdate', sessionId, messageId, storeContent);
}
turn.assistantMessageId = null;
@@ -2238,6 +2306,15 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
turn.currentAssistantSegmentText = finalSegmentText;
if (turn.assistantMessageId) {
// Flush any pending throttled update and force-emit the latest store content
// so the renderer sees the final text even if the last throttled emit was skipped.
this.clearPendingMessageUpdate(turn.assistantMessageId);
const storeSession = this.store.getSession(sessionId);
const storeMsg = storeSession?.messages.find((m) => m.id === turn.assistantMessageId);
if (storeMsg?.content) {
this.emit('messageUpdate', sessionId, turn.assistantMessageId, storeMsg.content);
}
const persistedSegmentText = finalSegmentText || previousSegmentText;
if (persistedSegmentText) {
this.store.updateMessage(sessionId, turn.assistantMessageId, {
@@ -2608,6 +2685,9 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
console.log('[Debug:syncFinal] canonicalText length:', canonicalText.length, 'assistantMessageId:', turn.assistantMessageId);
const canonicalSegmentText = this.resolveAssistantSegmentText(turn, canonicalText);
console.debug('[Debug:syncFinal] canonicalSegmentText length:', canonicalSegmentText.length,
'committed.length:', turn.committedAssistantText.length,
'segment:', canonicalSegmentText.slice(0, 80));
turn.currentText = canonicalText;
turn.currentAssistantSegmentText = canonicalSegmentText;
@@ -2639,9 +2719,13 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
const currentMessage = session?.messages.find((message) => message.id === turn.assistantMessageId);
const currentText = currentMessage?.content.trim() ?? '';
if (canonicalSegmentText === currentText) {
// Content matches but renderer may not have received the last throttled update.
// Force-emit so the UI shows the final text.
this.emit('messageUpdate', sessionId, turn.assistantMessageId, canonicalSegmentText);
return;
}
console.debug('[Debug:syncFinal] updating last segment:', currentText.length, '->', canonicalSegmentText.length);
this.store.updateMessage(sessionId, turn.assistantMessageId, {
content: canonicalSegmentText,
metadata: {
@@ -3293,6 +3377,7 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim
committedAssistantText: '',
currentAssistantSegmentText: '',
currentText: '',
agentAssistantTextLength: 0,
currentContentText: '',
currentContentBlocks: [],
sawNonTextContentBlocks: false,
+42 -1
View File
@@ -1,4 +1,4 @@
import { app, BrowserWindow, ipcMain, session, nativeTheme, dialog, shell, nativeImage, systemPreferences, Menu, protocol, net, powerMonitor } from 'electron';
import { app, BrowserWindow, ipcMain, session, nativeTheme, dialog, shell, nativeImage, systemPreferences, Menu, protocol, net, powerMonitor, powerSaveBlocker } from 'electron';
import type { WebContents } from 'electron';
import path from 'path';
import fs from 'fs';
@@ -567,6 +567,7 @@ let openClawBootstrapPromise: Promise<OpenClawEngineStatus> | null = null;
let openClawStatusForwarderBound = false;
let coworkRuntimeForwarderBound = false;
let memoryMigrationDone = false;
let preventSleepBlockerId: number | null = null;
const initStore = async (): Promise<SqliteStore> => {
if (!storeInitPromise) {
@@ -1682,6 +1683,36 @@ if (!gotTheLock) {
}
});
ipcMain.handle('app:getPreventSleep', () => {
const enabled = getStore().get<boolean>('prevent_sleep_enabled') ?? false;
return { enabled };
});
ipcMain.handle('app:setPreventSleep', (_event, enabled: unknown) => {
if (typeof enabled !== 'boolean') {
return { success: false, error: 'Invalid parameter: enabled must be boolean' };
}
try {
if (enabled) {
if (preventSleepBlockerId === null || !powerSaveBlocker.isStarted(preventSleepBlockerId)) {
preventSleepBlockerId = powerSaveBlocker.start('prevent-display-sleep');
}
} else {
if (preventSleepBlockerId !== null && powerSaveBlocker.isStarted(preventSleepBlockerId)) {
powerSaveBlocker.stop(preventSleepBlockerId);
preventSleepBlockerId = null;
}
}
getStore().set('prevent_sleep_enabled', enabled);
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to set prevent-sleep',
};
}
});
// Window control IPC handlers
ipcMain.on('window-minimize', () => {
mainWindow?.minimize();
@@ -3991,6 +4022,16 @@ if (!gotTheLock) {
setAutoLaunchEnabled(true);
}
// Restore prevent-sleep setting
const preventSleepEnabled = getStore().get<boolean>('prevent_sleep_enabled');
if (preventSleepEnabled) {
try {
preventSleepBlockerId = powerSaveBlocker.start('prevent-display-sleep');
} catch (err) {
console.error('[Main] Failed to start prevent-sleep blocker:', err);
}
}
let lastLanguage = getStore().get<AppConfigSettings>('app_config')?.language;
let lastUseSystemProxy = getUseSystemProxyFromConfig(getStore().get<AppConfigSettings>('app_config'));
getStore().onDidChange<AppConfigSettings>('app_config', (newConfig, oldConfig) => {
+4
View File
@@ -262,6 +262,10 @@ contextBridge.exposeInMainWorld('electron', {
get: () => ipcRenderer.invoke('app:getAutoLaunch'),
set: (enabled: boolean) => ipcRenderer.invoke('app:setAutoLaunch', enabled),
},
preventSleep: {
get: () => ipcRenderer.invoke('app:getPreventSleep'),
set: (enabled: boolean) => ipcRenderer.invoke('app:setPreventSleep', enabled),
},
appInfo: {
getVersion: () => ipcRenderer.invoke('app:getVersion'),
getSystemLocale: () => ipcRenderer.invoke('app:getSystemLocale'),
+59 -1
View File
@@ -418,6 +418,8 @@ const Settings: React.FC<SettingsProps> = ({ onClose, initialTab, notice, onUpda
const [autoLaunch, setAutoLaunchState] = useState(false);
const [useSystemProxy, setUseSystemProxy] = useState(false);
const [isUpdatingAutoLaunch, setIsUpdatingAutoLaunch] = useState(false);
const [preventSleep, setPreventSleepState] = useState(false);
const [isUpdatingPreventSleep, setIsUpdatingPreventSleep] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [noticeMessage, setNoticeMessage] = useState<string | null>(notice ?? null);
@@ -643,7 +645,14 @@ const Settings: React.FC<SettingsProps> = ({ onClose, initialTab, notice, onUpda
}).catch(err => {
console.error('Failed to load auto-launch setting:', err);
});
// Load prevent-sleep setting
window.electron.preventSleep.get().then(({ enabled }) => {
setPreventSleepState(enabled);
}).catch(err => {
console.error('Failed to load prevent-sleep setting:', err);
});
// Set up providers based on saved config
if (config.api) {
// For backward compatibility with older config
@@ -2127,6 +2136,55 @@ const Settings: React.FC<SettingsProps> = ({ onClose, initialTab, notice, onUpda
</label>
</div>
{/* Prevent Sleep Section */}
<div>
<h4 className="text-sm font-medium dark:text-claude-darkText text-claude-text mb-3">
{i18nService.t('preventSleep')}
</h4>
<label className="flex items-center justify-between cursor-pointer">
<span className="text-sm dark:text-claude-darkSecondaryText text-claude-secondaryText">
{i18nService.t('preventSleepDescription')}
</span>
<button
type="button"
role="switch"
aria-checked={preventSleep}
onClick={async () => {
if (isUpdatingPreventSleep) return;
const next = !preventSleep;
setIsUpdatingPreventSleep(true);
try {
const result = await window.electron.preventSleep.set(next);
if (result.success) {
setPreventSleepState(next);
} else {
setError(result.error || 'Failed to update prevent-sleep setting');
}
} catch (err) {
console.error('Failed to set prevent-sleep:', err);
setError('Failed to update prevent-sleep setting');
} finally {
setIsUpdatingPreventSleep(false);
}
}}
disabled={isUpdatingPreventSleep}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${
isUpdatingPreventSleep ? 'opacity-50 cursor-not-allowed' : ''
} ${
preventSleep
? 'bg-claude-accent'
: 'bg-gray-300 dark:bg-gray-600'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
preventSleep ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</label>
</div>
{/* System proxy Section */}
<div>
<h4 className="text-sm font-medium dark:text-claude-darkText text-claude-text mb-3">
+4
View File
@@ -888,6 +888,8 @@ const translations: Record<LanguageType, Record<string, string>> = {
autoLaunchDescription: '系统启动时自动运行应用',
useSystemProxy: '使用系统代理',
useSystemProxyDescription: '开启后网络请求将跟随系统代理(保存后生效)',
preventSleep: '防止休眠',
preventSleepDescription: '防止系统在应用运行时进入睡眠模式',
// 定时任务
scheduledTasks: '定时任务',
@@ -1962,6 +1964,8 @@ const translations: Record<LanguageType, Record<string, string>> = {
autoLaunchDescription: 'Automatically start the app when you log in',
useSystemProxy: 'Use System Proxy',
useSystemProxyDescription: 'When enabled, network requests follow system proxy settings (applies after Save)',
preventSleep: 'Prevent Sleep',
preventSleepDescription: 'Prevent the system from sleeping while the app is running',
// Scheduled Tasks
scheduledTasks: 'Scheduled Tasks',
+4
View File
@@ -357,6 +357,10 @@ interface IElectronAPI {
get: () => Promise<{ enabled: boolean }>;
set: (enabled: boolean) => Promise<{ success: boolean; error?: string }>;
};
preventSleep: {
get: () => Promise<{ enabled: boolean }>;
set: (enabled: boolean) => Promise<{ success: boolean; error?: string }>;
};
appInfo: {
getVersion: () => Promise<string>;
getSystemLocale: () => Promise<string>;