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
One taskbar icon per window + tabs born in the right window (John's audit + his ask: 'why are all the windows you opened in the same taskbar icon? when i say to pup open a window i generally want them as separate taskbar icons. is that not as feasible now that you do everything under the adom-you profile?'). It IS feasible: window identity is a per-HWND property, not per-process, so the shared profile is irrelevant. pup now gives every window a ONE-TIME gentle AUMID stamp at creation (and the 10s enforcer catches up any unstamped survivor once) - one stamp, one button recreation, then done: none of the category re-stamp churn or jump-list machinery that made the old split mode invasive (those stay off under minimal-touch). After a stamp the taskbar button is REBORN bare, so the overlay is marked unpainted and the enforcer immediately repaints the new button with the window's favicon badge. The dashboard mimic groups accordingly: stamped window = its own button; only unstamped windows (stamp pending/failed, e.g. PDF viewers) share the process button. ALSO fixed the audit's phantom '-split1-' sessions: browser_open_tab created tabs via newPage(), which has NO window affinity - Chrome dropped the tab into the process's most-recently-active window, and the (correct) drag-out detector then adopted the displaced original as a new session. Tabs are now created FROM INSIDE the session's own window via CDP Runtime.evaluate window.open() with userGesture:true (satisfies the popup blocker with no global flag), with newPage() as fallback. Ralph's grouping assertion updated to stamped+process-group arithmetic.
4 files changed
+71−22
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.9.244",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.244/adom-bridge-puppeteer-v1.9.244.zip",- "sha256": "72249f2f8adf5b2367718334f56179852a214d26e15cb0c4afb40afe8bfcbbe8",- "size": 466384,+ "version": "1.9.245",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.245/adom-bridge-puppeteer-v1.9.245.zip",+ "sha256": "65d8f4c947fc49b402b63130bdb912d24946bfc237a09dd83c1e76eb156b0740",+ "size": 467513, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.244+1.9.245
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.9.244",+ "version": "1.9.245", "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. browser_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. browser_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, browser_alert_window posts a native toast whose click focuses the window, browser_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-desktop", "author": "Adom Inc.",
src/server.js+65−16@@ -2880,11 +2880,21 @@ async function ensurePupIdentityRegistered() { return true; } catch (e) { console.log(`[identity] register threw: ${e.message}`); return false; } }-async function stampPupIdentity(sessionId) {+async function stampPupIdentity(sessionId, opts) { // v1.9.185 minimal-touch: the AUMID stamp is the single most invasive thing pup does to a window // it does not own (Windows DESTROYS and RECREATES the taskbar button on an AppUserModelID change). // Gated at the choke point so no caller can leak it.- if (minimalTouch()) return false;+ // v1.9.245 (John: "when i say open a window i generally want them as separate taskbar icons"):+ // {once:true} callers may stamp UNDER minimal-touch too — but exactly ONE gentle stamp per window,+ // at creation (or the enforcer's one-time catch-up), never the category re-stamp churn that made+ // the old machinery invasive. One stamp = one button recreation = each window gets its OWN taskbar+ // icon, which is what he wants; the shared adom-you profile is irrelevant because window identity+ // is a per-HWND property, not per-process.+ if (minimalTouch()) {+ if (!(opts && opts.once)) return false;+ const _s0 = sessions.get(sessionId);+ if (_s0 && _s0._curAppId) return true; // once means ONCE+ } if (process.platform !== 'win32' || !sessionId || _identitySupported === false) return false; if (!_identityRegistered) { await ensurePupIdentityRegistered(); if (_identitySupported === false) return false; } try {@@ -2944,7 +2954,7 @@ async function stampPupIdentity(sessionId) { // the header baked GENERIC and the later headerBranded:true had no visible effect — exactly what // John kept seeing. Brand the header (writes the window's Relaunch trio) FIRST, so the button is // born with it.- try { await updateWikiJumplist(sess, sessionId, { appId }); } catch (e) {}+ if (!minimalTouch()) { try { await updateWikiJumplist(sess, sessionId, { appId }); } catch (e) {} } } const r = await chrome.adCommand('desktop_set_window_identity', { titleContains: `(session: ${sessionId}`, cacheKey: `pup-${sessionId}-${cat}`,@@ -2974,9 +2984,13 @@ async function stampPupIdentity(sessionId) { console.log(`[identity] "${sessionId}" stamped appId=${appId}: ${(data.applied || []).join(',') || 'ok'}`); if (sess && !grouped && prevAppId !== appId) { sess._curAppId = appId;+ // v1.9.245: the stamp DESTROYED and RECREATED the taskbar button, so whatever overlay was+ // painted is gone from glass. Mark bare so the 10s enforcer repaints the new button; also+ // drop the cached hwnd — the new button may live on a new handle.+ sess._appOverlay = false; sess._overlayEnforceAt = 0; sess._hwnd = null; // The jump list attaches to the AUMID — a new appId starts with NO jump list, so force a // re-attach on the next updateWikiJumplist pass.- updateWikiJumplist(sess, sessionId).catch(() => {}); // deduped by _wikiJumplistKey+ if (!minimalTouch()) updateWikiJumplist(sess, sessionId).catch(() => {}); // deduped by _wikiJumplistKey // v1.9.112 (John: "still shows for 200ms then disappears; second right-click works") — // RE-COMMIT THE JUMP LIST ONCE THE BUTTON IS REAL. Measured elimination got us here: nothing // fires while idle (0 log lines in 40s), and windows with exactly ONE AUMID registration blip@@ -6822,7 +6836,7 @@ async function launchSession(sessionId, url, { freshProfile = false, profile, st // v1.9.185 minimal-touch: do NOT stamp a per-session AUMID. Changing a window's AppUserModelID // makes Windows DESTROY and RECREATE its taskbar button — the single most invasive thing pup did // to a window it does not own. In minimal mode the window keeps Chrome's own identity.- if (process.platform === 'win32' && !minimalTouch()) { stampPupIdentity(sessionId).catch(() => {}); }+ if (process.platform === 'win32') { stampPupIdentity(sessionId, { once: true }).catch(() => {}); } // v1.8.68: the park is DEFINED here but KICKED OFF only after the real URL's navigation has // SETTLED (below). Rationale: the first cross-origin paint RAISES the window; if the window@@ -7024,10 +7038,33 @@ async function addTabToSession(sessionId, url) { // handler splits it. Funnelling newPage through the same _profileOpenChain that serializes opens // removes the interleave — each tab is created when its session's window is the settled context. const _tabPrev = _profileOpenChain.get(session.profileName) || Promise.resolve();- const _tabCur = _tabPrev.then(() => Promise.race([- be.browser.newPage(),- new Promise((_, rej) => setTimeout(() => rej(new Error('newPage timed out — the browser did not create the tab in 20s. Retry browser_open_tab; if it persists, browser_close_window + browser_open_window.')), 20000)),- ]));+ // v1.9.245 (John's audit caught phantom "-split1-" sessions): newPage() has NO window affinity —+ // Chrome puts the tab in the process's most-recently-active window, and with many windows on the+ // shared profile that is the WRONG window as often as not. Serializing (1.9.157) narrowed the race+ // but could not remove it: the API simply cannot target a window. So create the tab FROM INSIDE the+ // session's own window: CDP Runtime.evaluate window.open() with userGesture:true (satisfies the+ // popup blocker without a global --disable-popup-blocking). The drag-out detector then never sees a+ // misplaced tab. newPage() stays as the fallback if the in-window open yields nothing in 5s.+ const _tabCur = _tabPrev.then(async () => {+ const anchor = (session.tabs || []).find(t => t.tabId === session.activeTabId) || (session.tabs || [])[0];+ if (anchor && anchor.page && !anchor._crashed) {+ let cdp = null;+ try {+ cdp = await anchor.page.target().createCDPSession();+ await cdp.send('Runtime.evaluate', { expression: 'window.open("about:blank")', userGesture: true });+ const t = await be.browser.waitForTarget(+ tg => { try { return tg.opener() === anchor.page.target() && tg.type() === 'page'; } catch (e) { return false; } },+ { timeout: 5000 });+ const pg = t && await t.page();+ if (pg) return pg;+ } catch (e) { console.log(`[tab] in-window open failed (${e.message}) — falling back to newPage`); }+ finally { try { if (cdp) await cdp.detach(); } catch (e) {} }+ }+ return Promise.race([+ be.browser.newPage(),+ new Promise((_, rej) => setTimeout(() => rej(new Error('newPage timed out — the browser did not create the tab in 20s. Retry browser_open_tab; if it persists, browser_close_window + browser_open_window.')), 20000)),+ ]);+ }); _profileOpenChain.set(session.profileName, _tabCur.catch(() => {})); const page = await _tabCur; try { page._pupCreated = true; } catch (e) {}@@ -8019,6 +8056,7 @@ async function dashboardSnapshot() { // green = pup painted a REAL page favicon and the exact bytes are on disk to serve the badge // amber = painted, but the page had no favicon so it's the legit generic fallback // red = not painted yet / paint failed → the taskbar button would be bare or wrong+ _curAppId: sess._curAppId || null, overlayLed: (() => { const painted = !!sess._appOverlay; const hasBytes = (() => { try { return fs.existsSync(path.join(require('os').tmpdir(), 'pup-badge-debug', sid + '.png')); } catch (e) { return false; } })();@@ -8050,13 +8088,21 @@ async function dashboardSnapshot() { // hwnd/session invented a button per window and the dashboard showed more rows than the real // taskbar had icons. Key by PROFILE (= one Chrome process = one button), which is why the header // count "chromes N" equals the number of real taskbar icons.- // v1.9.237 (John: 'we stopped using aumid so what are you talking about?'). Correct: minimal-touch- // stamps NO AUMID (verified: zero Adom.Pup* registry keys, every session curAppId=null), so there- // is exactly ONE real taskbar button per Chrome PROCESS. Group by process; the button's base icon- // is that browser exe's real icon. No AUMID branch - it was dead code chasing a deleted mechanism.- key = 'chrome-process:' + w.profile;- label = 'Chrome (' + w.profile + ')';- const baseIcon = '/dashboard/baseicon?profile=' + encodeURIComponent(w.profile);+ // v1.9.245 (John: 'when i say open a window i generally want them as separate taskbar icons'):+ // pup now stamps each window ONCE at creation (per-HWND identity, works fine on the shared+ // profile), so a STAMPED window has its OWN real taskbar button; only unstamped windows (stamp+ // pending/failed, e.g. PDF viewers with no title tag) share the process button. This mirrors+ // exactly what Windows does, which is the whole point of the mimic.+ let baseIcon;+ if (w._curAppId) {+ key = 'aumid:' + w._curAppId;+ label = w.sessionId;+ baseIcon = '/dashboard/baseicon?profile=' + encodeURIComponent(w.profile);+ } else {+ key = 'chrome-process:' + w.profile;+ label = 'Chrome (' + w.profile + ')';+ baseIcon = '/dashboard/baseicon?profile=' + encodeURIComponent(w.profile);+ } if (!buttons.has(key)) buttons.set(key, { key, label, kind: 'process', baseIcon, grouping: minimalTouch() ? 'minimal-touch' : grouping, windows: [], badgeSession: null, badgeAt: 0, badgeVia: null }); const btn = buttons.get(key); btn.windows.push(w.sessionId);@@ -12275,6 +12321,9 @@ server.listen(PORT, BIND_HOST, async () => { try { for (const [sid2, s2] of [...sessions]) { if (s2._lostBrowser || !(s2.tabs || []).length) continue;+ // v1.9.245: per-window taskbar identity is enforced like the overlay — a window without its+ // one-time stamp gets it here (covers windows opened before this build / restart recovery).+ if (!s2._curAppId && !s2._stampTriedAt) { s2._stampTriedAt = Date.now(); stampPupIdentity(sid2, { once: true }).catch(() => {}); continue; } const bare = !s2._appOverlay; const stuckGeneric = s2._appOverlay && s2._overlayIsGeneric; if (!bare && !stuckGeneric) continue;