From bc4a7d358a75924c27777b984f522a9faa07846b Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 1 Jul 2026 19:07:20 +0300 Subject: [PATCH] feat(desktop): add keep-awake setting --- packages/electron/main.mjs | 45 ++++++++++- .../openchamber/DesktopNetworkSettings.tsx | 79 +++++++++++++++++++ packages/ui/src/lib/desktop.ts | 42 +++++++++- .../ui/src/lib/i18n/messages/en.settings.ts | 5 ++ .../ui/src/lib/i18n/messages/es.settings.ts | 5 ++ .../ui/src/lib/i18n/messages/fr.settings.ts | 5 ++ .../ui/src/lib/i18n/messages/ja.settings.ts | 5 ++ .../ui/src/lib/i18n/messages/ko.settings.ts | 5 ++ .../ui/src/lib/i18n/messages/pl.settings.ts | 5 ++ .../src/lib/i18n/messages/pt-BR.settings.ts | 5 ++ .../ui/src/lib/i18n/messages/uk.settings.ts | 5 ++ .../src/lib/i18n/messages/zh-CN.settings.ts | 5 ++ .../src/lib/i18n/messages/zh-TW.settings.ts | 5 ++ packages/ui/src/lib/settings/search.ts | 8 ++ .../server/lib/opencode/settings-helpers.js | 3 + .../lib/opencode/settings-helpers.test.js | 11 +++ 16 files changed, 236 insertions(+), 2 deletions(-) diff --git a/packages/electron/main.mjs b/packages/electron/main.mjs index 91ec2f78..826c4dcb 100644 --- a/packages/electron/main.mjs +++ b/packages/electron/main.mjs @@ -1,4 +1,4 @@ -import { app, BrowserWindow, dialog, ipcMain, Menu, nativeTheme, net as electronNet, Notification, powerMonitor, protocol, screen, session, shell, webContents } from 'electron'; +import { app, BrowserWindow, dialog, ipcMain, Menu, nativeTheme, net as electronNet, Notification, powerMonitor, powerSaveBlocker, protocol, screen, session, shell, webContents } from 'electron'; import contextMenu from 'electron-context-menu'; import log from 'electron-log/main.js'; import dgram from 'node:dgram'; @@ -193,6 +193,32 @@ const state = { sshLogs: new Map(), trayController: null, lastFocusedWindowId: null, + keepAwakeBlockerId: null, +}; + +const setDesktopKeepAwakeActive = (enabled) => { + const currentId = state.keepAwakeBlockerId; + const isActive = Number.isInteger(currentId) && powerSaveBlocker.isStarted(currentId); + + if (enabled) { + if (!isActive) { + state.keepAwakeBlockerId = powerSaveBlocker.start('prevent-app-suspension'); + } + return Number.isInteger(state.keepAwakeBlockerId) && powerSaveBlocker.isStarted(state.keepAwakeBlockerId); + } + + if (isActive) { + powerSaveBlocker.stop(currentId); + } + state.keepAwakeBlockerId = null; + return false; +}; + +const readDesktopKeepAwakeStatus = () => { + const enabled = readSettingsRoot().desktopKeepAwakeEnabled === true; + const currentId = state.keepAwakeBlockerId; + const active = Number.isInteger(currentId) && powerSaveBlocker.isStarted(currentId); + return { supported: true, enabled, active }; }; const quitRisk = { @@ -228,6 +254,7 @@ const quitConfirmationMessage = () => { const shutdownBackgroundServices = () => { if (state.backgroundShutdownComplete) return; state.backgroundShutdownComplete = true; + setDesktopKeepAwakeActive(false); if (state.installingUpdate) return; killSidecar(); setImmediate(() => { @@ -271,6 +298,8 @@ const prepareForQuit = ({ installingUpdate = false } = {}) => { } } + setDesktopKeepAwakeActive(false); + if (installingUpdate) { state.backgroundShutdownComplete = true; return; @@ -1141,6 +1170,7 @@ const spawnLocalServer = async () => { // so phones/tablets on the same Wi-Fi can reach the app. UI shows a clear // warning and persists the flag via /api/config/settings. const lanAccessEnabled = settings.desktopLanAccessEnabled === true; + setDesktopKeepAwakeActive(settings.desktopKeepAwakeEnabled === true); const desktopUiPassword = typeof settings.desktopUiPassword === 'string' ? settings.desktopUiPassword.trim() : ''; const lanAccessBlockedByMissingPassword = lanAccessEnabled && !desktopUiPassword; const effectiveLanAccessEnabled = lanAccessEnabled && !lanAccessBlockedByMissingPassword; @@ -3166,6 +3196,19 @@ const handleInvoke = async (browserWindow, command, args = {}) => { return { supported: true, enabled: settings.openAtLogin === true }; } + case 'desktop_get_keep_awake': { + return readDesktopKeepAwakeStatus(); + } + + case 'desktop_set_keep_awake': { + const enabled = args.enabled === true; + await mutateSettingsRoot((root) => { + root.desktopKeepAwakeEnabled = enabled; + }); + const active = setDesktopKeepAwakeActive(enabled); + return { supported: true, enabled, active }; + } + case 'desktop_browser_capture_page': { const wcId = Number.isFinite(args.webContentsId) ? Math.trunc(args.webContentsId) : null; if (wcId === null || wcId < 0) throw new Error('webContentsId is required'); diff --git a/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx b/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx index a2861c3c..661d492f 100644 --- a/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx @@ -5,10 +5,12 @@ import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { getDesktopLanAddress, + getDesktopKeepAwake, getDesktopLaunchAtLogin, isDesktopLocalOriginActive, isDesktopShell, restartDesktopApp, + setDesktopKeepAwake, setDesktopLaunchAtLogin, } from '@/lib/desktop'; import { useI18n } from '@/lib/i18n'; @@ -29,6 +31,9 @@ export const DesktopNetworkSettings: React.FC = () => { const [launchAtLoginSupported, setLaunchAtLoginSupported] = React.useState(false); const [launchAtLoginEnabled, setLaunchAtLoginEnabled] = React.useState(false); const [isSavingLaunchAtLogin, setIsSavingLaunchAtLogin] = React.useState(false); + const [keepAwakeSupported, setKeepAwakeSupported] = React.useState(false); + const [keepAwakeEnabled, setKeepAwakeEnabled] = React.useState(false); + const [isSavingKeepAwake, setIsSavingKeepAwake] = React.useState(false); const [error, setError] = React.useState(null); const [lanAddress, setLanAddress] = React.useState(null); @@ -107,6 +112,27 @@ export const DesktopNetworkSettings: React.FC = () => { }; }, [isLocalDesktop]); + React.useEffect(() => { + if (!isLocalDesktop) { + setKeepAwakeSupported(false); + return; + } + + let cancelled = false; + void (async () => { + const status = await getDesktopKeepAwake(); + if (cancelled) { + return; + } + setKeepAwakeSupported(status?.supported === true); + setKeepAwakeEnabled(status?.enabled === true); + })(); + + return () => { + cancelled = true; + }; + }, [isLocalDesktop]); + React.useEffect(() => { if (!isLocalDesktop || !draftValue) { setLanAddress(null); @@ -183,6 +209,30 @@ export const DesktopNetworkSettings: React.FC = () => { } }, [isSavingLaunchAtLogin, launchAtLoginEnabled, launchAtLoginSupported, t]); + const handleKeepAwakeToggle = React.useCallback(async () => { + if (!keepAwakeSupported || isSavingKeepAwake) { + return; + } + + const nextValue = !keepAwakeEnabled; + setKeepAwakeEnabled(nextValue); + setIsSavingKeepAwake(true); + setError(null); + + try { + const status = await setDesktopKeepAwake(nextValue); + if (!status?.supported) { + throw new Error(t('settings.openchamber.desktopNetwork.error.keepAwakeUnsupported')); + } + setKeepAwakeEnabled(status.enabled); + } catch (cause) { + setKeepAwakeEnabled(!nextValue); + setError(cause instanceof Error ? cause.message : t('settings.openchamber.desktopNetwork.error.keepAwakeSaveFailed')); + } finally { + setIsSavingKeepAwake(false); + } + }, [isSavingKeepAwake, keepAwakeEnabled, keepAwakeSupported, t]); + const handleSaveAndRestart = React.useCallback(async () => { if (!isDirty) { return; @@ -261,6 +311,35 @@ export const DesktopNetworkSettings: React.FC = () => { ) : null} + {keepAwakeSupported ? ( +
{ + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleKeepAwakeToggle(); + } + }} + > + +
+
{t('settings.openchamber.desktopNetwork.field.keepAwake')}
+
+ {t('settings.openchamber.desktopNetwork.field.keepAwakeDescription')} +
+
+
+ ) : null} +