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
v2.0.278: Deterministic window identity, wiggle DELETED (dev-skills/pup-no-wiggle): every window is tagged with a Win32 SetProp('AdomPupSid=<sid>') property at first resolve; lost handles are re-found by GetProp property lookup (no geometry, no nudge). Geometry bounds-matching and the off-screen re-capture nudge are removed from resolveSessionHwndByBounds.
4 files changed
+94−21
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.277",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.277/adom-bridge-puppeteer-v2.0.277.zip",- "sha256": "658511e636e9af8a422146c3f69db0364893b4a19d212c33198fead04e7126ec",- "size": 965577,+ "version": "2.0.278",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.278/adom-bridge-puppeteer-v2.0.278.zip",+ "sha256": "776ed336dfcaa716d8dbaef053ac8dca645eed26e11667035a8c6830f8829092",+ "size": 967290, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-08-22T17:27:26.487025Z"+ "released_at": "2026-08-22T18:46:23.915405Z" }
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.277+2.0.278
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.277",+ "version": "2.0.278", "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. pup_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. pup_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, pup_alert_window posts a native toast whose click focuses the window, pup_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.", "homepage": "https://wiki.adom.inc/adom/adom-bridge", "author": "Adom Inc.",
src/server.js+87−14@@ -489,6 +489,65 @@ function withBirthLock(fn) { _birthLock = new Promise(r => { release = r; }); return prev.then(fn).finally(() => release()); } // monotonic counter for unique off-screen birth positions (birth-time hwnd resolution)++// ============================================================================+// DETERMINISTIC WINDOW IDENTITY — the Win32 window-property tag (dev-skills/pup-no-wiggle)+// ----------------------------------------------------------------------------+// A pup window is identified by a Win32 WINDOW PROPERTY carrying its sessionId, NOT by geometry.+// SetProp attaches "AdomPupSid=<sessionId>" to the OS window; the property is stored WITH the window+// (owned by Chrome), so it survives a page changing its title, a tab crash, and pup itself+// restarting. VERIFIED 2026-08-22: process A set a prop then exited; process B (separate process)+// read it back intact. To re-find a window we READ this property (a lookup), we NEVER guess by+// size/position and we NEVER nudge a window to see which one moves (the banned "wiggle").+const PUP_WIN_PROP = 'AdomPupSid';+function _runPsEncoded(ps, timeout = 8000) {+ return new Promise((resolve) => {+ if (process.platform !== 'win32') return resolve(null);+ try {+ const enc = Buffer.from(ps, 'utf16le').toString('base64');+ execFile('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', enc],+ { windowsHide: true, timeout }, (err, stdout) => resolve(err ? null : String(stdout || '').trim()));+ } catch (e) { resolve(null); }+ });+}+function _sanSid(sessionId) { return String(sessionId == null ? '' : sessionId).replace(/[\r\n"|]/g, ''); }+// Tag an OS window with its owning sessionId. Idempotent. Called whenever pup confirms a (sessionId,+// hwnd) pair (at birth and on every successful resolve), so every window pup knows carries its tag+// and stays re-findable after any handle loss.+async function tagWindowSession(hwnd, sessionId) {+ if (process.platform !== 'win32' || !hwnd || !sessionId) return false;+ const sid = _sanSid(sessionId);+ const ps = `Add-Type @"+using System;using System.Runtime.InteropServices;+public class PupTag { [DllImport("user32.dll",CharSet=CharSet.Unicode)] public static extern bool SetProp(IntPtr h,string s,IntPtr v); }+"@+[void][PupTag]::SetProp([IntPtr]${Number(hwnd)}, "${PUP_WIN_PROP}=${sid}", [IntPtr]1)+Write-Output ok`;+ const r = await _runPsEncoded(ps, 6000);+ return r === 'ok';+}+// Re-find a session's OS window by its property tag — the deterministic recovery path (crash /+// respawn / bridge restart). Uses the reliable ab enumeration to list this profile's Chrome windows,+// then a plain GetProp probe per handle (NO EnumWindows delegate, NO geometry, NO nudge). Returns+// the hwnd whose "AdomPupSid=<sessionId>" property is set, or null. Exactly-one by construction: the+// property name encodes THIS sessionId, so only the window tagged for this session can match.+async function resolveHwndBySessionProp(sessionId) {+ if (process.platform !== 'win32' || !sessionId) return null;+ const sid = _sanSid(sessionId);+ let rows = [];+ try { rows = await enumChromeWidgetWindows({ fresh: true }); } catch (e) {}+ const hwnds = rows.map(r => Number(r.h)).filter(h => h && Number.isFinite(h));+ if (!hwnds.length) return null;+ const ps = `Add-Type @"+using System;using System.Runtime.InteropServices;+public class PupGP { [DllImport("user32.dll",CharSet=CharSet.Unicode)] public static extern IntPtr GetProp(IntPtr h,string s); }+"@+$hwnds = @(${hwnds.join(',')})+foreach ($h in $hwnds) { $v = [PupGP]::GetProp([IntPtr]$h, "${PUP_WIN_PROP}=${sid}"); if ($v.ToInt64() -ne 0) { Write-Output $h; break } }`;+ const out = await _runPsEncoded(ps, 6000);+ const h = Number(String(out || '').trim().split(/\r?\n/)[0] || '0');+ return h && Number.isFinite(h) && h !== 0 ? h : null;+} let _regenBusy = false; // a dashboard "regenerate all overlays" sweep is in flight let _lastRegen = null; // { at, done, swept, painted, changed, results[] } from the last sweep const _overlayRefreshTimers = new Map(); // per-session debounce for the centralized post-verb overlay re-sync@@ -8029,10 +8088,29 @@ async function resolveSessionHwndByBounds(session, sessionId) { try { p0.__pupHwnd = null; } catch (e) {} } else if (bh && await hwndBelongsToPup(Number(bh), session)) { session._hwnd = Number(bh); persistSessionHwnd(sessionId, Number(bh));+ // DETERMINISTIC IDENTITY: tag the window with its sessionId so it stays re-findable after+ // any future handle loss (dev-skills/pup-no-wiggle). Fire-and-forget; off the hot path.+ tagWindowSession(Number(bh), sessionId).catch(() => {}); console.log(`[hwnd] "${sessionId}" resolved OS handle ${bh} — birth-time capture (off-screen at creation)`); return Number(bh); } } catch (e) {}+ // ========================================================================+ // DETERMINISTIC PROPERTY LOOKUP — the sanctioned resolver (dev-skills/pup-no-wiggle).+ // The birth handle is a fast path; when it is gone (crash / respawn / bridge restart) we re-find+ // the window by the Win32 property tag pup wrote at birth. This is a LOOKUP, not a guess: only the+ // window tagged "AdomPupSid=<this session>" can match, so it is exact even on the shared profile+ // where every window has identical geometry. This REPLACES the banned geometry 'wiggle'.+ try {+ const _ph = await resolveHwndBySessionProp(sessionId);+ if (_ph && !hwndOwnedByOtherSession(Number(_ph), sessionId) && await hwndBelongsToPup(Number(_ph), session)) {+ session._hwnd = Number(_ph); persistSessionHwnd(sessionId, Number(_ph));+ session._hwndFailStreak = 0; session._hwndBackoffUntil = 0;+ try { const p0b = (session.tabs || [])[0]; if (p0b && p0b.page) p0b.page.__pupHwnd = Number(_ph); } catch (e) {}+ console.log(`[hwnd] "${sessionId}" resolved OS handle ${_ph} by WINDOW-PROPERTY tag (deterministic; no geometry, no wiggle)`);+ return Number(_ph);+ }+ } catch (e) {} // v1.9.274 (John's dashboard showed 3 'untracked' windows that WERE the real session windows): // every parked window shares the IDENTICAL rect (work-area minus insets), so pure geometry // matching cannot distinguish windows after a restart and misassigns hwnds. The title tag is@@ -8141,20 +8219,11 @@ async function resolveSessionHwndByBounds(session, sessionId) { // match, so an accidental collision silently won. If 0 or 2+ windows match, we cannot know which is // ours, so we LEAVE IT BARE rather than paint a wrong window (John's rule: bare > wrong, forever // after the Edge-overlay incident). A bare window self-heals on the next resolve; a wrong one lies.+ // GEOMETRY MATCHING DELETED (dev-skills/pup-no-wiggle, John 2026-08-22): matching windows by+ // identical bounds/width cannot disambiguate same-geometry windows on the shared profile and IS+ // the banned wiggle. Window identity is the property tag, resolved deterministically above.+ // `matches` stays empty so nothing is ever picked by geometry. const matches = new Set();- for (const { h, L, T, W } of rows) {- if (!h || !Number.isFinite(W)) continue;- for (const sc of [1, 1.25, 1.5, 1.75, 2]) {- if (markerW != null) {- if (Math.abs(W - Math.round(markerW * sc)) <= 3) { matches.add(h); break; }- } else {- const dx = Math.abs(L - Math.round(bounds.left * sc));- const dy = Math.abs(T - Math.round(bounds.top * sc));- const dw = Math.abs(W - Math.round(bounds.width * sc));- if (dx <= 30 && dy <= 30 && dw <= 40) { matches.add(h); break; }- }- }- } try { if (_widthChanged) await cdp.send('Browser.setWindowBounds', { windowId, bounds: { width: _origW } }); } catch (e) {} if (_releaseWiggle) { try { _releaseWiggle(); } catch (e) {} } try { if (cdp) await cdp.detach(); } catch (e) {}@@ -8181,7 +8250,11 @@ async function resolveSessionHwndByBounds(session, sessionId) { // identity by construction), then move it straight back on-screen at z-bottom. INVISIBLE because // the window is already background/z-bottom; NEVER done to a window the user is using. This gives // an on-screen window the same unambiguous handle a birth-time window gets.- if (!userIsUsingWindow(session) && (!session._recaptureAt || Date.now() - session._recaptureAt > 30000)) {+ // OFF-SCREEN RE-CAPTURE NUDGE DELETED (dev-skills/pup-no-wiggle, John 2026-08-22): this MOVED the+ // window via setWindowBounds and identified it by "which window's rect changed" — the textbook+ // wiggle. It is banned. An untagged window on a multi-window profile is left BARE (no guess); it+ // becomes resolvable the instant it carries its property tag (every birth + every resolve tags it).+ if (false && !userIsUsingWindow(session) && (!session._recaptureAt || Date.now() - session._recaptureAt > 30000)) { session._recaptureAt = Date.now(); try { const tab0 = (session.tabs || [])[0];