app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← Commit history
Merge PR #126: mac: hide/raise via NSRunningApplication, no System Events (Automation TCC prompt)
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}`);