mirror of
https://gh-proxy.org/https://github.com/klinsmaya/LobsterAI.git
synced 2026-11-08 01:06:44 +08:00
Merge pull request #659 from wuleihenbang/feat/prevent-sleep
feat: 添加防休眠功能 & 实现 OpenClaw 助手消息与工具卡片交替显示
This commit is contained in:
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
|
||||
Vendored
+4
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user