From 8b21551e3e6fcd6dde21553d264affdbd20e135e Mon Sep 17 00:00:00 2001 From: thomas-yanga Date: Sun, 26 Jul 2026 18:29:56 +0800 Subject: [PATCH 1/2] feat(desktop): add prevent-sleep settings toggle The desktop app unconditionally started a powerSaveBlocker at launch with no user-facing way to opt out. Add a "Prevent system sleep" switch under a new Power section in General settings (desktop only). - main: persist preventSleep in the settings store; default stays enabled (only explicit false opts out) so existing installs keep the historical always-on behavior; set-prevent-sleep IPC starts/stops the blocker live and broadcasts to all windows - preload/renderer/app: bridge get/setPreventSleepEnabled through the platform context, mirroring the pinchZoom pattern - i18n: translate the new section and row across all 18 locales - tests: unit-cover the persisted-setting resolution (Electron-free, same split as close-to-tray); add prevent-sleep-smoke, a Playwright-driven dynamic test that clicks the real Settings switch and asserts macOS pmset NoIdleSleepAssertion across default-on, disable, relaunch persistence, and re-enable - ci: run the smoke in a new desktop-smoke (macos) job in test.yml Signed-off-by: thomas-yanga --- .github/workflows/test.yml | 32 ++++++ .../src/components/settings-v2/general.tsx | 34 ++++++ packages/app/src/context/platform.tsx | 6 + packages/app/src/i18n/ar.ts | 3 + packages/app/src/i18n/br.ts | 3 + packages/app/src/i18n/bs.ts | 3 + packages/app/src/i18n/da.ts | 3 + packages/app/src/i18n/de.ts | 3 + packages/app/src/i18n/en.ts | 3 + packages/app/src/i18n/es.ts | 3 + packages/app/src/i18n/fr.ts | 3 + packages/app/src/i18n/ja.ts | 3 + packages/app/src/i18n/ko.ts | 3 + packages/app/src/i18n/no.ts | 3 + packages/app/src/i18n/pl.ts | 3 + packages/app/src/i18n/ru.ts | 3 + packages/app/src/i18n/th.ts | 3 + packages/app/src/i18n/tr.ts | 3 + packages/app/src/i18n/uk.ts | 3 + packages/app/src/i18n/zh.ts | 3 + packages/app/src/i18n/zht.ts | 3 + packages/desktop/package.json | 1 + .../desktop/scripts/prevent-sleep-smoke.ts | 106 ++++++++++++++++++ packages/desktop/src/main/index.ts | 7 +- packages/desktop/src/main/ipc.ts | 8 ++ packages/desktop/src/main/power.test.ts | 21 ++++ packages/desktop/src/main/power.ts | 22 ++++ packages/desktop/src/main/prevent-sleep.ts | 6 + packages/desktop/src/main/store-keys.ts | 1 + packages/desktop/src/preload/index.ts | 7 ++ packages/desktop/src/preload/types.ts | 3 + packages/desktop/src/renderer/index.tsx | 4 + 32 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 packages/desktop/scripts/prevent-sleep-smoke.ts create mode 100644 packages/desktop/src/main/power.test.ts create mode 100644 packages/desktop/src/main/prevent-sleep.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4849069..3c55bd09 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,6 +96,38 @@ jobs: retention-days: 7 path: packages/*/.artifacts/unit/junit.xml + desktop-smoke: + name: desktop-smoke (macos) + # The prevent-sleep smoke drives the real packaged main bundle through Playwright and asserts + # against pmset, so it needs a macOS runner with a window server; keep it out of the unit matrix. + runs-on: macos-14 + defaults: + run: + shell: bash + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "24" + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Build desktop main bundle + working-directory: packages/desktop + run: bun run build + timeout-minutes: 15 + + - name: Run prevent-sleep smoke + working-directory: packages/desktop + run: bun run test:prevent-sleep-smoke + timeout-minutes: 10 + e2e: name: e2e (${{ matrix.settings.name }}) strategy: diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx index 4594c5fc..e447a6c4 100644 --- a/packages/app/src/components/settings-v2/general.tsx +++ b/packages/app/src/components/settings-v2/general.tsx @@ -132,6 +132,12 @@ export const SettingsGeneralV2: Component = () => { { initialValue: false }, ) + const [preventSleep, { mutate: setPreventSleep }] = createResource( + () => (desktop() && platform.getPreventSleepEnabled ? true : false), + () => Promise.resolve(platform.getPreventSleepEnabled?.() ?? true).catch(() => true), + { initialValue: true }, + ) + onMount(() => { void theme.loadThemes() }) @@ -252,6 +258,13 @@ export const SettingsGeneralV2: Component = () => { void update.catch(() => setPinchZoom(!checked)) } + const onPreventSleepChange = (checked: boolean) => { + setPreventSleep(checked) + const update = platform.setPreventSleepEnabled?.(checked) + if (!update) return + void update.catch(() => setPreventSleep(!checked)) + } + const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [ { value: "system", label: language.t("theme.scheme.system") }, { value: "light", label: language.t("theme.scheme.light") }, @@ -915,6 +928,25 @@ export const SettingsGeneralV2: Component = () => { ) + const PowerSection = () => ( + +
+

{language.t("settings.general.section.power")}

+ + + +
+ +
+
+
+
+
+ ) + return ( <>
@@ -940,6 +972,8 @@ export const SettingsGeneralV2: Component = () => { + +
diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index d2806fda..98d8548f 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -122,6 +122,12 @@ type PlatformBase = { /** Allow native pinch/Ctrl-scroll zoom gestures (desktop only) */ setPinchZoomEnabled?(enabled: boolean): Promise | void + /** Get whether the app keeps the system awake while running (desktop only) */ + getPreventSleepEnabled?(): Promise | boolean + + /** Keep the system awake while the app is running (desktop only) */ + setPreventSleepEnabled?(enabled: boolean): Promise | void + /** Run a desktop-only menu action from the app chrome */ runDesktopMenuAction?(action: DesktopMenuAction): Promise | void diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 181b543c..f94a0fe2 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -560,6 +560,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "تفعيل لجنة الخبراء افتراضيًا", "settings.general.row.expertPanelDefault.description": "بدء المحادثات الجديدة مع تفعيل لجنة الخبراء، بحيث يبدأ زرها مراجعة السياق الحالي عند الطلب.", "settings.general.section.display": "شاشة العرض", + "settings.general.section.power": "الطاقة", "settings.general.row.language.title": "اللغة", "settings.general.row.language.description": "تغيير لغة العرض لـ DeepAgent Code", "settings.general.row.appearance.title": "المظهر", @@ -930,6 +931,8 @@ export const dict = { "تفعيل التخطيط والصفحة الرئيسية والمحرر وواجهة الجلسة المعاد تصميمها", "settings.general.row.pinchZoom.title": "التكبير بالقرص", "settings.general.row.pinchZoom.description": "السماح بإيماءات قرص لوحة اللمس و Ctrl-تمرير للتكبير", + "settings.general.row.preventSleep.title": "منع سكون النظام", + "settings.general.row.preventSleep.description": "إبقاء النظام مستيقظًا أثناء تشغيل التطبيق؛ ويمكن أن تنطفئ الشاشة", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "جار التنزيل...", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index b2ff7bef..31699b30 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -567,6 +567,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "Ativar painel de especialistas por padrão", "settings.general.row.expertPanelDefault.description": "Iniciar novas conversas com o painel de especialistas ativado, para que seu botão inicie uma revisão do contexto atual sob demanda.", "settings.general.section.display": "Tela", + "settings.general.section.power": "Energia", "settings.general.row.language.title": "Idioma", "settings.general.row.language.description": "Alterar o idioma de exibição do DeepAgent Code", "settings.general.row.appearance.title": "Aparência", @@ -954,6 +955,8 @@ export const dict = { "settings.general.row.pinchZoom.title": "Pinçar para zoom", "settings.general.row.pinchZoom.description": "Permitir gestos de pellizco en trackpad y Ctrl-desplazamiento para ampliar", + "settings.general.row.preventSleep.title": "Impedir suspensão do sistema", + "settings.general.row.preventSleep.description": "Manter o sistema ativo enquanto o app está em execução; a tela ainda pode desligar", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "Baixando...", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index cc9f9115..d97be89f 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -632,6 +632,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "Zadano uključi ekspertni panel", "settings.general.row.expertPanelDefault.description": "Pokreni nove razgovore s uključenim ekspertnim panelom, tako da njegovo dugme po potrebi pokreće pregled trenutnog konteksta.", "settings.general.section.display": "Prikaz", + "settings.general.section.power": "Napajanje", "settings.general.row.language.title": "Jezik", "settings.general.row.language.description": "Promijeni jezik prikaza u DeepAgent Code-u", @@ -1027,6 +1028,8 @@ export const dict = { "settings.general.row.pinchZoom.title": "Powiększanie gestem szczypania", "settings.general.row.pinchZoom.description": "Zezwól na powiększanie gestem szczypania na trackpadzie i Ctrl-przewijaniem", + "settings.general.row.preventSleep.title": "Spriječi mirovanje sistema", + "settings.general.row.preventSleep.description": "Zadržava sistem budnim dok je aplikacija pokrenuta; ekran se i dalje može ugasiti", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "Preuzimanje...", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 6af26897..b725d224 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -627,6 +627,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "Aktivér ekspertpanel som standard", "settings.general.row.expertPanelDefault.description": "Start nye samtaler med ekspertpanelet aktiveret, så knappen kan gennemgå den aktuelle kontekst efter behov.", "settings.general.section.display": "Skærm", + "settings.general.section.power": "Strøm", "settings.general.row.language.title": "Sprog", "settings.general.row.language.description": "Ændr visningssproget for DeepAgent Code", @@ -1016,6 +1017,8 @@ export const dict = { "settings.general.row.newLayoutDesigns.description": "Aktivér det redesignede layout, hjem, composer og sessions-UI", "settings.general.row.pinchZoom.title": "Knib for at zoome", "settings.general.row.pinchZoom.description": "Tillad trackpad-knib og Ctrl-scroll til zoom", + "settings.general.row.preventSleep.title": "Forhindr dvale", + "settings.general.row.preventSleep.description": "Hold systemet vågent, mens appen kører; skærmen kan stadig slukkes", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "Downloader...", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index e682c164..f1972566 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -576,6 +576,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "Expertengremium standardmäßig aktivieren", "settings.general.row.expertPanelDefault.description": "Neue Unterhaltungen mit aktiviertem Expertengremium starten, damit dessen Schaltfläche den aktuellen Kontext bei Bedarf prüft.", "settings.general.section.display": "Anzeige", + "settings.general.section.power": "Energie", "settings.general.row.language.title": "Sprache", "settings.general.row.language.description": "Die Anzeigesprache für DeepAgent Code ändern", "settings.general.row.appearance.title": "Erscheinungsbild", @@ -961,6 +962,8 @@ export const dict = { "Überarbeitetes Layout, Startseite, Composer und Sitzungs-UI aktivieren", "settings.general.row.pinchZoom.title": "Zum Zoomen kneifen", "settings.general.row.pinchZoom.description": "Trackpad-Kneifen und Strg-Scrollen zum Zoomen erlauben", + "settings.general.row.preventSleep.title": "Ruhezustand des Systems verhindern", + "settings.general.row.preventSleep.description": "Hält das System wach, solange die App läuft; der Bildschirm kann sich weiterhin ausschalten", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "Wird heruntergeladen...", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index b385af78..bcd0e3c5 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -1204,6 +1204,7 @@ export const dict = { "settings.general.section.sounds": "Sound effects", "settings.general.section.feed": "Feed", "settings.general.section.display": "Display", + "settings.general.section.power": "Power", "settings.general.section.sharing": "Sharing", "settings.general.row.language.title": "Language", @@ -1367,6 +1368,8 @@ export const dict = { "settings.general.row.newLayoutDesigns.description": "Enable the redesigned layout, home, composer, and session UI", "settings.general.row.pinchZoom.title": "Pinch to zoom", "settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom", + "settings.general.row.preventSleep.title": "Prevent system sleep", + "settings.general.row.preventSleep.description": "Keep the system awake while the app is running; the display can still turn off", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 5ec07a63..0d6b9085 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -635,6 +635,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "Activar el panel de expertos por defecto", "settings.general.row.expertPanelDefault.description": "Iniciar nuevas conversaciones con el panel de expertos activado, para que su botón revise el contexto actual cuando se solicite.", "settings.general.section.display": "Pantalla", + "settings.general.section.power": "Energía", "settings.general.row.language.title": "Idioma", "settings.general.row.language.description": "Cambiar el idioma de visualización para DeepAgent Code", @@ -1037,6 +1038,8 @@ export const dict = { "settings.general.row.pinchZoom.title": "Pellizcar para ampliar", "settings.general.row.pinchZoom.description": "Permitir gestos de pellizco en trackpad y Ctrl-desplazamiento para ampliar", + "settings.general.row.preventSleep.title": "Evitar la suspensión del sistema", + "settings.general.row.preventSleep.description": "Mantener el sistema activo mientras la app está en ejecución; la pantalla aún puede apagarse", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "Descargando...", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 9e01f181..93df50ca 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -574,6 +574,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "Activer le panel d'experts par défaut", "settings.general.row.expertPanelDefault.description": "Démarrer les nouvelles conversations avec le panel d'experts activé, afin que son bouton examine le contexte actuel à la demande.", "settings.general.section.display": "Affichage", + "settings.general.section.power": "Alimentation", "settings.general.row.language.title": "Langue", "settings.general.row.language.description": "Changer la langue d'affichage pour DeepAgent Code", "settings.general.row.appearance.title": "Apparence", @@ -962,6 +963,8 @@ export const dict = { "Activer la mise en page, l’accueil, le compositeur et l’interface de session repensés", "settings.general.row.pinchZoom.title": "Pincer pour zoomer", "settings.general.row.pinchZoom.description": "Autoriser le pincement du trackpad et Ctrl-défilement pour zoomer", + "settings.general.row.preventSleep.title": "Empêcher la mise en veille du système", + "settings.general.row.preventSleep.description": "Garde le système actif pendant que l'app est en cours d'exécution ; l'écran peut toujours s'éteindre", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "Téléchargement...", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index ad5e45cb..c79ca0f3 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -566,6 +566,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "エキスパートパネルをデフォルトで有効化", "settings.general.row.expertPanelDefault.description": "新しい会話をエキスパートパネル有効の状態で開始し、ボタンで現在のコンテキストをいつでも会議レビューできるようにします。", "settings.general.section.display": "ディスプレイ", + "settings.general.section.power": "電源", "settings.general.row.language.title": "言語", "settings.general.row.language.description": "DeepAgent Codeの表示言語を変更します", "settings.general.row.appearance.title": "外観", @@ -943,6 +944,8 @@ export const dict = { "再設計されたレイアウト、ホーム、コンポーザー、セッションUIを有効にします", "settings.general.row.pinchZoom.title": "ピンチでズーム", "settings.general.row.pinchZoom.description": "トラックパッドのピンチとCtrlスクロールでズームを許可します", + "settings.general.row.preventSleep.title": "システムのスリープを防止", + "settings.general.row.preventSleep.description": "アプリの実行中はシステムをスリープさせません。ディスプレイはオフにできます", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "ダウンロード中...", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index d8f0e408..1b742aee 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -562,6 +562,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "전문가 패널을 기본으로 활성화", "settings.general.row.expertPanelDefault.description": "새 대화를 전문가 패널이 활성화된 상태로 시작하여 버튼으로 현재 컨텍스트를 필요할 때 검토할 수 있게 합니다.", "settings.general.section.display": "디스플레이", + "settings.general.section.power": "전원", "settings.general.row.language.title": "언어", "settings.general.row.language.description": "DeepAgent Code 표시 언어 변경", "settings.general.row.appearance.title": "모양", @@ -934,6 +935,8 @@ export const dict = { "settings.general.row.newLayoutDesigns.description": "재설계된 레이아웃, 홈, 작성기 및 세션 UI를 활성화합니다", "settings.general.row.pinchZoom.title": "핀치로 확대/축소", "settings.general.row.pinchZoom.description": "트랙패드 핀치와 Ctrl-스크롤 제스처로 확대/축소를 허용합니다", + "settings.general.row.preventSleep.title": "시스템 절전 방지", + "settings.general.row.preventSleep.description": "앱이 실행되는 동안 시스템을 절전 상태로 전환하지 않습니다. 디스플레이는 꺼질 수 있습니다", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "다운로드 중...", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 2d21e9a8..d0a87222 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -635,6 +635,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "Aktiver ekspertpanel som standard", "settings.general.row.expertPanelDefault.description": "Start nye samtaler med ekspertpanelet aktivert, slik at knappen kan gjennomgå gjeldende kontekst ved behov.", "settings.general.section.display": "Skjerm", + "settings.general.section.power": "Strøm", "settings.general.row.language.title": "Språk", "settings.general.row.language.description": "Endre visningsspråket for DeepAgent Code", @@ -1023,6 +1024,8 @@ export const dict = { "settings.general.row.newLayoutDesigns.description": "Aktivér det redesignede layout, hjem, composer og sessions-UI", "settings.general.row.pinchZoom.title": "Knib for at zoome", "settings.general.row.pinchZoom.description": "Tillad trackpad-knib og Ctrl-scroll til zoom", + "settings.general.row.preventSleep.title": "Forhindre hvilemodus", + "settings.general.row.preventSleep.description": "Hold systemet våkent mens appen kjører; skjermen kan fortsatt slås av", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "Laster ned...", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 8ade8e49..ea9daff2 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -566,6 +566,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "Domyślnie włącz panel ekspertów", "settings.general.row.expertPanelDefault.description": "Rozpoczynaj nowe rozmowy z włączonym panelem ekspertów, aby jego przycisk mógł na żądanie przeglądać bieżący kontekst.", "settings.general.section.display": "Ekran", + "settings.general.section.power": "Zasilanie", "settings.general.row.language.title": "Język", "settings.general.row.language.description": "Zmień język wyświetlania dla DeepAgent Code", "settings.general.row.appearance.title": "Wygląd", @@ -949,6 +950,8 @@ export const dict = { "settings.general.row.pinchZoom.title": "Powiększanie gestem szczypania", "settings.general.row.pinchZoom.description": "Zezwól na powiększanie gestem szczypania na trackpadzie i Ctrl-przewijaniem", + "settings.general.row.preventSleep.title": "Zapobiegaj uśpieniu systemu", + "settings.general.row.preventSleep.description": "Utrzymuj system aktywny, gdy aplikacja jest uruchomiona; ekran nadal może się wyłączyć", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "Pobieranie...", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index c88ca046..7e599757 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -631,6 +631,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "Включить экспертную панель по умолчанию", "settings.general.row.expertPanelDefault.description": "Начинать новые беседы с включённой экспертной панелью, чтобы её кнопка по запросу проверяла текущий контекст.", "settings.general.section.display": "Дисплей", + "settings.general.section.power": "Питание", "settings.general.row.language.title": "Язык", "settings.general.row.language.description": "Изменить язык отображения DeepAgent Code", @@ -1028,6 +1029,8 @@ export const dict = { "Включить обновленный макет, главную страницу, композер и интерфейс сессии", "settings.general.row.pinchZoom.title": "Масштабирование щипком", "settings.general.row.pinchZoom.description": "Разрешить масштабирование щипком на трекпаде и Ctrl-прокруткой", + "settings.general.row.preventSleep.title": "Запретить сон системы", + "settings.general.row.preventSleep.description": "Не давать системе засыпать, пока приложение запущено; экран по-прежнему может гаснуть", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "Загрузка...", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index d9822fb2..8cea2edd 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -626,6 +626,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "เปิดใช้คณะผู้เชี่ยวชาญโดยค่าเริ่มต้น", "settings.general.row.expertPanelDefault.description": "เริ่มการสนทนาใหม่โดยเปิดใช้คณะผู้เชี่ยวชาญ เพื่อให้ปุ่มสามารถตรวจทานบริบทปัจจุบันได้ตามต้องการ", "settings.general.section.display": "การแสดงผล", + "settings.general.section.power": "พลังงาน", "settings.general.row.language.title": "ภาษา", "settings.general.row.language.description": "เปลี่ยนภาษาที่แสดงสำหรับ DeepAgent Code", @@ -1011,6 +1012,8 @@ export const dict = { "settings.general.row.newLayoutDesigns.description": "เปิดใช้เลย์เอาต์ หน้าแรก ตัวเขียน และ UI เซสชันที่ออกแบบใหม่", "settings.general.row.pinchZoom.title": "จีบนิ้วเพื่อซูม", "settings.general.row.pinchZoom.description": "อนุญาตท่าทางจีบนิ้วบนแทร็กแพดและ Ctrl-เลื่อนเพื่อซูม", + "settings.general.row.preventSleep.title": "ป้องกันการสลีปของระบบ", + "settings.general.row.preventSleep.description": "ทำให้ระบบทำงานต่อเนื่องขณะแอปกำลังทำงาน หน้าจอยังคงดับได้", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "กำลังดาวน์โหลด...", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index ce03a0a0..e4abc007 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -637,6 +637,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "Uzman panelini varsayılan olarak etkinleştir", "settings.general.row.expertPanelDefault.description": "Yeni sohbetleri uzman paneli etkin olarak başlatın; böylece düğmesi istendiğinde mevcut bağlamı inceler.", "settings.general.section.display": "Ekran", + "settings.general.section.power": "Güç", "settings.general.row.language.title": "Dil", "settings.general.row.language.description": "DeepAgent Code'un görünüm dilini değiştirin", @@ -1036,6 +1037,8 @@ export const dict = { "settings.general.row.pinchZoom.title": "Sıkıştırarak yakınlaştır", "settings.general.row.pinchZoom.description": "Trackpad sıkıştırma ve Ctrl-kaydırma hareketleriyle yakınlaştırmaya izin ver", + "settings.general.row.preventSleep.title": "Sistem uykusunu engelle", + "settings.general.row.preventSleep.description": "Uygulama çalışırken sistemi uyanık tutar; ekran yine de kapanabilir", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "İndiriliyor...", diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts index 931f58d4..7a82b2a5 100644 --- a/packages/app/src/i18n/uk.ts +++ b/packages/app/src/i18n/uk.ts @@ -735,6 +735,7 @@ export const dict = { "settings.general.row.expertPanelDefault.title": "Типово вмикати експертну панель", "settings.general.row.expertPanelDefault.description": "Починати нові розмови з увімкненою експертною панеллю, щоб її кнопка за потреби переглядала поточний контекст.", "settings.general.section.display": "Дисплей", + "settings.general.section.power": "Живлення", "settings.general.row.language.title": "Мова", "settings.general.row.language.description": "Змінити мову інтерфейсу DeepAgent Code", @@ -1044,6 +1045,8 @@ export const dict = { "Включить обновленный макет, главную страницу, композер и интерфейс сессии", "settings.general.row.pinchZoom.title": "Масштабування щипком", "settings.general.row.pinchZoom.description": "Разрешить масштабирование щипком на трекпаде и Ctrl-прокруткой", + "settings.general.row.preventSleep.title": "Запобігати сну системи", + "settings.general.row.preventSleep.description": "Не давати системі засинати, доки застосунок працює; екран все одно може вимикатися", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "Завантаження...", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index f9c26a02..fba9bd37 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -995,6 +995,7 @@ export const dict = { "settings.general.section.sounds": "音效", "settings.general.section.feed": "动态", "settings.general.section.display": "显示", + "settings.general.section.power": "电源", "settings.general.section.sharing": "分享", "settings.general.row.shareUrl.title": "分享服务器地址", "settings.general.row.shareUrl.description": "分享会话时使用的服务器基础地址。留空则使用默认值。", @@ -1442,6 +1443,8 @@ export const dict = { "settings.general.row.newLayoutDesigns.description": "启用重新设计的布局、首页、输入器和会话界面", "settings.general.row.pinchZoom.title": "双指缩放", "settings.general.row.pinchZoom.description": "允许触控板双指和 Ctrl 滚动手势缩放", + "settings.general.row.preventSleep.title": "阻止系统休眠", + "settings.general.row.preventSleep.description": "应用运行时保持系统唤醒,显示器仍可关闭", "settings.general.row.zoom.title": "缩放比例", "settings.general.row.zoom.description": "设置界面缩放比例", "settings.updates.action.downloading": "正在下载...", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index fb825dae..eb1a5734 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -781,6 +781,7 @@ export const dict = { "settings.general.section.sounds": "音效", "settings.general.section.feed": "資訊流", "settings.general.section.display": "顯示", + "settings.general.section.power": "電源", "settings.general.section.sharing": "分享", "settings.general.row.shareUrl.title": "分享伺服器網址", "settings.general.row.shareUrl.description": "分享工作階段時使用的伺服器基礎網址。留空則使用預設值。", @@ -1164,6 +1165,8 @@ export const dict = { "settings.general.row.newLayoutDesigns.description": "啟用重新設計的布局、首頁、輸入器和會話介面", "settings.general.row.pinchZoom.title": "雙指縮放", "settings.general.row.pinchZoom.description": "允許觸控板雙指和 Ctrl 捲動手勢縮放", + "settings.general.row.preventSleep.title": "防止系統休眠", + "settings.general.row.preventSleep.description": "應用程式執行時保持系統喚醒,顯示器仍可關閉", "settings.general.row.zoom.title": "Zoom level", "settings.general.row.zoom.description": "Set the interface zoom level", "settings.updates.action.downloading": "正在下載...", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index ed67945e..7af9f0b6 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -19,6 +19,7 @@ "test": "bun test ./src", "test:ci": "mkdir -p .artifacts/unit && bun test ./src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", "test:subagents-smoke": "node --experimental-strip-types ./scripts/subagents-smoke.ts", + "test:prevent-sleep-smoke": "node --experimental-strip-types ./scripts/prevent-sleep-smoke.ts", "test:subagents-sourcemap": "bun ./scripts/verify-subagents-sourcemap.ts", "package": "bun ./scripts/package.ts", "package:mac": "bun ./scripts/package.ts --mac", diff --git a/packages/desktop/scripts/prevent-sleep-smoke.ts b/packages/desktop/scripts/prevent-sleep-smoke.ts new file mode 100644 index 00000000..34d42ccd --- /dev/null +++ b/packages/desktop/scripts/prevent-sleep-smoke.ts @@ -0,0 +1,106 @@ +#!/usr/bin/env bun +// Dynamic smoke test for the "Prevent system sleep" settings toggle (macOS only). +// +// Drives the real packaged main bundle through Playwright and asserts against the OS: +// 1. default launch registers a NoIdleSleepAssertion (historical always-on default) +// 2. toggling the switch off in Settings removes the assertion while the app keeps running +// 3. the disabled state persists across a relaunch (no assertion after restart) +// 4. toggling back on re-registers the assertion +// +// Requires a prior `bun run build` (uses out/main/index.js), same as subagents-smoke. +import { strict as assert } from "node:assert" +import { execFileSync } from "node:child_process" +import { mkdtemp, realpath } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join, resolve } from "node:path" +import { _electron as electron, type ElectronApplication, type Page } from "@playwright/test" + +if (process.platform !== "darwin") { + console.log("prevent-sleep smoke is macOS-only (pmset assertions), skipping") + process.exit(0) +} + +const root = await realpath(await mkdtemp(join(tmpdir(), "deepagent-code-prevent-sleep-smoke-"))) +const main = resolve("out/main/index.js") + +const env = Object.fromEntries( + Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined), +) +env.DEEPAGENT_CODE_TEST_ONBOARDING = "1" +env.DEEPAGENT_CODE_TEST_ROOT = root +env.DEEPAGENT_CODE_DB = join(root, "deepagent.sqlite") +env.DEEPAGENT_CODE_DISABLE_CHANNEL_DB = "1" + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + +function idleSleepAssertionPids(): number[] { + const output = execFileSync("pmset", ["-g", "assertions"], { encoding: "utf8" }) + return [...output.matchAll(/pid (\d+)\([^)]*\): \[[^\]]*\] [\d:]+ NoIdleSleepAssertion/g)].map((match) => + Number(match[1]), + ) +} + +async function waitForAssertion(pid: number, expected: boolean, timeoutMs = 15_000) { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + if (idleSleepAssertionPids().includes(pid) === expected) return + await sleep(500) + } + throw new Error(`timed out waiting for NoIdleSleepAssertion of pid ${pid} to become ${expected}`) +} + +async function launch() { + const app = await electron.launch({ args: [main], env, timeout: 30_000 }) + const page = await app.firstWindow({ timeout: 30_000 }) + await page.waitForFunction(() => Boolean((window as { api?: unknown }).api)) + await page.evaluate(() => + (window as unknown as { api: { awaitInitialization(): Promise } }).api.awaitInitialization(), + ) + const pid = app.process().pid + assert.ok(pid, "main process pid") + return { app, page, pid } +} + +const switchSelector = '[data-action="settings-prevent-sleep"] [data-component="switch"]' +const switchInputSelector = `${switchSelector} input[data-slot="switch-input"]` + +async function setPreventSleepViaSettings(page: Page, enabled: boolean) { + await page.keyboard.press("Meta+,") + const control = page.locator(`${switchSelector} [data-slot="switch-control"]`) + await control.waitFor({ state: "visible", timeout: 15_000 }) + // Wait until the switch reflects the opposite state before clicking, so the + // click deterministically lands on the desired state even if the resource is slow. + const before = enabled ? "false" : "true" + await page.waitForFunction( + ({ selector, before }) => document.querySelector(selector)?.getAttribute("aria-checked") === before, + { selector: switchInputSelector, before }, + { timeout: 15_000 }, + ) + await control.click() + await page.keyboard.press("Escape") +} + +// 1. Fresh launch: prevention defaults on. +const first = await launch() +await waitForAssertion(first.pid, true) +console.log("ok 1 - default launch registers NoIdleSleepAssertion") + +// 2. Turning the Settings toggle off releases the assertion without quitting. +await setPreventSleepViaSettings(first.page, false) +await waitForAssertion(first.pid, false) +console.log("ok 2 - disabling the toggle releases the assertion while running") +await first.app.close() + +// 3. Relaunch: the persisted disabled state keeps the assertion off. +const second = await launch() +await sleep(3_000) +assert.ok(!idleSleepAssertionPids().includes(second.pid), "assertion must stay off after relaunch with setting disabled") +console.log("ok 3 - disabled state persists across relaunch") + +// 4. Re-enabling registers the assertion again. +await setPreventSleepViaSettings(second.page, true) +await waitForAssertion(second.pid, true) +console.log("ok 4 - re-enabling the toggle registers the assertion again") +await second.app.close() + +console.log("prevent-sleep smoke passed") diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index 777d3e41..034b9dbb 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -40,7 +40,7 @@ import { createWslServersController } from "./wsl/servers" import { registerWslIpcHandlers } from "./wsl/ipc" import { spawnWslSidecar } from "./wsl/sidecar" import { migrate } from "./migrate" -import { startPowerSaveBlocker, stopPowerSaveBlocker } from "./power" +import { initPowerSaveBlocker, stopPowerSaveBlocker } from "./power" import { createTray, destroyTray } from "./tray" const APP_NAMES: Record = { @@ -303,11 +303,12 @@ const main = Effect.gen(function* () { }) } - // Keep the app running (prevent idle sleep/hibernate) while it is active. The tray icon enables + // Keep the app running (prevent idle sleep/hibernate) while it is active, unless the user opted + // out in settings. The tray icon enables // close-to-tray: closing the window hides it to the tray with a right-click “Quit” to fully exit. // On platforms without tray support (e.g. Linux GNOME default), tray creation fails silently and // close-to-tray stays disabled so closing the window quits normally — avoiding a stranded window. - startPowerSaveBlocker() + initPowerSaveBlocker() const trayCreated = createTray(() => mainWindow) setCloseToTrayEnabled(trayCreated) diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index 9d8b4ce4..51a8792d 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -12,6 +12,7 @@ import { archivePath, copyPath, extractPath, guardFileOpCall, movePath, removePa import { fileLog, isTracked } from "./git" import { getStore } from "./store" import { getPinchZoomEnabled, setPinchZoomEnabled, setTitlebar, updateTitlebar } from "./windows" +import { getPreventSleepEnabled, setPreventSleepEnabled } from "./power" import { browserView } from "./browser-view" import type { UpdaterController } from "./updater-controller" import { createUpdaterSubscriptions } from "./updater-subscriptions" @@ -246,6 +247,13 @@ export function registerIpcHandlers(deps: Deps) { ipcMain.handle("set-pinch-zoom-enabled", (_event: IpcMainInvokeEvent, enabled: boolean) => { setPinchZoomEnabled(enabled) }) + ipcMain.handle("get-prevent-sleep", () => getPreventSleepEnabled()) + ipcMain.handle("set-prevent-sleep", (_event: IpcMainInvokeEvent, enabled: boolean) => { + setPreventSleepEnabled(enabled) + for (const win of BrowserWindow.getAllWindows()) { + win.webContents.send("prevent-sleep-changed", enabled) + } + }) ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => { const win = BrowserWindow.fromWebContents(event.sender) if (!win) return diff --git a/packages/desktop/src/main/power.test.ts b/packages/desktop/src/main/power.test.ts new file mode 100644 index 00000000..16aa5518 --- /dev/null +++ b/packages/desktop/src/main/power.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "bun:test" +import { resolvePreventSleepEnabled } from "./prevent-sleep" + +// Covers the persisted-setting semantics of the sleep-prevention toggle without the Electron +// runtime. The same resolution runs in power.ts on startup and on every settings change. + +describe("resolvePreventSleepEnabled", () => { + test("defaults to enabled when the setting was never stored (historical always-on behavior)", () => { + expect(resolvePreventSleepEnabled(undefined)).toBe(true) + }) + + test("treats any value other than explicit false as enabled", () => { + expect(resolvePreventSleepEnabled(true)).toBe(true) + expect(resolvePreventSleepEnabled(null)).toBe(true) + expect(resolvePreventSleepEnabled("false")).toBe(true) + }) + + test("disables only on explicit false", () => { + expect(resolvePreventSleepEnabled(false)).toBe(false) + }) +}) diff --git a/packages/desktop/src/main/power.ts b/packages/desktop/src/main/power.ts index aa8c6121..9dcde958 100644 --- a/packages/desktop/src/main/power.ts +++ b/packages/desktop/src/main/power.ts @@ -1,9 +1,31 @@ import { powerSaveBlocker } from "electron" +import { resolvePreventSleepEnabled } from "./prevent-sleep" +import { getStore } from "./store" +import { PREVENT_SLEEP_KEY } from "./store-keys" + // prevent-app-suspension keeps the system/app from idling to sleep while allowing the display to // turn off (go dark) and the lid to close — i.e. it blocks idle sleep & hibernate, not screen blank. let blockerId: number | null = null +export function getPreventSleepEnabled(): boolean { + return resolvePreventSleepEnabled(getStore().get(PREVENT_SLEEP_KEY)) +} + +export function setPreventSleepEnabled(enabled: boolean): void { + getStore().set(PREVENT_SLEEP_KEY, enabled) + if (enabled) { + startPowerSaveBlocker() + return + } + stopPowerSaveBlocker() +} + +export function initPowerSaveBlocker(): void { + if (!getPreventSleepEnabled()) return + startPowerSaveBlocker() +} + export function startPowerSaveBlocker(): void { if (blockerId !== null) return blockerId = powerSaveBlocker.start("prevent-app-suspension") diff --git a/packages/desktop/src/main/prevent-sleep.ts b/packages/desktop/src/main/prevent-sleep.ts new file mode 100644 index 00000000..d90a830d --- /dev/null +++ b/packages/desktop/src/main/prevent-sleep.ts @@ -0,0 +1,6 @@ +// Sleep prevention predates the settings toggle, so an unset key must keep the historical +// always-on behavior; only an explicit `false` opts out. Kept Electron-free so the decision +// can be unit-tested without the runtime (same split as close-to-tray.ts). +export function resolvePreventSleepEnabled(stored: unknown): boolean { + return stored !== false +} diff --git a/packages/desktop/src/main/store-keys.ts b/packages/desktop/src/main/store-keys.ts index beb18b8b..85ae29c0 100644 --- a/packages/desktop/src/main/store-keys.ts +++ b/packages/desktop/src/main/store-keys.ts @@ -2,3 +2,4 @@ export const SETTINGS_STORE = "deepagent-code.settings" export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl" export const WSL_SERVERS_KEY = "wslServers" export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled" +export const PREVENT_SLEEP_KEY = "preventSleep" diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts index 1c83db5a..108f4953 100644 --- a/packages/desktop/src/preload/index.ts +++ b/packages/desktop/src/preload/index.ts @@ -122,6 +122,13 @@ const api: ElectronAPI = { ipcRenderer.on("pinch-zoom-enabled-changed", handler) return () => ipcRenderer.removeListener("pinch-zoom-enabled-changed", handler) }, + getPreventSleepEnabled: () => ipcRenderer.invoke("get-prevent-sleep"), + setPreventSleepEnabled: (enabled) => ipcRenderer.invoke("set-prevent-sleep", enabled), + onPreventSleepChanged: (cb) => { + const handler = (_: unknown, enabled: boolean) => cb(enabled) + ipcRenderer.on("prevent-sleep-changed", handler) + return () => ipcRenderer.removeListener("prevent-sleep-changed", handler) + }, onZoomFactorChanged: (cb) => { const handler = (_: unknown, factor: number) => cb(factor) ipcRenderer.on("zoom-factor-changed", handler) diff --git a/packages/desktop/src/preload/types.ts b/packages/desktop/src/preload/types.ts index 3d882a6b..db0b741c 100644 --- a/packages/desktop/src/preload/types.ts +++ b/packages/desktop/src/preload/types.ts @@ -123,6 +123,9 @@ export type ElectronAPI = { getPinchZoomEnabled: () => Promise setPinchZoomEnabled: (enabled: boolean) => Promise onPinchZoomEnabledChanged: (cb: (enabled: boolean) => void) => () => void + getPreventSleepEnabled: () => Promise + setPreventSleepEnabled: (enabled: boolean) => Promise + onPreventSleepChanged: (cb: (enabled: boolean) => void) => () => void onZoomFactorChanged: (cb: (factor: number) => void) => () => void setTitlebar: (theme: TitlebarTheme) => Promise runDesktopMenuAction: (action: DesktopMenuAction) => Promise diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index 33b95d1d..9f4b56cc 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -265,6 +265,10 @@ const createPlatform = (): Platform => { setPinchZoomEnabled, + getPreventSleepEnabled: () => window.api.getPreventSleepEnabled(), + + setPreventSleepEnabled: (enabled: boolean) => window.api.setPreventSleepEnabled(enabled), + runDesktopMenuAction, checkAppExists: async (appName: string) => { From f6bc1e55de8b9885a78dc51adc9da6aa549855d2 Mon Sep 17 00:00:00 2001 From: thomas-yanga Date: Sun, 26 Jul 2026 18:54:46 +0800 Subject: [PATCH 2/2] fix(desktop): route prevent-sleep writes through the side-effecting IPC The generic store-set/store-delete/store-clear IPCs could write the preventSleep key in the settings store without starting/stopping the powerSaveBlocker, desyncing the persisted setting from the running blocker (a string "false" even resolved back to enabled on next launch). - store-set: reject writes to the preventSleep key; the dedicated set-prevent-sleep IPC is now the single write path - store-delete/store-clear: after touching the settings store, re-sync the blocker via syncPowerSaveBlocker() so the reset to the default-on behavior takes effect immediately Verified with the macOS prevent-sleep smoke (4/4) plus a Playwright guard check: rejected store-set leaves the assertion running, and store-delete/store-clear restore the NoIdleSleepAssertion. Signed-off-by: thomas-yanga --- packages/desktop/src/main/ipc.ts | 11 ++++++++++- packages/desktop/src/main/power.ts | 8 +++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index 51a8792d..d3fca7f3 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -12,7 +12,8 @@ import { archivePath, copyPath, extractPath, guardFileOpCall, movePath, removePa import { fileLog, isTracked } from "./git" import { getStore } from "./store" import { getPinchZoomEnabled, setPinchZoomEnabled, setTitlebar, updateTitlebar } from "./windows" -import { getPreventSleepEnabled, setPreventSleepEnabled } from "./power" +import { getPreventSleepEnabled, setPreventSleepEnabled, syncPowerSaveBlocker } from "./power" +import { PREVENT_SLEEP_KEY, SETTINGS_STORE } from "./store-keys" import { browserView } from "./browser-view" import type { UpdaterController } from "./updater-controller" import { createUpdaterSubscriptions } from "./updater-subscriptions" @@ -93,13 +94,21 @@ export function registerIpcHandlers(deps: Deps) { } }) ipcMain.handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => { + // preventSleep drives a live powerSaveBlocker side effect; routing writes through the + // dedicated set-prevent-sleep IPC keeps the blocker in sync with the persisted value. + if (name === SETTINGS_STORE && key === PREVENT_SLEEP_KEY) { + throw new Error(`"${PREVENT_SLEEP_KEY}" must be written via the set-prevent-sleep IPC`) + } getStore(name).set(key, value) }) ipcMain.handle("store-delete", (_event: IpcMainInvokeEvent, name: string, key: string) => { getStore(name).delete(key) + // Deleting the key resets it to the default-on behavior; re-sync so the blocker matches. + if (name === SETTINGS_STORE && key === PREVENT_SLEEP_KEY) syncPowerSaveBlocker() }) ipcMain.handle("store-clear", (_event: IpcMainInvokeEvent, name: string) => { getStore(name).clear() + if (name === SETTINGS_STORE) syncPowerSaveBlocker() }) ipcMain.handle("store-keys", (_event: IpcMainInvokeEvent, name: string) => { const store = getStore(name) diff --git a/packages/desktop/src/main/power.ts b/packages/desktop/src/main/power.ts index 9dcde958..bd7c2db7 100644 --- a/packages/desktop/src/main/power.ts +++ b/packages/desktop/src/main/power.ts @@ -14,7 +14,13 @@ export function getPreventSleepEnabled(): boolean { export function setPreventSleepEnabled(enabled: boolean): void { getStore().set(PREVENT_SLEEP_KEY, enabled) - if (enabled) { + syncPowerSaveBlocker() +} + +// Reconciles the running blocker with the persisted setting. Must be called after any write path +// that can change the stored value outside setPreventSleepEnabled (generic store delete/clear). +export function syncPowerSaveBlocker(): void { + if (getPreventSleepEnabled()) { startPowerSaveBlocker() return }