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 (
<>
>
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..d3fca7f3 100644
--- a/packages/desktop/src/main/ipc.ts
+++ b/packages/desktop/src/main/ipc.ts
@@ -12,6 +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, 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"
@@ -92,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)
@@ -246,6 +256,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..bd7c2db7 100644
--- a/packages/desktop/src/main/power.ts
+++ b/packages/desktop/src/main/power.ts
@@ -1,9 +1,37 @@
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)
+ 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
+ }
+ 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) => {