← Commit history

Merge PR #126: mac: hide/raise via NSRunningApplication, no System Events (Automation TCC prompt)

John Lauer ·a01207fe76 ·1mo ago ·parent 9feb6ee
1 file changed +17−10
src/server.js+17−10
@@ -126,23 +126,30 @@ function runPsHiddenPromise(scriptPath, { timeout = 8000 } = {}) {     });   }); }-function runOsascriptPromise(script, { timeout = 8000 } = {}) {+// v2.0.595 (Kyle, 2026-09-16: "i just got a popup for adom bridge trying to control system events+// ... system events shouldn't be required"). NO `tell application "System Events"` anywhere on mac:+// Apple Events to System Events need the Automation TCC grant, which nobody is granted, and since+// Hydrogen forwards every plugin verb through Bridge the prompt reads "Adom Bridge wants to control+// System Events". Hide/unhide/activate go through NSRunningApplication instead, called in-process+// by osascript's JavaScript-for-Automation ObjC bridge: no Apple Event leaves the process, so no+// TCC prompt of any kind. Same doctrine as bridge-macos 1.9.202 and the Rust host-mac crate.+function runJxaPromise(script, { timeout = 8000 } = {}) {   return new Promise(resolve => {-    execFile('/usr/bin/osascript', ['-e', script], { encoding: 'utf8', timeout }, (err, stdout) => resolve(err ? null : (stdout || '').trim()));+    execFile('/usr/bin/osascript', ['-l', 'JavaScript', '-e', script], { encoding: 'utf8', timeout }, (err, stdout) => resolve(err ? null : (stdout || '').trim()));   }); }-// Hide (true) / unhide (false) every window of a process. Resolves true if the script ran.+// Hide (true) / unhide (false) every window of a process (the Cmd+H state). NSRunningApplication's+// hide/unhide are asynchronous, so the result is judged by `hidden` a beat later, not by the call. async function macSetAppHiddenByPid(pid, hidden) {   if (!pid) return false;-  const r = await runOsascriptPromise(`tell application "System Events" to set visible of (first process whose unix id is ${Number(pid)}) to ${hidden ? 'false' : 'true'}`);-  return r !== null;+  const r = await runJxaPromise(`ObjC.import('AppKit'); var a = $.NSRunningApplication.runningApplicationWithProcessIdentifier(${Number(pid)}); (a && !a.isNil()) ? (${hidden ? 'a.hide' : 'a.unhide'}, delay(0.3), String(a.hidden)) : 'null'`);+  return r === String(hidden); } // Unhide + bring the process's windows to the foreground (the user-asked-to-watch path). async function macActivateByPid(pid) {   if (!pid) return false;-  await runOsascriptPromise(`tell application "System Events" to set visible of (first process whose unix id is ${Number(pid)}) to true`);-  const r = await runOsascriptPromise(`tell application "System Events" to set frontmost of (first process whose unix id is ${Number(pid)}) to true`);-  return r !== null;+  const r = await runJxaPromise(`ObjC.import('AppKit'); var a = $.NSRunningApplication.runningApplicationWithProcessIdentifier(${Number(pid)}); (a && !a.isNil()) ? (a.unhide, String(a.activateWithOptions($.NSApplicationActivateAllWindows))) : 'null'`);+  return r !== null && r !== 'null'; } // The pid that owns a session's browser (detached launches record it; puppeteer.launch // exposes process()). Null when unresolvable — callers report applied:false.@@ -9900,7 +9907,7 @@ async function parkSessionWindowDirect(page, sessionId) {       if (!pid) { console.log(`[park] "${sessionId}": no pid to hide (mac) — park-failed`); return 'park-failed'; }       const hidden = await macSetAppHiddenByPid(pid, true);       if (!hidden) {-        console.log(`[park] "${sessionId}": mac hide failed (System Events denied? grant Automation to Adom Bridge) — park-failed`);+        console.log(`[park] "${sessionId}": mac hide failed (NSRunningApplication had no app for the pid; browser gone?) — park-failed`);         return 'park-failed';       }       return 'backgrounded';@@ -17339,7 +17346,7 @@ const server = http.createServer(async (req, res) => {             let _raisedByHwnd = false;             if (process.platform === 'darwin') {               // macOS raise = unhide the session's process + make it frontmost. The CDP-              // bounds/maximize above already sized it; System Events does the activation.+              // bounds/maximize above already sized it; NSRunningApplication does the activation.               const pid = macSessionPid(session);               _raisedByHwnd = await macActivateByPid(pid);               console.log(`[foreground] "${sessionId}" mac activate pid=${pid} → ${_raisedByHwnd}`);