diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index cac3031dee6..7f09220e148 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -105,8 +105,8 @@ import { } from './hardening' import { createLinkTitleWindow, guardLinkTitleSession, readLinkTitleWindowTitle } from './link-title-window' import { ensureMainWindow } from './main-window-lifecycle' -import { createKeepAwake } from './power-save' import { serializeJsonBody, setJsonRequestHeaders } from './oauth-net-request' +import { createKeepAwake } from './power-save' import { decideProfileDeleteAction, profileNameFromDeleteRequest, resolveRouteProfile } from './profile-delete-routing' import { buildSessionWindowUrl, @@ -8556,11 +8556,31 @@ ipcMain.on('hermes:translucency', (_event, payload) => { } }) -// Keep-awake: the renderer owns the preference; main holds the one blocker. +// Keep-awake: hold the machine awake for long/overnight runs. Main owns the one +// blocker and its persisted state so a cold launch restores it (applied on +// ready — powerSaveBlocker needs the app ready). The renderer toggles it from +// Settings → Advanced over IPC. See store/keep-awake. +const KEEP_AWAKE_CONFIG_PATH = path.join(app.getPath('userData'), 'keep-awake.json') const keepAwake = createKeepAwake(powerSaveBlocker) +function readPersistedKeepAwake() { + try { + return JSON.parse(fs.readFileSync(KEEP_AWAKE_CONFIG_PATH, 'utf8')).on === true + } catch { + return false + } +} + ipcMain.on('hermes:keep-awake', (_event, on) => { - keepAwake.set(Boolean(on)) + const enabled = Boolean(on) + keepAwake.set(enabled) + + try { + fs.mkdirSync(path.dirname(KEEP_AWAKE_CONFIG_PATH), { recursive: true }) + fs.writeFileSync(KEEP_AWAKE_CONFIG_PATH, JSON.stringify({ on: enabled }, null, 2), 'utf8') + } catch (error) { + rememberLog(`[keep-awake] write failed: ${error.message}`) + } }) ipcMain.handle('hermes:openExternal', (_event, url) => { @@ -9563,6 +9583,7 @@ app.whenReady().then(() => { ensureWslWindowsFonts() configureSpellChecker() registerPowerResumeListeners() + keepAwake.set(readPersistedKeepAwake()) createWindow() // Win/Linux cold start: the launching hermes:// URL is in our own argv. diff --git a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx index 94485b4fef4..7d73180217a 100644 --- a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +++ b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx @@ -9,12 +9,11 @@ import { GatewayMenuPanel } from '@/app/shell/gateway-menu-panel' import { Codicon } from '@/components/ui/codicon' import { GlyphSpinner } from '@/components/ui/glyph-spinner' import { useI18n } from '@/i18n' -import { Activity, AlertCircle, Clock, Command, FolderOpen, Hash, Loader2, Sun, Terminal } from '@/lib/icons' +import { Activity, AlertCircle, Clock, Command, FolderOpen, Hash, Loader2, Terminal } from '@/lib/icons' import type { RuntimeReadinessResult } from '@/lib/runtime-readiness' import { contextBarLabel, LiveDuration, usageContextLabel } from '@/lib/statusbar' import { cn } from '@/lib/utils' import { copyFilePath, revealFile } from '@/store/file-actions' -import { $keepAwake, toggleKeepAwake } from '@/store/keep-awake' import { revealFileInTree } from '@/store/layout' import { $activeGatewayProfile } from '@/store/profile' import { $projectTree, projectNameForCwd } from '@/store/projects' @@ -91,7 +90,6 @@ export function useStatusbarItems({ const primaryActiveSessionId = useStore($activeSessionId) const activeGatewayProfile = useStore($activeGatewayProfile) const terminalTakeover = useStore($terminalTakeover) - const keepAwake = useStore($keepAwake) const primaryBusy = useStore($busy) const currentCwd = useStore($currentCwd) // Derive the workspace's project name from the already-cached project tree @@ -453,14 +451,6 @@ export function useStatusbarItems({ title: terminalTakeover ? copy.hideTerminal : copy.showTerminal, variant: 'action' }, - { - className: `w-7 justify-center px-0${keepAwake ? ' bg-accent/55 text-foreground' : ''}`, - icon: , - id: 'keep-awake', - onSelect: () => toggleKeepAwake(), - title: keepAwake ? copy.keepAwakeOn : copy.keepAwakeOff, - variant: 'action' - }, clientVersionItem, ...(backendVersionItem ? [backendVersionItem] : []) ], @@ -475,7 +465,6 @@ export function useStatusbarItems({ contextUsage, copy, currentUsage, - keepAwake, requestGateway, sessionStartedAt, gatewayState, diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index 3202e977934..a17ecbb3bd6 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -2176,8 +2176,6 @@ export const en: Translations = { openCommandCenter: 'Open Command Center', showTerminal: 'Show terminal', hideTerminal: 'Hide terminal', - keepAwakeOn: 'Keeping awake — click to allow sleep', - keepAwakeOff: 'Keep computer awake', gateway: 'Gateway', gatewayReady: 'ready', gatewayNeedsSetup: 'needs setup', diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index fabc1bc7b70..3ade882c6ab 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -2102,8 +2102,6 @@ export const ja = defineLocale({ openCommandCenter: 'コマンドセンターを開く', showTerminal: 'ターミナルを表示', hideTerminal: 'ターミナルを非表示', - keepAwakeOn: 'スリープ抑止中 — クリックで解除', - keepAwakeOff: 'コンピューターをスリープさせない', gateway: 'ゲートウェイ', gatewayReady: '準備完了', gatewayNeedsSetup: '設定が必要', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index c479706e29d..25a9f67e219 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -1804,8 +1804,6 @@ export interface Translations { openCommandCenter: string showTerminal: string hideTerminal: string - keepAwakeOn: string - keepAwakeOff: string gateway: string gatewayReady: string gatewayNeedsSetup: string diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index 986bf379235..d061a6d57a7 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -2035,8 +2035,6 @@ export const zhHant = defineLocale({ openCommandCenter: '開啟命令中心', showTerminal: '顯示終端機', hideTerminal: '隱藏終端機', - keepAwakeOn: '保持喚醒中 — 點擊以允許睡眠', - keepAwakeOff: '保持電腦喚醒', gateway: '閘道', gatewayReady: '就緒', gatewayNeedsSetup: '需要設定', diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index deb75047850..61d6ff602aa 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -2346,8 +2346,6 @@ export const zh: Translations = { openCommandCenter: '打开命令中心', showTerminal: '显示终端', hideTerminal: '隐藏终端', - keepAwakeOn: '保持唤醒中 — 点击以允许休眠', - keepAwakeOff: '保持电脑唤醒', gateway: '网关', gatewayReady: '就绪', gatewayNeedsSetup: '需要设置', diff --git a/apps/desktop/src/store/keep-awake.test.ts b/apps/desktop/src/store/keep-awake.test.ts index e6acffa9f93..4807e436ae4 100644 --- a/apps/desktop/src/store/keep-awake.test.ts +++ b/apps/desktop/src/store/keep-awake.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { storedBoolean } from '@/lib/storage' -import { $keepAwake, setKeepAwake, toggleKeepAwake } from './keep-awake' +import { $keepAwake, setKeepAwake } from './keep-awake' const KEY = 'hermes.desktop.keepAwake.v1' const desktopWindow = window as unknown as { hermesDesktop?: Window['hermesDesktop'] } @@ -30,11 +30,4 @@ describe('keep-awake store', () => { expect(storedBoolean(KEY, true)).toBe(false) expect(setKeepAwakeBridge).toHaveBeenLastCalledWith(false) }) - - it('toggles the current value', () => { - toggleKeepAwake() - expect($keepAwake.get()).toBe(true) - toggleKeepAwake() - expect($keepAwake.get()).toBe(false) - }) }) diff --git a/apps/desktop/src/store/keep-awake.ts b/apps/desktop/src/store/keep-awake.ts index b0db1cbfdb5..f2d2022391f 100644 --- a/apps/desktop/src/store/keep-awake.ts +++ b/apps/desktop/src/store/keep-awake.ts @@ -1,11 +1,12 @@ /** * Keep-awake — stop the machine sleeping during long, unattended runs. * - * A device-local preference (each computer keeps its own), off by default. The - * renderer owns the value and persists it; the main process holds the actual - * power-save blocker (see electron/power-save.ts) and re-reads this on every - * window load via the subscribe below. Linux/web builds without the bridge just - * no-op. + * A device-local preference (each computer keeps its own), off by default. This + * atom backs the Settings → Advanced toggle and mirrors changes to the main + * process, which owns the real power-save blocker AND its own persisted copy — + * so a cold launch restores the blocker without the renderer visiting Settings + * (see electron/main.ts + electron/power-save.ts). Linux/web without the bridge + * just no-op. */ import { atom } from 'nanostores' @@ -20,10 +21,6 @@ export function setKeepAwake(on: boolean): void { $keepAwake.set(on) } -export function toggleKeepAwake(): void { - $keepAwake.set(!$keepAwake.get()) -} - if (typeof window !== 'undefined') { $keepAwake.subscribe(on => { persistBoolean(KEY, on)