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.367: Stop pinning the page viewport on every sized open (the cause John saw across three different threads' windows): a sized pup_open_window applied a permanent CDP device-metrics override and never recorded it, so the page stopped tracking its own window and every disclosure stayed silent. The original repair is kept but applied only when the window genuinely fails to follow its bounds, and an unrecorded override is now disclosed from measurement.
5 files changed
+52−17
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.366",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.366/adom-bridge-puppeteer-v2.0.366.zip",- "sha256": "0b1800bcf45efbac23a7865b3409f6442b23698fbe468bead8b5a11eaad14ab3",- "size": 1533384,+ "version": "2.0.367",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.367/adom-bridge-puppeteer-v2.0.367.zip",+ "sha256": "57a9a0d09b708c6cddda49bd182e62112328e80dfe5af0aedfb3fbcd0c7e4b9a",+ "size": 1534302, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-08-28T10:40:30.512960Z"+ "released_at": "2026-08-28T21:49:13.730322Z" }
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "pup-bridge",- "version": "2.0.366",+ "version": "2.0.367", "type": "app", "description": "The pup SKILL PACK \u2014 the AI skills that teach a cloud/container AI how to drive the Adom Desktop Puppeteer (pup) bridge (browser_* verbs): the everyday `pup` skill plus focused sub-skills for windows/sessions/tabs, screenshots+recording, browsers+Chrome install, and driving the Adom wiki. Installs into ~/.claude/skills + ~/.codex/skills. DOCS ONLY \u2014 this is NOT the bridge runtime. The bridge itself (the Node program that drives the browser) runs on the DESKTOP: Adom Desktop bundles it and auto-installs/updates it, and it's published separately on the Releases tab as adom-bridge-puppeteer-v<ver>.zip. The pkg version is kept in lockstep with the Release version so the page's max version row always carries BOTH a pkg tarball (skills) and a runtime zip (bridge) \u2014 a skills-only or runtime-only bump on the top row breaks constraint installs. TL;DR: pkg install = the skills for your AI; the bridge = the Release zip, which Adom Desktop manages for you.", "keywords": [
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.366+2.0.367
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.366",+ "version": "2.0.367", "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+44−9@@ -2528,7 +2528,7 @@ async function recoverSessions() { try { const t0 = (session.tabs || [])[0]; if (!t0 || !t0.page) return;- const m = await measureViewport(t0.page);+ const m = await measureViewport(t0.page, session); if (m && m.emulated) { session._emulatedViewport = { width: m.innerWidth, height: m.innerHeight, deviceScaleFactor: m.devicePixelRatio, derived: true }; session._lastOuterWidth = m.outerWidth;@@ -12078,13 +12078,15 @@ function looksEmulated(innerWidth, outerWidth) { if (innerWidth > outerWidth + 2) return true; // wider than the window: content cropped return (outerWidth - innerWidth) > EMULATION_SLACK; // far narrower: dead space beside it }-async function measureViewport(page) {+async function measureViewport(page, session) { try { const d = await page.evaluate(() => ({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, devicePixelRatio: window.devicePixelRatio, }));- return { ...d, emulated: looksEmulated(d.innerWidth, d.outerWidth) };+ const out = { ...d, emulated: looksEmulated(d.innerWidth, d.outerWidth), at: Date.now() };+ if (session) { session._measuredViewport = out; session._lastOuterWidth = d.outerWidth; }+ return out; } catch (e) { return null; } } function sessionForResponse(obj, argsIn) {@@ -12120,7 +12122,14 @@ function attachSessionNotices(obj, verb, argsIn) { // in-page measurement agrees with itself and disagrees with the screen, so the override has to // say so on the verbs used to measure and to look. if (obj._viewportOverride === undefined && VIEWPORT_SENSITIVE.has(verb)) {- const ev = sess._emulatedViewport;+ // Disclose an UNRECORDED override too. Today's root cause pinned the viewport without ever+ // writing the record, so a warning that trusts the record is silent exactly when it matters.+ // sess._measuredViewport is refreshed by any verb that measures the page.+ let ev = sess._emulatedViewport;+ if (!ev && sess._measuredViewport && sess._measuredViewport.emulated) {+ ev = { width: sess._measuredViewport.innerWidth, height: sess._measuredViewport.innerHeight,+ deviceScaleFactor: sess._measuredViewport.devicePixelRatio, derived: true };+ } if (ev && ev.width) { // Both failure directions are real and they look opposite on screen. NARROWER than the // window leaves dead space down the side; WIDER crops content off the edge, which is the@@ -15220,11 +15229,37 @@ const server = http.createServer(async (req, res) => { const foreground = fgGranted; // v1.8.70: the gate's verdict, not the raw flag if (sized) { try { await setSessionWindowBounds(session, { x: wx, y: wy, width: w, height: h }); } catch (e) {}- // v1.8.21: ALSO pin the page VIEWPORT (CDP device-metrics) to width×height.- // The OS-window bounds alone don't guarantee a matching inner viewport (DPI /- // window-state quirks left it at ~170px on some laptops); page.setViewport makes- // innerWidth/innerHeight deterministic, which is what fullPage/element shots need.- try { await session.page.setViewport({ width: Math.round(w), height: Math.round(h), deviceScaleFactor: Number(args.deviceScaleFactor) || 1 }); } catch (e) {}+ // v2.0.367 (John: "it keeps happening to all ai threads... you're doing something to+ // fuck up the browser client area"). THIS LINE WAS THE CAUSE, and it is why the problem+ // followed every thread rather than one.+ //+ // 1.8.21 pinned the page viewport to the requested width/height on EVERY sized open, to+ // fix a real bug (DPI/window-state quirks left innerWidth at ~170px on some laptops).+ // But a CDP device-metrics override is PERMANENT for the life of the window, so from+ // that moment the page stopped tracking its own window. Maximise it, or let the user+ // resize it, and the page keeps rendering at the original width inside a much larger+ // frame: content stops short and the rest is dead space. Three different threads' windows+ // showed exactly that today.+ //+ // It was also INVISIBLE, because this path never recorded _emulatedViewport. So pup's+ // listings said "no override", every warning I added keyed off that record and stayed+ // silent, and the owning AI measured innerWidth, saw the page filling it, and honestly+ // reported "100% width" while the human looked at a gap.+ //+ // The fix keeps the original repair and drops the permanent cost: size the OS window,+ // MEASURE what the page actually got, and only pin if the page failed to follow (the+ // ~170px case). A window that tracks its own bounds is left alone to keep tracking them.+ try {+ const _got = await session.page.evaluate(() => ({ iw: window.innerWidth, ih: window.innerHeight }));+ const _wantW = Math.round(w);+ const _followed = _got && _got.iw > 200 && Math.abs(_got.iw - _wantW) <= Math.max(40, _wantW * 0.15);+ if (!_followed) {+ await session.page.setViewport({ width: _wantW, height: Math.round(h), deviceScaleFactor: Number(args.deviceScaleFactor) || 1 });+ session._emulatedViewport = { width: _wantW, height: Math.round(h), deviceScaleFactor: Number(args.deviceScaleFactor) || 1, reason: 'window did not follow its bounds' };+ session._lastOuterWidth = _got && _got.iw ? null : null;+ console.log(`[viewport] "${sessionId}": window did not follow its bounds (page reported ${_got ? _got.iw : '?'}px for a ${_wantW}px window) — PINNED the viewport as a repair, and recorded it so it is disclosed`);+ }+ } catch (e) {} } // v1.8.69: re-assert background after an explicit sizing call — Z-ORDER ONLY. // The old call here was the legacy osBackgroundWindowByPid, whose PS park MOVES the