mirror of
https://github.com/openai/codex.git
synced 2026-05-05 22:01:37 +03:00
feat(tui): prevent macOS idle sleep while turns run (#11711)
## Summary - add a shared `codex-core` sleep inhibitor that uses native macOS IOKit assertions (`IOPMAssertionCreateWithName` / `IOPMAssertionRelease`) instead of spawning `caffeinate` - wire sleep inhibition to turn lifecycle in `tui` (`TurnStarted` enables; `TurnComplete` and abort/error finalization disable) - gate this behavior behind a `/experimental` feature toggle (`[features].prevent_idle_sleep`) instead of a dedicated `[tui]` config flag - expose the toggle in `/experimental` on macOS; keep it under development on other platforms - keep behavior no-op on non-macOS targets <img width="1326" height="577" alt="image" src="https://github.com/user-attachments/assets/73fac06b-97ae-46a2-800a-30f9516cf8a3" /> ## Testing - `cargo check -p codex-core -p codex-tui` - `cargo test -p codex-core sleep_inhibitor::tests -- --nocapture` - `cargo test -p codex-core tui_config_missing_notifications_field_defaults_to_enabled -- --nocapture` - `cargo test -p codex-core prevent_idle_sleep_is_ -- --nocapture` ## Semantics and API references - This PR targets `caffeinate -i` semantics: prevent *idle system sleep* while allowing display idle sleep. - `caffeinate -i` mapping in Apple open source (`assertionMap`): - `kIdleAssertionFlag -> kIOPMAssertionTypePreventUserIdleSystemSleep` - Source: https://github.com/apple-oss-distributions/PowerManagement/blob/PowerManagement-1846.60.12/caffeinate/caffeinate.c#L52-L54 - Apple IOKit docs for assertion types and API: - https://developer.apple.com/documentation/iokit/iopmlib_h/iopmassertiontypes - https://developer.apple.com/documentation/iokit/1557092-iopmassertioncreatewithname - https://developer.apple.com/library/archive/qa/qa1340/_index.html ## Codex Electron vs this PR (full stack path) - Codex Electron app requests sleep blocking with `powerSaveBlocker.start("prevent-app-suspension")`: - https://github.com/openai/codex/blob/main/codex/codex-vscode/electron/src/electron-message-handler.ts - Electron maps that string to Chromium wake lock type `kPreventAppSuspension`: - https://github.com/electron/electron/blob/main/shell/browser/api/electron_api_power_save_blocker.cc - Chromium macOS backend maps wake lock types to IOKit assertion constants and calls IOKit: - `kPreventAppSuspension -> kIOPMAssertionTypeNoIdleSleep` - `kPreventDisplaySleep / kPreventDisplaySleepAllowDimming -> kIOPMAssertionTypeNoDisplaySleep` - https://github.com/chromium/chromium/blob/main/services/device/wake_lock/power_save_blocker/power_save_blocker_mac.cc ## Why this PR uses a different macOS constant name - This PR uses `"PreventUserIdleSystemSleep"` directly, via `IOPMAssertionCreateWithName`, in `codex-rs/core/src/sleep_inhibitor.rs`. - Apple’s IOKit header documents `kIOPMAssertionTypeNoIdleSleep` as deprecated and recommends `kIOPMAssertPreventUserIdleSystemSleep` / `kIOPMAssertionTypePreventUserIdleSystemSleep`: - https://github.com/apple-oss-distributions/IOKitUser/blob/IOKitUser-100222.60.2/pwr_mgt.subproj/IOPMLib.h#L1000-L1030 - So Chromium and this PR are using different constant names, but semantically equivalent idle-system-sleep prevention behavior. ## Future platform support The architecture is intentionally set up for multi-platform extensions: - UI code (`tui`) only calls `SleepInhibitor::set_turn_running(...)` on turn lifecycle boundaries. - Platform-specific behavior is isolated in `codex-rs/core/src/sleep_inhibitor.rs` behind `cfg(...)` blocks. - Feature exposure is centralized in `core/src/features.rs` and surfaced via `/experimental`. - Adding new OS backends should not require additional TUI wiring; only the backend internals and feature stage metadata need to change. Potential follow-up implementations: - Windows: - Add a backend using Win32 power APIs (`SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)` as baseline). - Optionally move to `PowerCreateRequest` / `PowerSetRequest` / `PowerClearRequest` for richer assertion semantics. - Linux: - Add a backend using logind inhibitors over D-Bus (`org.freedesktop.login1.Manager.Inhibit` with `what="sleep"`). - Keep a no-op fallback where logind/D-Bus is unavailable. This PR keeps the cross-platform API surface minimal so future PRs can add Windows/Linux support incrementally with low churn. --------- Co-authored-by: jif-oai <jif@openai.com>
This commit is contained in:
committed by
GitHub
parent
851fcc377b
commit
32da5eb358
@@ -131,6 +131,7 @@ use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_protocol::request_user_input::RequestUserInputEvent;
|
||||
use codex_protocol::user_input::TextElement;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_sleep_inhibitor::SleepInhibitor;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyEventKind;
|
||||
@@ -520,6 +521,7 @@ pub(crate) struct ChatWidget {
|
||||
skills_initial_state: Option<HashMap<PathBuf, bool>>,
|
||||
last_unified_wait: Option<UnifiedExecWaitState>,
|
||||
unified_exec_wait_streak: Option<UnifiedExecWaitStreak>,
|
||||
turn_sleep_inhibitor: SleepInhibitor,
|
||||
task_complete_pending: bool,
|
||||
unified_exec_processes: Vec<UnifiedExecProcessSummary>,
|
||||
/// Tracks whether codex-core currently considers an agent turn to be in progress.
|
||||
@@ -1275,6 +1277,7 @@ impl ChatWidget {
|
||||
|
||||
fn on_task_started(&mut self) {
|
||||
self.agent_turn_running = true;
|
||||
self.turn_sleep_inhibitor.set_turn_running(true);
|
||||
self.saw_plan_update_this_turn = false;
|
||||
self.saw_plan_item_this_turn = false;
|
||||
self.plan_delta_buffer.clear();
|
||||
@@ -1332,6 +1335,7 @@ impl ChatWidget {
|
||||
// Mark task stopped and request redraw now that all content is in history.
|
||||
self.pending_status_indicator_restore = false;
|
||||
self.agent_turn_running = false;
|
||||
self.turn_sleep_inhibitor.set_turn_running(false);
|
||||
self.update_task_running_state();
|
||||
self.running_commands.clear();
|
||||
self.suppressed_exec_calls.clear();
|
||||
@@ -1568,6 +1572,7 @@ impl ChatWidget {
|
||||
self.finalize_active_cell_as_failed();
|
||||
// Reset running state and clear streaming buffers.
|
||||
self.agent_turn_running = false;
|
||||
self.turn_sleep_inhibitor.set_turn_running(false);
|
||||
self.update_task_running_state();
|
||||
self.running_commands.clear();
|
||||
self.suppressed_exec_calls.clear();
|
||||
@@ -2544,6 +2549,7 @@ impl ChatWidget {
|
||||
let model = model.filter(|m| !m.trim().is_empty());
|
||||
let mut config = config;
|
||||
config.model = model.clone();
|
||||
let prevent_idle_sleep = config.features.enabled(Feature::PreventIdleSleep);
|
||||
let mut rng = rand::rng();
|
||||
let placeholder = PLACEHOLDERS[rng.random_range(0..PLACEHOLDERS.len())].to_string();
|
||||
let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), thread_manager);
|
||||
@@ -2611,6 +2617,7 @@ impl ChatWidget {
|
||||
suppressed_exec_calls: HashSet::new(),
|
||||
last_unified_wait: None,
|
||||
unified_exec_wait_streak: None,
|
||||
turn_sleep_inhibitor: SleepInhibitor::new(prevent_idle_sleep),
|
||||
task_complete_pending: false,
|
||||
unified_exec_processes: Vec::new(),
|
||||
agent_turn_running: false,
|
||||
@@ -2710,6 +2717,7 @@ impl ChatWidget {
|
||||
let model = model.filter(|m| !m.trim().is_empty());
|
||||
let mut config = config;
|
||||
config.model = model.clone();
|
||||
let prevent_idle_sleep = config.features.enabled(Feature::PreventIdleSleep);
|
||||
let mut rng = rand::rng();
|
||||
let placeholder = PLACEHOLDERS[rng.random_range(0..PLACEHOLDERS.len())].to_string();
|
||||
|
||||
@@ -2776,6 +2784,7 @@ impl ChatWidget {
|
||||
suppressed_exec_calls: HashSet::new(),
|
||||
last_unified_wait: None,
|
||||
unified_exec_wait_streak: None,
|
||||
turn_sleep_inhibitor: SleepInhibitor::new(prevent_idle_sleep),
|
||||
task_complete_pending: false,
|
||||
unified_exec_processes: Vec::new(),
|
||||
agent_turn_running: false,
|
||||
@@ -2862,6 +2871,7 @@ impl ChatWidget {
|
||||
otel_manager,
|
||||
} = common;
|
||||
let model = model.filter(|m| !m.trim().is_empty());
|
||||
let prevent_idle_sleep = config.features.enabled(Feature::PreventIdleSleep);
|
||||
let mut rng = rand::rng();
|
||||
let placeholder = PLACEHOLDERS[rng.random_range(0..PLACEHOLDERS.len())].to_string();
|
||||
|
||||
@@ -2930,6 +2940,7 @@ impl ChatWidget {
|
||||
suppressed_exec_calls: HashSet::new(),
|
||||
last_unified_wait: None,
|
||||
unified_exec_wait_streak: None,
|
||||
turn_sleep_inhibitor: SleepInhibitor::new(prevent_idle_sleep),
|
||||
task_complete_pending: false,
|
||||
unified_exec_processes: Vec::new(),
|
||||
agent_turn_running: false,
|
||||
@@ -5950,6 +5961,11 @@ impl ChatWidget {
|
||||
if feature == Feature::Personality {
|
||||
self.sync_personality_command_enabled();
|
||||
}
|
||||
if feature == Feature::PreventIdleSleep {
|
||||
self.turn_sleep_inhibitor = SleepInhibitor::new(enabled);
|
||||
self.turn_sleep_inhibitor
|
||||
.set_turn_running(self.agent_turn_running);
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
if matches!(
|
||||
feature,
|
||||
|
||||
Reference in New Issue
Block a user