feat: added macOS power assertion module to prevent idle-sleep during sessions
- Added MacOSPowerAssertion native module to prevent idle-sleep on macOS during active sessions. - Integrated power assertion lifecycle management into coding-agent session with automatic start/stop. - Implemented N-API bindings for macOS IOKit power assertions with RAII cleanup and cross-platform no-op fallback. - Reformatted indentation from spaces to tabs across Rust chunk module for consistency.
This commit is contained in:
@@ -47,104 +47,103 @@ fn recurse_nix_attrset(node: Node<'_>) -> Option<RecurseSpec<'_>> {
|
||||
}
|
||||
|
||||
fn recurse_nix_binding_value(node: Node<'_>) -> Option<RecurseSpec<'_>> {
|
||||
let expression = node.child_by_field_name("expression")?;
|
||||
if matches!(
|
||||
expression.kind(),
|
||||
"attrset_expression" | "let_attrset_expression" | "rec_attrset_expression"
|
||||
) {
|
||||
return recurse_nix_attrset(expression);
|
||||
}
|
||||
recurse_value_container(node)
|
||||
let expression = node.child_by_field_name("expression")?;
|
||||
if matches!(
|
||||
expression.kind(),
|
||||
"attrset_expression" | "let_attrset_expression" | "rec_attrset_expression"
|
||||
) {
|
||||
return recurse_nix_attrset(expression);
|
||||
}
|
||||
recurse_value_container(node)
|
||||
}
|
||||
|
||||
fn classify_nix_binding<'t>(node: Node<'t>, source: &str) -> RawChunkCandidate<'t> {
|
||||
let name = extract_nix_binding_name(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
let chunk_name = format!("attr_{name}");
|
||||
let expression = node.child_by_field_name("expression");
|
||||
if let Some(expression) = expression
|
||||
&& matches!(
|
||||
expression.kind(),
|
||||
"attrset_expression" | "let_attrset_expression" | "rec_attrset_expression"
|
||||
)
|
||||
{
|
||||
return make_container_chunk(node, chunk_name, source, recurse_nix_attrset(expression));
|
||||
}
|
||||
make_named_chunk(node, chunk_name, source, recurse_nix_binding_value(node))
|
||||
let name = extract_nix_binding_name(node, source).unwrap_or_else(|| "anonymous".to_string());
|
||||
let chunk_name = format!("attr_{name}");
|
||||
let expression = node.child_by_field_name("expression");
|
||||
if let Some(expression) = expression
|
||||
&& matches!(
|
||||
expression.kind(),
|
||||
"attrset_expression" | "let_attrset_expression" | "rec_attrset_expression"
|
||||
) {
|
||||
return make_container_chunk(node, chunk_name, source, recurse_nix_attrset(expression));
|
||||
}
|
||||
make_named_chunk(node, chunk_name, source, recurse_nix_binding_value(node))
|
||||
}
|
||||
|
||||
impl LangClassifier for NixHclClassifier {
|
||||
fn classify_root<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// Nix top-level attrsets should recurse into their binding_set so the file exposes
|
||||
// structural attr chunks instead of a single opaque attrset_expr leaf.
|
||||
"attrset_expression" | "let_attrset_expression" | "rec_attrset_expression" => {
|
||||
Some(make_container_chunk(
|
||||
node,
|
||||
sanitize_node_kind(node.kind()),
|
||||
source,
|
||||
recurse_nix_attrset(node),
|
||||
))
|
||||
},
|
||||
// Older tree-sitter-nix revisions used `attribute`; current grammars expose `binding`.
|
||||
"attribute" | "binding" => Some(classify_nix_binding(node, source)),
|
||||
// HCL top-level block, or diff hunk fallback
|
||||
"block" => {
|
||||
if let Some(name) = extract_hcl_block_name(node, source) {
|
||||
Some(make_container_chunk(
|
||||
node,
|
||||
format!("block_{name}"),
|
||||
source,
|
||||
recurse_into(node, ChunkContext::ClassBody, &[], &["body"]),
|
||||
))
|
||||
} else {
|
||||
Some(group_candidate(node, "hunks", source))
|
||||
}
|
||||
},
|
||||
// Nix expressions
|
||||
"function_expression" | "let_expression" => {
|
||||
Some(named_candidate(node, "expr", source, recurse_value_container(node)))
|
||||
},
|
||||
// Nix inherit
|
||||
"inherit" => Some(group_candidate(node, "imports", source)),
|
||||
// Variable/assignment declarations
|
||||
"variable_declaration" | "assignment" => Some(group_candidate(node, "decls", source)),
|
||||
// HCL top-level block types
|
||||
"provider" | "resource" | "data" | "locals" | "variable" | "output" | "module" => {
|
||||
Some(container_candidate(
|
||||
node,
|
||||
sanitize_node_kind(node.kind()).as_str(),
|
||||
source,
|
||||
recurse_into(node, ChunkContext::ClassBody, &[], &["body"]),
|
||||
))
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
match node.kind() {
|
||||
// Nix top-level attrsets should recurse into their binding_set so the file exposes
|
||||
// structural attr chunks instead of a single opaque attrset_expr leaf.
|
||||
"attrset_expression" | "let_attrset_expression" | "rec_attrset_expression" => {
|
||||
Some(make_container_chunk(
|
||||
node,
|
||||
sanitize_node_kind(node.kind()),
|
||||
source,
|
||||
recurse_nix_attrset(node),
|
||||
))
|
||||
},
|
||||
// Older tree-sitter-nix revisions used `attribute`; current grammars expose `binding`.
|
||||
"attribute" | "binding" => Some(classify_nix_binding(node, source)),
|
||||
// HCL top-level block, or diff hunk fallback
|
||||
"block" => {
|
||||
if let Some(name) = extract_hcl_block_name(node, source) {
|
||||
Some(make_container_chunk(
|
||||
node,
|
||||
format!("block_{name}"),
|
||||
source,
|
||||
recurse_into(node, ChunkContext::ClassBody, &[], &["body"]),
|
||||
))
|
||||
} else {
|
||||
Some(group_candidate(node, "hunks", source))
|
||||
}
|
||||
},
|
||||
// Nix expressions
|
||||
"function_expression" | "let_expression" => {
|
||||
Some(named_candidate(node, "expr", source, recurse_value_container(node)))
|
||||
},
|
||||
// Nix inherit
|
||||
"inherit" => Some(group_candidate(node, "imports", source)),
|
||||
// Variable/assignment declarations
|
||||
"variable_declaration" | "assignment" => Some(group_candidate(node, "decls", source)),
|
||||
// HCL top-level block types
|
||||
"provider" | "resource" | "data" | "locals" | "variable" | "output" | "module" => {
|
||||
Some(container_candidate(
|
||||
node,
|
||||
sanitize_node_kind(node.kind()).as_str(),
|
||||
source,
|
||||
recurse_into(node, ChunkContext::ClassBody, &[], &["body"]),
|
||||
))
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_class<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
match node.kind() {
|
||||
// Nested HCL block — only promote if it has an identifiable block name
|
||||
"block" => extract_hcl_block_name(node, source).map(|name| {
|
||||
make_container_chunk(
|
||||
node,
|
||||
format!("block_{name}"),
|
||||
source,
|
||||
recurse_into(node, ChunkContext::ClassBody, &[], &["body"]),
|
||||
)
|
||||
}),
|
||||
// Nested Nix attrset values recurse into their binding_set just like top-level ones.
|
||||
"attrset_expression" | "let_attrset_expression" | "rec_attrset_expression" => {
|
||||
Some(make_container_chunk(
|
||||
node,
|
||||
sanitize_node_kind(node.kind()),
|
||||
source,
|
||||
recurse_nix_attrset(node),
|
||||
))
|
||||
},
|
||||
// Nested Nix binding
|
||||
"binding" => Some(classify_nix_binding(node, source)),
|
||||
_ => None,
|
||||
}
|
||||
match node.kind() {
|
||||
// Nested HCL block — only promote if it has an identifiable block name
|
||||
"block" => extract_hcl_block_name(node, source).map(|name| {
|
||||
make_container_chunk(
|
||||
node,
|
||||
format!("block_{name}"),
|
||||
source,
|
||||
recurse_into(node, ChunkContext::ClassBody, &[], &["body"]),
|
||||
)
|
||||
}),
|
||||
// Nested Nix attrset values recurse into their binding_set just like top-level ones.
|
||||
"attrset_expression" | "let_attrset_expression" | "rec_attrset_expression" => {
|
||||
Some(make_container_chunk(
|
||||
node,
|
||||
sanitize_node_kind(node.kind()),
|
||||
source,
|
||||
recurse_nix_attrset(node),
|
||||
))
|
||||
},
|
||||
// Nested Nix binding
|
||||
"binding" => Some(classify_nix_binding(node, source)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_function<'t>(&self, node: Node<'t>, source: &str) -> Option<RawChunkCandidate<'t>> {
|
||||
|
||||
@@ -1384,31 +1384,37 @@ impl Config {
|
||||
|
||||
#[test]
|
||||
fn nix_chunk_tree_exposes_attr_bindings() {
|
||||
let source = r#"{
|
||||
let source = r#"{
|
||||
hello = "world";
|
||||
nested = {
|
||||
value = 1;
|
||||
};
|
||||
}
|
||||
"#;
|
||||
let tree = build_chunk_tree(source, "nix").expect("tree should build");
|
||||
let attrset = tree
|
||||
.chunks
|
||||
.iter()
|
||||
.find(|chunk| chunk.path == "attrset_expr")
|
||||
.expect("attrset_expr chunk");
|
||||
assert!(!tree.fallback, "nix should use tree-sitter chunking");
|
||||
assert!(!attrset.leaf, "top-level attrset should recurse into bindings");
|
||||
assert!(
|
||||
attrset.children.iter().any(|child| child == "attrset_expr.attr_hello"),
|
||||
"expected attr_hello child, got {:?}",
|
||||
attrset.children
|
||||
);
|
||||
assert!(
|
||||
attrset.children.iter().any(|child| child == "attrset_expr.attr_nested"),
|
||||
"expected attr_nested child, got {:?}",
|
||||
attrset.children
|
||||
);
|
||||
let tree = build_chunk_tree(source, "nix").expect("tree should build");
|
||||
let attrset = tree
|
||||
.chunks
|
||||
.iter()
|
||||
.find(|chunk| chunk.path == "attrset_expr")
|
||||
.expect("attrset_expr chunk");
|
||||
assert!(!tree.fallback, "nix should use tree-sitter chunking");
|
||||
assert!(!attrset.leaf, "top-level attrset should recurse into bindings");
|
||||
assert!(
|
||||
attrset
|
||||
.children
|
||||
.iter()
|
||||
.any(|child| child == "attrset_expr.attr_hello"),
|
||||
"expected attr_hello child, got {:?}",
|
||||
attrset.children
|
||||
);
|
||||
assert!(
|
||||
attrset
|
||||
.children
|
||||
.iter()
|
||||
.any(|child| child == "attrset_expr.attr_nested"),
|
||||
"expected attr_nested child, got {:?}",
|
||||
attrset.children
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -40,6 +40,9 @@ pub mod image;
|
||||
pub mod indent;
|
||||
pub mod keys;
|
||||
pub mod language;
|
||||
|
||||
pub mod power;
|
||||
|
||||
pub mod prof;
|
||||
pub mod projfs_overlay;
|
||||
pub mod ps;
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
//! macOS power assertions for preventing idle sleep.
|
||||
//!
|
||||
//! Exposes a small N-API handle that acquires a macOS `IOKit` power assertion
|
||||
//! on construction and releases it on `stop()`/drop. On non-macOS platforms the
|
||||
//! handle is a no-op so higher layers can use one code path.
|
||||
|
||||
use napi_derive::napi;
|
||||
|
||||
/// Options for starting a macOS power assertion.
|
||||
#[napi(object, js_name = "MacOSPowerAssertionOptions")]
|
||||
pub struct MacOSPowerAssertionOptions {
|
||||
/// Human-readable reason shown in macOS power diagnostics.
|
||||
pub reason: Option<String>,
|
||||
/// Keep the display awake in addition to preventing idle system sleep.
|
||||
pub display: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod platform {
|
||||
use std::{
|
||||
ffi::{CString, c_char, c_void},
|
||||
ptr,
|
||||
};
|
||||
|
||||
use napi::{Error, Result};
|
||||
|
||||
const UTF8_ENCODING: u32 = 0x0800_0100;
|
||||
const ASSERTION_LEVEL_ON: u32 = 255;
|
||||
const ASSERTION_ID_NONE: u32 = 0;
|
||||
const PREVENT_USER_IDLE_SYSTEM_SLEEP: &str = "PreventUserIdleSystemSleep";
|
||||
const PREVENT_USER_IDLE_DISPLAY_SLEEP: &str = "PreventUserIdleDisplaySleep";
|
||||
|
||||
type CFStringRef = *const c_void;
|
||||
type CFTypeRef = *const c_void;
|
||||
type IOPMAssertionID = u32;
|
||||
type IOPMAssertionLevel = u32;
|
||||
type IOReturn = i32;
|
||||
|
||||
#[link(name = "CoreFoundation", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
fn CFStringCreateWithCString(
|
||||
alloc: *const c_void,
|
||||
c_str: *const c_char,
|
||||
encoding: u32,
|
||||
) -> CFStringRef;
|
||||
fn CFRelease(value: CFTypeRef);
|
||||
}
|
||||
|
||||
#[link(name = "IOKit", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
fn IOPMAssertionCreateWithName(
|
||||
assertion_type: CFStringRef,
|
||||
assertion_level: IOPMAssertionLevel,
|
||||
assertion_name: CFStringRef,
|
||||
assertion_id: *mut IOPMAssertionID,
|
||||
) -> IOReturn;
|
||||
fn IOPMAssertionRelease(assertion_id: IOPMAssertionID) -> IOReturn;
|
||||
}
|
||||
|
||||
struct CfString(CFStringRef);
|
||||
|
||||
impl CfString {
|
||||
fn new(value: &str) -> Result<Self> {
|
||||
let c_string = CString::new(value).map_err(|_| {
|
||||
Error::from_reason("Power assertion strings must not contain NUL bytes")
|
||||
})?;
|
||||
// SAFETY: `c_string` is a valid, NUL-terminated UTF-8 byte sequence for the
|
||||
// duration of the call, and CoreFoundation copies the contents into a new
|
||||
// `CFString` when creation succeeds.
|
||||
let string_ref =
|
||||
unsafe { CFStringCreateWithCString(ptr::null(), c_string.as_ptr(), UTF8_ENCODING) };
|
||||
if string_ref.is_null() {
|
||||
return Err(Error::from_reason(
|
||||
"Failed to allocate CoreFoundation string for power assertion",
|
||||
));
|
||||
}
|
||||
Ok(Self(string_ref))
|
||||
}
|
||||
|
||||
const fn as_ptr(&self) -> CFStringRef {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CfString {
|
||||
fn drop(&mut self) {
|
||||
if self.0.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: `self.0` was returned by `CFStringCreateWithCString` in
|
||||
// `CfString::new` and this wrapper owns the single outstanding reference, so
|
||||
// releasing it here balances creation exactly once.
|
||||
unsafe { CFRelease(self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AssertionInner {
|
||||
assertion_id: IOPMAssertionID,
|
||||
}
|
||||
|
||||
impl AssertionInner {
|
||||
pub fn start(reason: &str, display: bool) -> Result<Self> {
|
||||
let assertion_type = CfString::new(if display {
|
||||
PREVENT_USER_IDLE_DISPLAY_SLEEP
|
||||
} else {
|
||||
PREVENT_USER_IDLE_SYSTEM_SLEEP
|
||||
})?;
|
||||
let assertion_reason = CfString::new(reason)?;
|
||||
let mut assertion_id = ASSERTION_ID_NONE;
|
||||
// SAFETY: both `CFStringRef` values are valid live CoreFoundation strings owned
|
||||
// by this stack frame, `ASSERTION_LEVEL_ON` is the documented enabled value,
|
||||
// and `assertion_id` points to writable storage for the returned identifier.
|
||||
let status = unsafe {
|
||||
IOPMAssertionCreateWithName(
|
||||
assertion_type.as_ptr(),
|
||||
ASSERTION_LEVEL_ON,
|
||||
assertion_reason.as_ptr(),
|
||||
&raw mut assertion_id,
|
||||
)
|
||||
};
|
||||
if status != 0 {
|
||||
return Err(Error::from_reason(format!(
|
||||
"Failed to acquire macOS power assertion (IOReturn={status})"
|
||||
)));
|
||||
}
|
||||
Ok(Self { assertion_id })
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) -> Result<()> {
|
||||
if self.assertion_id == ASSERTION_ID_NONE {
|
||||
return Ok(());
|
||||
}
|
||||
let assertion_id = self.assertion_id;
|
||||
self.assertion_id = ASSERTION_ID_NONE;
|
||||
// SAFETY: `assertion_id` came from a successful `IOPMAssertionCreateWithName`
|
||||
// call owned by this handle, and we clear local ownership before releasing so
|
||||
// the same assertion cannot be released twice.
|
||||
let status = unsafe { IOPMAssertionRelease(assertion_id) };
|
||||
if status != 0 {
|
||||
return Err(Error::from_reason(format!(
|
||||
"Failed to release macOS power assertion (IOReturn={status})"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AssertionInner {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Long-lived macOS power assertion.
|
||||
///
|
||||
/// On macOS this acquires an `IOKit` assertion that prevents idle sleep until
|
||||
/// the handle is stopped or dropped. On other platforms it is a no-op handle so
|
||||
/// the caller can keep one cross-platform code path.
|
||||
#[napi(js_name = "MacOSPowerAssertion")]
|
||||
pub struct MacOSPowerAssertion {
|
||||
#[cfg(target_os = "macos")]
|
||||
inner: Option<platform::AssertionInner>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl MacOSPowerAssertion {
|
||||
/// Acquire a macOS power assertion.
|
||||
#[napi(factory)]
|
||||
pub fn start(options: Option<MacOSPowerAssertionOptions>) -> napi::Result<Self> {
|
||||
let reason = options
|
||||
.as_ref()
|
||||
.and_then(|value| value.reason.as_deref())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or("Oh My Pi agent session");
|
||||
let display = options
|
||||
.as_ref()
|
||||
.and_then(|value| value.display)
|
||||
.unwrap_or(false);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Ok(Self { inner: Some(platform::AssertionInner::start(reason, display)?) })
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let _ = reason;
|
||||
let _ = display;
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
/// Release the power assertion early.
|
||||
#[napi]
|
||||
pub fn stop(&mut self) -> napi::Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(inner) = &mut self.inner {
|
||||
inner.stop()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Agent sessions now hold a macOS power assertion for their lifetime so the host stays awake during active coding-agent runs
|
||||
- Updated chunk edit prompt documentation to use ellipsis (…) instead of ellipsis (...) for consistency in operation examples
|
||||
- Modified chunk path parsing to preserve raw selector strings and extract CRC separately, enabling accurate chunk reference round-tripping in read/edit workflows
|
||||
- Changed chunk edit behavior to auto-accept stale CRC checksums for subsequent operations on the same chunk within a batch, improving usability when applying multiple edits to the same target
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
modelsAreEqual,
|
||||
parseRateLimitReason,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import type { SearchDb } from "@oh-my-pi/pi-natives";
|
||||
import { MacOSPowerAssertion, type SearchDb } from "@oh-my-pi/pi-natives";
|
||||
import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
||||
import type { AsyncJob, AsyncJobManager } from "../async";
|
||||
import type { Rule } from "../capability/rule";
|
||||
@@ -396,6 +396,9 @@ export class AgentSession {
|
||||
readonly sessionManager: SessionManager;
|
||||
readonly settings: Settings;
|
||||
readonly searchDb: SearchDb | undefined;
|
||||
|
||||
#powerAssertion: MacOSPowerAssertion | undefined;
|
||||
|
||||
readonly configWarnings: string[] = [];
|
||||
|
||||
#asyncJobManager: AsyncJobManager | undefined = undefined;
|
||||
@@ -507,11 +510,36 @@ export class AgentSession {
|
||||
#promptGeneration = 0;
|
||||
#providerSessionState = new Map<string, ProviderSessionState>();
|
||||
|
||||
#startPowerAssertion(): void {
|
||||
if (process.platform !== "darwin") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.#powerAssertion = MacOSPowerAssertion.start({ reason: "Oh My Pi agent session" });
|
||||
} catch (error) {
|
||||
logger.warn("Failed to acquire macOS power assertion", { error: String(error) });
|
||||
}
|
||||
}
|
||||
|
||||
#stopPowerAssertion(): void {
|
||||
const assertion = this.#powerAssertion;
|
||||
this.#powerAssertion = undefined;
|
||||
if (!assertion) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
assertion.stop();
|
||||
} catch (error) {
|
||||
logger.warn("Failed to release macOS power assertion", { error: String(error) });
|
||||
}
|
||||
}
|
||||
|
||||
constructor(config: AgentSessionConfig) {
|
||||
this.agent = config.agent;
|
||||
this.sessionManager = config.sessionManager;
|
||||
this.settings = config.settings;
|
||||
this.searchDb = config.searchDb;
|
||||
this.#startPowerAssertion();
|
||||
this.#asyncJobManager = config.asyncJobManager;
|
||||
this.#scopedModels = config.scopedModels ?? [];
|
||||
this.#thinkingLevel = config.thinkingLevel;
|
||||
@@ -1673,6 +1701,7 @@ export class AgentSession {
|
||||
if (drained === false && deliveryState) {
|
||||
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
|
||||
}
|
||||
this.#stopPowerAssertion();
|
||||
await this.sessionManager.close();
|
||||
this.#closeAllProviderSessions("dispose");
|
||||
this.#unsubscribePendingActionPush?.();
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
- Generated native module bindings in `native/index.js` and `native/index.d.ts` from napi-rs build output
|
||||
- Added `gen-enums.ts` script to extract and export runtime enum values from TypeScript const enums
|
||||
- Added `embedded-addon.js` for managing embedded native addon variants and metadata
|
||||
- Added `MacOSPowerAssertion` for session-scoped macOS idle-sleep prevention without shelling out
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
Vendored
+22
@@ -78,6 +78,20 @@ export declare class MacAppearanceObserver {
|
||||
stop(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Long-lived macOS power assertion.
|
||||
*
|
||||
* On macOS this acquires an `IOKit` assertion that prevents idle sleep until
|
||||
* the handle is stopped or dropped. On other platforms it is a no-op handle so
|
||||
* the caller can keep one cross-platform code path.
|
||||
*/
|
||||
export declare class MacOSPowerAssertion {
|
||||
/** Acquire a macOS power assertion. */
|
||||
static start(options?: MacOSPowerAssertionOptions | undefined | null): MacOSPowerAssertion
|
||||
/** Release the power assertion early. */
|
||||
stop(): void
|
||||
}
|
||||
|
||||
/** Image container for native interop. */
|
||||
export declare class PhotonImage {
|
||||
/**
|
||||
@@ -943,6 +957,14 @@ export declare enum MacOSAppearance {
|
||||
Light = 'light'
|
||||
}
|
||||
|
||||
/** Options for starting a macOS power assertion. */
|
||||
export interface MacOSPowerAssertionOptions {
|
||||
/** Human-readable reason shown in macOS power diagnostics. */
|
||||
reason?: string
|
||||
/** Keep the display awake in addition to preventing idle system sleep. */
|
||||
display?: boolean
|
||||
}
|
||||
|
||||
/** A single match in the content. */
|
||||
export interface Match {
|
||||
/** 1-indexed line number. */
|
||||
|
||||
@@ -177,7 +177,7 @@ function loadNative() {
|
||||
const bindings = logger.time(`native:loadNative:require:${path.basename(candidate)}`, () =>
|
||||
require_(candidate),
|
||||
);
|
||||
validateNative(bindings, candidate);
|
||||
validateNative(bindings, candidate);
|
||||
if (process.env.PI_DEV) {
|
||||
console.log(`Loaded native addon from ${candidate}`);
|
||||
}
|
||||
@@ -265,6 +265,7 @@ function validateNative(bindings, source) {
|
||||
checkFn("astEdit");
|
||||
checkFn("detectMacOSAppearance");
|
||||
checkFn("MacAppearanceObserver");
|
||||
checkFn("MacOSPowerAssertion");
|
||||
checkFn("projfsOverlayProbe");
|
||||
checkFn("projfsOverlayStart");
|
||||
checkFn("projfsOverlayStop");
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
grep,
|
||||
htmlToMarkdown,
|
||||
invalidateFsScanCache,
|
||||
MacOSPowerAssertion,
|
||||
PtySession,
|
||||
sanitizeText,
|
||||
truncateToWidth,
|
||||
@@ -476,5 +477,12 @@ describe("pi-natives", () => {
|
||||
const input = "\x1b]0;title\x07hello";
|
||||
expect(sanitizeText(input)).toBe("hello");
|
||||
});
|
||||
describe("MacOSPowerAssertion", () => {
|
||||
it("should create a stoppable power assertion handle", () => {
|
||||
const assertion = MacOSPowerAssertion.start({ reason: "pi-natives test" });
|
||||
assertion.stop();
|
||||
assertion.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user