diff --git a/docs/superpowers/plans/2026-03-21-prevent-sleep.md b/docs/superpowers/plans/2026-03-21-prevent-sleep.md new file mode 100644 index 0000000..faaea52 --- /dev/null +++ b/docs/superpowers/plans/2026-03-21-prevent-sleep.md @@ -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('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('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 ``, line 1949)** + +```tsx + {/* Prevent Sleep Section */} +
+

+ {i18nService.t('preventSleep')} +

+ +
+``` + +- [ ] **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" +``` diff --git a/docs/superpowers/specs/2026-03-21-prevent-sleep-design.md b/docs/superpowers/specs/2026-03-21-prevent-sleep-design.md new file mode 100644 index 0000000..ba7597f --- /dev/null +++ b/docs/superpowers/specs/2026-03-21-prevent-sleep-design.md @@ -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 diff --git a/src/main/libs/agentEngine/openclawRuntimeAdapter.ts b/src/main/libs/agentEngine/openclawRuntimeAdapter.ts index c1895e3..ed36787 100644 --- a/src/main/libs/agentEngine/openclawRuntimeAdapter.ts +++ b/src/main/libs/agentEngine/openclawRuntimeAdapter.ts @@ -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; + if (p.stream !== 'assistant') return; + + const dataField = isRecord(p.data) ? p.data as Record : 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, diff --git a/src/main/main.ts b/src/main/main.ts index e166390..4246f9f 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -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 | null = null; let openClawStatusForwarderBound = false; let coworkRuntimeForwarderBound = false; let memoryMigrationDone = false; +let preventSleepBlockerId: number | null = null; const initStore = async (): Promise => { if (!storeInitPromise) { @@ -1682,6 +1683,36 @@ if (!gotTheLock) { } }); + ipcMain.handle('app:getPreventSleep', () => { + const enabled = getStore().get('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('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('app_config')?.language; let lastUseSystemProxy = getUseSystemProxyFromConfig(getStore().get('app_config')); getStore().onDidChange('app_config', (newConfig, oldConfig) => { diff --git a/src/main/preload.ts b/src/main/preload.ts index 80d3f2f..64cb535 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -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'), diff --git a/src/renderer/components/Settings.tsx b/src/renderer/components/Settings.tsx index df246b5..c083f41 100644 --- a/src/renderer/components/Settings.tsx +++ b/src/renderer/components/Settings.tsx @@ -418,6 +418,8 @@ const Settings: React.FC = ({ 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(null); const [noticeMessage, setNoticeMessage] = useState(notice ?? null); @@ -643,7 +645,14 @@ const Settings: React.FC = ({ 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 = ({ onClose, initialTab, notice, onUpda + {/* Prevent Sleep Section */} +
+

+ {i18nService.t('preventSleep')} +

+ +
+ {/* System proxy Section */}

diff --git a/src/renderer/services/i18n.ts b/src/renderer/services/i18n.ts index 7d53772..be0be73 100644 --- a/src/renderer/services/i18n.ts +++ b/src/renderer/services/i18n.ts @@ -888,6 +888,8 @@ const translations: Record> = { autoLaunchDescription: '系统启动时自动运行应用', useSystemProxy: '使用系统代理', useSystemProxyDescription: '开启后网络请求将跟随系统代理(保存后生效)', + preventSleep: '防止休眠', + preventSleepDescription: '防止系统在应用运行时进入睡眠模式', // 定时任务 scheduledTasks: '定时任务', @@ -1962,6 +1964,8 @@ const translations: Record> = { 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', diff --git a/src/renderer/types/electron.d.ts b/src/renderer/types/electron.d.ts index b0417fa..d742e5b 100644 --- a/src/renderer/types/electron.d.ts +++ b/src/renderer/types/electron.d.ts @@ -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; getSystemLocale: () => Promise;