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.230: A first cleanup pass on the bridge: the four-second sweep now makes one CDP round-trip per tab instead of three and no longer runs a title blob that was a measured no-op, the sixty-second forced badge repaint stretches to five minutes since real clears already trigger a repaint by event, window-alive verdicts are cached so a single stuck session can no longer pin a PowerShell C-sharp recompile every eight seconds, the idle process sweeps skip PowerShell entirely when nothing could have leaked, dashboard icon endpoints serve from an mtime-keyed cache instead of decoding an ICO per window per two seconds, screencast frames write asynchronously instead of blocking the event loop sixty times a second, and about two hundred and forty lines of dead and unreachable code are gone.
4 files changed
+118−294
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.229",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.229/adom-bridge-puppeteer-v2.0.229.zip",- "sha256": "1115f6291d99a628d64b07d64e59b9b9314824b9291b4df2adb169a4c8bcd4ad",- "size": 947291,+ "version": "2.0.230",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.230/adom-bridge-puppeteer-v2.0.230.zip",+ "sha256": "32bc9171c0918256fb48105e3b46dfa9d33175dea8f8dc1da36a84996f69f8c4",+ "size": 943680, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-08-18T10:44:14.114224Z"+ "released_at": "2026-08-18T11:16:35.547046Z" }
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.229+2.0.230
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.229",+ "version": "2.0.230", "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+111−287@@ -57,21 +57,6 @@ function runPsHiddenPromise(scriptPath, { timeout = 8000 } = {}) { }); }); }-// ── macOS window-management primitives (the darwin analogs of the Win32 helpers) ──-// Strategy on macOS: no cross-process per-window z-bottom API exists, and AppKit CLAMPS-// windows onto a visible screen (the -32000 off-screen launch trick doesn't hold). The-// reliable "background by default" primitive is the APP-LEVEL HIDE (the Cmd+H state):-// every pup session runs its own browser process (one per profile), so hiding that-// process hides exactly that session's windows — while CDP keeps driving + rendering-// them (--disable-backgrounding-occluded-windows), so screenshot/eval still work.-// Raise = unhide + activate. Driven via System Events (rides the Automation grant the-// embedding Adom Desktop already holds; a denied grant degrades to applied:false, never-// a throw). Hide is PER-PROCESS, so multi-session shared-profile windows hide/show as a-// group on mac — a documented DEGRADED vs the per-hwnd Windows path.-function runOsascript(script, { timeout = 6000 } = {}) {- try { return execFileSync('/usr/bin/osascript', ['-e', script], { encoding: 'utf8', timeout }).trim(); }- catch (e) { return null; }-} function runOsascriptPromise(script, { timeout = 8000 } = {}) { return new Promise(resolve => { execFile('/usr/bin/osascript', ['-e', script], { encoding: 'utf8', timeout }, (err, stdout) => resolve(err ? null : (stdout || '').trim()));@@ -2374,54 +2359,6 @@ async function showPupCursor(page, x, y, { click = true, label = null } = {}) { }, x, y, click, label, pupCursorMarkUri()); } catch (e) {} }-// v1.9.167 (John's assisted-foreground protocol) — a general OS caption (what/why narration).-// v1.9.199 (John, 2026-07-29, after I typed into HIS window): NATIVE OS INPUT GUARD.-//-// desktop_click / desktop_type / desktop_press_key do NOT target a window — they go to WHATEVER-// WINDOW HAS OS FOCUS at that instant. This user runs many AI threads and alt-tabs away from a-// foregrounded window almost immediately, every time. An agent (me) fired desktop_type without ever-// warning him, he had already alt-tabbed back to his own work, and the text landed in HIS window.-// Then I misdiagnosed it as "Windows failed to foreground the window" — blaming the OS for a human-// simply taking their focus back, because nobody had asked them not to.-//-// This guard makes that impossible by construction. Every native-input path MUST call it immediately-// before EACH event (not once per sequence — focus can change between two keystrokes, and half a-// password in the wrong window is real harm). It: warns via AD toast + on-screen caption, tells the-// user explicitly to stop typing, waits until the target window is genuinely the OS foreground, and-// re-verifies at dispatch time. Returns false if we never got a safe window — callers MUST NOT-// dispatch on false.-async function ensureSafeForNativeInput(sessionId, { reason = 'drive the window directly', warn = true } = {}) {- if (process.platform !== 'win32') return true;- const hwnd = await sessionHwnd(sessionId);- if (!hwnd) {- console.log(`[native-guard] "${sessionId}": no hwnd — REFUSING native input (cannot prove where it would land)`);- return false;- }- const fg = await osGetForegroundWindow();- if (String(fg) === String(hwnd)) return true; // already ours; safe to dispatch-- if (!warn) {- console.log(`[native-guard] "${sessionId}": window is NOT foreground — REFUSING native input`);- return false;- }- // Warn BEFORE asking for the screen, and say the quiet part out loud: hands off the keyboard.- const msg = `Adom needs your "${sessionId}" window in front to ${reason}. Please DON'T type or click until I say done.`;- try { await chrome.abCommand('notify_user', { title: 'Adom needs the keyboard', body: msg }, { timeoutMs: 5000 }); } catch (e) {}- try { await pupCaption(msg, 15000, 'pup-native-guard'); } catch (e) {}- const ready = await waitForUserReady(sessionId, hwnd, { maxMs: 60000 });- if (!ready) {- try { await pupCaption('Adom skipped that step: the window never came to the front, so nothing was typed.', 6000, 'pup-native-guard'); } catch (e) {}- console.log(`[native-guard] "${sessionId}": user never brought the window forward — REFUSED native input`);- return false;- }- // Re-verify at the last possible moment.- const fg2 = await osGetForegroundWindow();- if (String(fg2) !== String(hwnd)) {- console.log(`[native-guard] "${sessionId}": focus moved again at dispatch time — REFUSED native input`);- return false;- }- return true;-} // v1.9.221 (John, standing rule): A CAPTION IS NEVER SHOWN LONGER THAN 2 SECONDS. Callers had been // passing 5s, 6s, 8s and even 15s, which parks text over the user's screen long after it has been@@ -2576,14 +2513,28 @@ function _pidsWithVisibleWindow(pids) { } catch (e) { resolve(new Set()); } }); }+// v2.0.230: this is called from the 8s reconciler, and each call was a PowerShell spawn PLUS a+// Roslyn C# compile (~0.5s CPU). A session the ground-truth check SPARES is not deleted, so it+// re-entered the same branch on every tick — a self-sustaining burner that one stuck window could+// pin forever. Verdicts are now cached per hwnd for 60s (a window does not blink in and out of+// existence faster than that, and the reconciler is a safety net, not a realtime probe).+const _hwndAliveCache = new Map(); // hwnd -> { alive, at }+const HWND_ALIVE_TTL = 60000; function osHwndAlive(hwnd) { return new Promise((resolve) => { const h = Number(hwnd); if (!_isWin || !Number.isFinite(h) || h <= 0) { resolve(false); return; }+ const c = _hwndAliveCache.get(h);+ if (c && Date.now() - c.at < HWND_ALIVE_TTL) { resolve(c.alive); return; }+ if (_hwndAliveCache.size > 200) { for (const [k, v] of _hwndAliveCache) { if (Date.now() - v.at > HWND_ALIVE_TTL) _hwndAliveCache.delete(k); } } const ps = `Add-Type -Namespace PW -Name U -MemberDefinition '[DllImport("user32.dll")] public static extern bool IsWindow(IntPtr h); [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);'; if ([PW.U]::IsWindow([IntPtr]${h}) -and [PW.U]::IsWindowVisible([IntPtr]${h})) { 'ALIVE' }`; try { execFile('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ps],- { encoding: 'utf8', timeout: 7000, windowsHide: true }, (err, out) => resolve(!err && /ALIVE/.test(out || '')));+ { encoding: 'utf8', timeout: 7000, windowsHide: true }, (err, out) => {+ const alive = !err && /ALIVE/.test(out || '');+ _hwndAliveCache.set(h, { alive, at: Date.now() });+ resolve(alive);+ }); } catch (e) { resolve(false); } }); }@@ -3682,9 +3633,6 @@ function pupCategory(sessionId) { return only; } catch (e) { return null; } }-function pupSessionAuthed(sessionId) {- try { const s = sessionId && sessions.get(sessionId); return !!(s && s._wikiAuthed); } catch (e) { return false; }-} function pupIconPath(sessionId) { if (sessionId === 'pup-dashboard') return notchedIconPath(path.join(__dirname, 'icons', PUP_CAT_ICONS.app || PUP_ICON_DEFAULT)); // v1.9.261 (John): aumidIconStyle 'standard' = ONE icon for every AUMID window (the default pup@@ -4353,26 +4301,6 @@ function threadSlug(name) { const h = require('crypto').createHash('sha1').update(String(name || '')).digest('hex').slice(0, 6); return `${s}-${h}`; }-function threadTileHtml(name) {- const font = fgFontB64();- const face = font ? `@font-face{font-family:FG;src:url(data:font/woff2;base64,${font}) format('woff2');font-weight:700}` : '';- const nm = String(name || '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));- // Render at 256; the tile fills the whole canvas (Windows rounds the taskbar itself). Text is- // #05221f on #00b8b1 (Adom teal), auto-fit with a smart 2-line wrap at a space/hyphen/mid-split.- return `<!doctype html><meta charset=utf8><style>${face}-html,body{margin:0}#c{width:256px;height:256px;background:#00b8b1;display:flex;align-items:center;justify-content:center;overflow:hidden}-#t{color:#05221f;font-family:FG,system-ui,sans-serif;font-weight:700;line-height:.9;text-align:center;white-space:pre;letter-spacing:-.02em}</style>-<div id=c><div id=t></div></div><script>-(function(){var name=${JSON.stringify(String(name || ''))},box=256,el=document.getElementById('t');-function fs(text){var t=document.createElement('div');t.style.cssText='position:absolute;visibility:hidden;font-family:FG,system-ui;font-weight:700;line-height:.9;white-space:pre;letter-spacing:-.02em';t.textContent=text;document.body.appendChild(t);var s=box*0.92;t.style.fontSize=s+'px';for(var i=0;i<90&&(t.scrollWidth>box*0.86||t.scrollHeight>box*0.86);i++){s*=0.95;t.style.fontSize=s+'px';}t.remove();return s;}-var cands=[name];var sp=name.lastIndexOf(' '),hy=name.indexOf('-');-if(sp>0)cands.push(name.slice(0,sp)+'\\n'+name.slice(sp+1));else if(hy>0)cands.push(name.slice(0,hy+1)+'\\n'+name.slice(hy+1));-if(sp<0&&hy<0&&name.length>=7){var m=Math.ceil(name.length/2);cands.push(name.slice(0,m)+'\\n'+name.slice(m));}-var best=name,bf=0;cands.forEach(function(x){var f=fs(x);if(f>bf){bf=f;best=x;}});-el.textContent=best;el.style.fontSize=bf+'px';document.fonts&&document.fonts.ready&&document.fonts.ready.then(function(){var f=fs(best);el.style.fontSize=f+'px';el.textContent=best;});-window.__tileReady=true;})();-</script>`;-} // Wrap a 256x256 PNG buffer into a multi-size .ico (256/48/32/16, each a PNG-compressed entry — the // modern ICO form Windows Vista+ reads). sharp does the downscales (available on the desktop). async function pngToIco(png256) {@@ -4414,6 +4342,22 @@ async function pngToIco(png256) { }); return Buffer.concat([header, dir, ...entries.map(e => e.data)]); }+// v2.0.230: decoded-icon cache for the dashboard's per-render icon endpoints (path+mtime keyed).+const _iconBufCache = new Map(); // "path|mtimeMs" -> Buffer+function cachedIconPng(file, decode) {+ try {+ const mt = fs.statSync(file).mtimeMs;+ const key = `${file}|${mt}`;+ const hit = _iconBufCache.get(key);+ if (hit) return hit;+ const out = decode(fs.readFileSync(file));+ if (out) {+ if (_iconBufCache.size > 60) _iconBufCache.clear();+ _iconBufCache.set(key, out);+ }+ return out;+ } catch (e) { return null; }+} const _threadTileInflight = new Map(); // name -> Promise (dedupe concurrent renders) // Render (or return cached) the .ico path for a thread name. v2.0.158: draw the tile on an OFFSCREEN // CANVAS inside an EXISTING session page (`page`) — the FG font is injected via the FontFace API — so@@ -5718,7 +5662,6 @@ const WIKI_AUTH_PROFILE = 'adom-wiki-authed'; // one Chrome process). Step 1 (reparent so Chrome survives a bridge restart) is what makes a // durable profile safe: without it, repeated dirty kills would rot the shared jar. const DEFAULT_PROFILE = 'adom-you';-function isWikiUrl(u) { try { return /(^|\.)wiki\.adom\.inc$/i.test(new URL(u).hostname); } catch (e) { return false; } } // v1.8.99: the SSO session covers the whole Adom ecosystem, not just the wiki. Gate the authed // treatment on ANY adom.inc host (wiki + other Adom properties). NON-Adom sites (ti.com, // digikey.com, even adom.cloud proxies) get NOTHING — no auth routing, hints, or glyph.@@ -5899,88 +5842,6 @@ function wikiViewHint(url, rawView, auth) { return ` (📖 Wiki on the SHARED profile — LOGGED IN as the user if they've signed into the wiki (the usual case, one shared login serves every window), logged out only if they never have. This is NOT the guaranteed-anonymous view: to see exactly what the world sees (e.g. confirm a publish didn't leak private source), pass wikiView:"public" (server-rendered anonymous view via ?view=public — same window, session untouched). For the explicit logged-in view, pass wikiView:"authed".)`; } -async function fetchPageFavicon(page) {- // v1.9.130: collect candidate URLs in-page (a DOM read — CORS-free), but FETCH them from the- // BRIDGE process. The old in-page fetch() only ever worked because --disable-web-security had- // CORS off globally; when v1.9.129 restored the same-origin policy, any favicon whose fetch- // crosses origins (accounts.google.com redirects its /favicon.ico to www.google.com) started- // CORS-failing and those windows silently lost their overlay — John spotted the missing icon- // within the hour. Node has no same-origin policy, so a bridge-side fetch cannot regress this.- // (Tradeoff, accepted: the bridge fetch carries no page cookies, so a favicon behind auth won't- // resolve — rare, and the count-badge fallback covers it. localhost favicons still work: the- // bridge runs on the same machine as Chrome.)- let hrefs = [];- try {- hrefs = await page.evaluate(() => {- // v1.9.43: a page can declare a PLACEHOLDER icon (example.com ships <link href="data:,">) —- // drop obvious junk, always keep /favicon.ico as the last resort.- const links = Array.from(document.querySelectorAll('link[rel~="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]'));- const out = links.map(l => l.href).filter(h => h && h !== 'data:,' && !/^data:,?$/.test(h));- out.push(new URL('/favicon.ico', location.origin).href);- return out;- });- } catch (e) { return null; }- // v1.9.217 (John: "the website being loaded has its favicon?" — dashboard-v2.adom.inc showed the- // GENERIC icon). The bridge-side fetch below carries NO COOKIES, so any favicon behind auth returns- // 401/403 and pup falls back to a generic icon. The old comment called that "rare"; it is not — it- // hits EVERY authed Adom dashboard and app. So try IN-PAGE first: the page's own fetch carries its- // cookies and same-origin favicons (the overwhelmingly common case, /favicon.svg on the same host)- // resolve cleanly. Only if that fails do we fall back to the cookie-less bridge fetch, which is- // still the right tool for a cross-origin icon the page itself cannot read.- for (const href of (hrefs || [])) {- if (href.startsWith('data:')) break; // handled by the loop below- try {- const inPage = await withTimeout(page.evaluate(async (u) => {- try {- const r = await fetch(u, { credentials: 'include', cache: 'force-cache' });- if (!r.ok) return null;- const b = await r.blob();- if (!b.size) return null;- const b64 = await new Promise((res, rej) => { const fr = new FileReader(); fr.onload = () => res(String(fr.result).split(',')[1]); fr.onerror = rej; fr.readAsDataURL(b); });- return b64 ? { b64, mime: b.type || '' } : null;- } catch (e) { return null; }- }, href), 4000, 'favicon-inpage').catch(() => null);- if (inPage && inPage.b64) {- console.log(`[overlay] favicon fetched IN-PAGE (with cookies) from ${href.slice(0, 70)}`);- return { b64: inPage.b64, mime: inPage.mime || '', href };- }- } catch (e) {}- }- for (const href of (hrefs || [])) {- try {- if (href.startsWith('data:')) {- // Inline icon — decode directly, nothing to fetch.- const m = href.match(/^data:([^;,]*)(;base64)?,(.*)$/);- if (m && m[3]) {- const b64 = m[2] ? m[3] : Buffer.from(decodeURIComponent(m[3])).toString('base64');- if (b64) return { b64, mime: m[1] || 'image/png', href: 'data:inline' };- }- continue;- }- // v1.9.204: a file:// page's favicon href is itself file:// — Node's fetch CANNOT fetch that,- // so every local-file window silently lost its icon and rendered no overlay at all (John's first- // taskbar button). The bridge runs on the SAME machine as Chrome, so just read it off disk.- if (/^file:/i.test(href)) {- try {- const fp = decodeURIComponent(new URL(href).pathname).replace(/^\/([A-Za-z]:)/, '$1');- const buf = fs.readFileSync(fp);- if (buf && buf.length) {- const ext = (fp.split('.').pop() || '').toLowerCase();- const mime = ext === 'ico' ? 'image/x-icon' : ext === 'svg' ? 'image/svg+xml' : ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png';- return { b64: buf.toString('base64'), mime, href };- }- } catch (e) {}- continue;- }- const r = await fetch(href, { redirect: 'follow', signal: AbortSignal.timeout(4000) });- if (!r || !r.ok) continue;- const buf = Buffer.from(await r.arrayBuffer());- if (!buf.length) continue;- return { b64: buf.toString('base64'), mime: r.headers.get('content-type') || '', href };- } catch (e) {}- }- return null;-} const _appOverlayCache = new Map(); // sessionId -> composited b64 // v1.9.204 (John: "the active tab in that first taskbar icon is this you fucktard" — a tab that // plainly HAS a favicon, while pup reported "no favicon on the active tab"). ROOT CAUSE: pup picked@@ -6649,8 +6510,16 @@ async function applyAppOverlay(session, sessionId) { const _dbgDir = path.join(require('os').tmpdir(), 'pup-badge-debug'); fs.mkdirSync(_dbgDir, { recursive: true }); const _dbgPath = path.join(_dbgDir, `${sessionId}.png`);- fs.writeFileSync(_dbgPath, Buffer.from(overlay, 'base64'));- console.log(`[overlay-debug] "${sessionId}" PAINT ${Buffer.from(overlay, 'base64').length}B -> ${_dbgPath} | src=${(src && src.href) || '?'} | hwnd=${session._hwnd || '(none, using title)'}`);+ const _dbgBuf = Buffer.from(overlay, 'base64');+ fs.writeFileSync(_dbgPath, _dbgBuf);+ session._overlayBytesAt = Date.now(); // v2.0.230: lets the dashboard stop stat-ing per push+ // v2.0.230: this fired on EVERY paint, including the periodic re-asserts, and was a top-3+ // source of log volume. The interesting event is a CHANGE of source, so collapse repeats.+ const _dbgSig = `${(src && src.href) || '?'}|${_dbgBuf.length}`;+ if (session._dbgLogSig !== _dbgSig) {+ session._dbgLogSig = _dbgSig;+ console.log(`[overlay-debug] "${sessionId}" PAINT ${_dbgBuf.length}B -> ${_dbgPath} | src=${(src && src.href) || '?'} | hwnd=${session._hwnd || '(none, using title)'}`);+ } } catch (e) { console.log(`[overlay-debug] dump failed: ${e.message}`); } await chrome.abCommand('desktop_taskbar', { hwnd: session._hwnd,@@ -7059,23 +6928,6 @@ function copyCaptureToClipboard(pngPath, sidecarText) { } catch (e) { resolve(false); } }); }-// In-page confirmation toast WITH a thumbnail of the ACTUAL captured PNG — the user sees their-// red circle and the grabbed margin before tabbing away.-async function showCaptureToast(pg, previewDataUrl, line1) {- try {- await pg.evaluate(function (dataUrl, txt) {- var old = document.getElementById('__pup_ann_toast'); if (old) old.remove();- var t = document.createElement('div'); t.id = '__pup_ann_toast';- t.style.cssText = 'position:fixed;left:50%;transform:translateX(-50%);bottom:18px;z-index:2147483647;background:#21262df2;border:1px solid #30363d;border-radius:12px;padding:10px;box-shadow:0 8px 30px #000c;font:12px Satoshi,system-ui,sans-serif;color:#c9d1d9;max-width:340px;cursor:pointer;';- var cap = document.createElement('div'); cap.textContent = txt; cap.style.cssText = 'margin-bottom:7px;color:#00d4cb;font-weight:500;';- t.appendChild(cap);- if (dataUrl) { var im = document.createElement('img'); im.src = dataUrl; im.style.cssText = 'display:block;max-width:320px;max-height:200px;border-radius:8px;border:1px solid #30363d;'; t.appendChild(im); }- t.onclick = function () { t.remove(); };- document.documentElement.appendChild(t);- setTimeout(function () { try { t.remove(); } catch (e) {} }, 4500);- }, previewDataUrl, line1);- } catch (e) {}-} // v2.0.53 (John repinned pup to CfT): the toolbar extension is now a LAUNCH ARG. Chrome for // Testing still honors --load-extension (branded Chrome >=137 removed it), so when the driving@@ -7275,12 +7127,6 @@ async function stageIdentityExtension() { } catch (e) {} return dstDir; }-// Chromium's unpacked-extension id: first 16 bytes of SHA-256 over the UTF-16LE path, hex mapped-// a..p. Validated against a live id 2026-08-10 (utf16le is the correct encoding on Windows).-function unpackedExtensionId(dir) {- const h = require('crypto').createHash('sha256').update(Buffer.from(dir, 'utf16le')).digest('hex').slice(0, 32);- return [...h].map(c => String.fromCharCode('a'.charCodeAt(0) + parseInt(c, 16))).join('');-} async function startAnnotation(session, sessionId, callerThread, opts) { const tab = ((opts && opts.tabId) && (session.tabs || []).find(t => t.tabId === opts.tabId)) || null; let pg = tab && tab.page;@@ -7507,53 +7353,8 @@ async function osRaiseSessionWindow(session, sessionId, reason) { } } catch (e) { console.log(`[foreground] "${sessionId}" hwnd raise threw: ${e.message}`); } return;- // v1.9.30: resolve the PID EXACTLY like the proven pup_raise_os_window handler does:- // browser.process() ONLY. For a DETACHED launch (every Windows pup browser) process() is null,- // so the PS SetForegroundWindow step is SKIPPED and the CDP place+maximize+bringToFront above- // is what shows the window — and that path demonstrably works. The old code fell back to- // _detachedPid, which made PS raise the LAUNCHER pid's MainWindowHandle: a different window- // than the session's. It reported "focused" every time while the real window stayed hidden,- // which is why the wiki toggle left John with captions and no window.- const be = browsers.get(session.profileName);- let pid = null;- try { pid = (be && be.browser && be.browser.process() && be.browser.process().pid) || null; } catch (e) {}- if (!pid) return;- const psScript = path.join(require('os').tmpdir(), '_adom_focus.ps1');- const psContent = [- 'Add-Type @"',- 'using System; using System.Runtime.InteropServices;',- 'public class WinFocus {',- ' [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);',- ' [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);',- ' [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);',- ' [DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr h, int i);',- ' [DllImport("user32.dll")] public static extern int SetWindowLong(IntPtr h, int i, int v);',- ' public static void Raise(IntPtr h){',- ' int ex = GetWindowLong(h, -20); SetWindowLong(h, -20, ex & ~0x08000000);',- ' if (IsIconic(h)) ShowWindow(h, 9); ShowWindow(h, 5); SetForegroundWindow(h);',- ' }',- '}',- '"@',- `$targetPid = ${pid}`,- '$p = Get-Process -Id $targetPid -ErrorAction SilentlyContinue',- 'if ($p -and $p.MainWindowHandle -ne 0) {',- ' [WinFocus]::Raise($p.MainWindowHandle)',- " Write-Output 'focused'",- '} else {',- ' Get-WmiObject Win32_Process | Where-Object { $_.ParentProcessId -eq $targetPid -and $_.Name -eq "chrome.exe" } | ForEach-Object {',- ' $cp = Get-Process -Id $_.ProcessId -ErrorAction SilentlyContinue',- ' if ($cp -and $cp.MainWindowHandle -ne 0) {',- ' [WinFocus]::Raise($cp.MainWindowHandle)',- " Write-Output 'focused'",- ' return',- ' }',- ' }',- '}',- ].join('\r\n');- try { fs.writeFileSync(psScript, psContent); } catch (e) { return; }- // ASYNC PS (runPsHiddenPromise) — the sync runPsHidden blocks pup's event loop for up to 8s,- // stalling every other request; the async variant frees the loop while the raise runs.- try { const r = (await runPsHiddenPromise(psScript, { timeout: 8000 })).toString().trim(); console.log(`[wiki] raised session "${sessionId}" to foreground: ${r}`); } catch (e) {}+ // v2.0.230 cleanup: ~50 lines of dead PowerShell WinFocus raise used to sit here, after an+ // unconditional return — superseded by the hwnd + desktop_bring_to_front path above. Deleted. } catch (e) {} } // ── v1.9.31: TAB DRAGGED OUT of a pup window → adopt it as its OWN session ──────────────@@ -9107,7 +8908,7 @@ async function parkSessionWindowDirect(page, sessionId) { let hwnd = null; try { const r = await chrome.abCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 4000 , caller: sessionCaller(sessions.get(sessionId))});- const o = r && (typeof r.output === 'string' ? JSON.parse(r.output) : r.output || r);+ const o = r && abPayload(r); hwnd = (o && ((o.best && o.best.hwnd) || o.hwnd)) || null; } catch (e) {} {@@ -9131,7 +8932,7 @@ async function parkSessionWindowDirect(page, sessionId) { if (!parkHwnd) { try { const r = await chrome.abCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 4000 , caller: sessionCaller(sessions.get(sessionId))});- const o = r && (typeof r.output === 'string' ? JSON.parse(r.output) : r.output || r);+ const o = r && abPayload(r); parkHwnd = (o && ((o.best && o.best.hwnd) || o.hwnd)) || null; } catch (e) {} }@@ -11635,7 +11436,10 @@ async function dashboardSnapshot() { identityPendingMs: sess._nudgePending ? Date.now() - sess._nudgePending.since : 0, 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; } })();+ // v2.0.230: this ran a SYNC stat per session on every 2s dashboard push. The overlay+ // compositor already records whether it wrote bytes for this session, so trust the flag and+ // only touch the disk when it has never painted (the rare, genuinely-unknown case).+ const hasBytes = (sess._overlayBytesAt ? true : (() => { try { return fs.existsSync(path.join(os.tmpdir(), 'pup-badge-debug', sid + '.png')); } catch (e) { return false; } })()); if (!painted || !hasBytes) return 'red'; return sess._overlayIsGeneric ? 'amber' : 'green'; })(),@@ -12888,6 +12692,9 @@ const server = http.createServer(async (req, res) => { res.end(buf); return; } catch (e) { res.writeHead(404); res.end(); return; } }+ // v2.0.230: the dashboard re-renders every 2s and emits FRESH <img> elements, so these endpoints+ // were doing a synchronous file read + full ICO decode per window per 2 seconds. Cache the+ // decoded bytes keyed on path+mtime; the art only changes when a tile is re-rendered. if (pathname === '/dashboard/stampicon') { // v2.0.210 (John: "why aren't your dashboard icons consistent with the settings?"): the strip // must show what the button ACTUALLY wears. In tiles mode that is the THREAD TILE, not the@@ -12897,15 +12704,17 @@ const server = http.createServer(async (req, res) => { const _s0 = sessions.get(_sid0); if (pupSettings().aumidIcons === 'on' && pupSettings().threadTiles === 'on' && _s0 && (_s0.owner || _s0._ownerThread)) { const _tp0 = path.join(THREAD_TILE_DIR, `${threadSlug(_s0.owner || _s0._ownerThread)}.ico`);- const _inner0 = icoLargestPng(fs.readFileSync(_tp0));- if (_inner0) { res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'no-store' }); res.end(_inner0); return; }+ const _inner0 = cachedIconPng(_tp0, icoLargestPng);+ // mtime-keyed cache upstream, so the browser may cache too — a re-render of the same tile+ // is byte-identical, and a NEW tile lands under a new mtime (and a fresh ?v= from the page).+ if (_inner0) { res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=60' }); res.end(_inner0); return; } } } catch (e) {} // v1.9.246 (John's audit): a STAMPED window's real taskbar button shows the icon the stamp // registered (pupIconPath's category tile), NOT the Chrome exe icon - the strip must match. try { const sid = url.searchParams.get('session') || '';- const inner = icoLargestPng(fs.readFileSync(pupIconPath(sid)));+ const inner = cachedIconPng(pupIconPath(sid), icoLargestPng); if (!inner) { res.writeHead(404); res.end(); return; } res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=600' }); res.end(inner); return;@@ -15490,7 +15299,7 @@ const server = http.createServer(async (req, res) => { // v1.8.43: route the FLASH through AD's desktop_flash_window (reliable, native). const r = await flashViaAD(session._sessionId || sessionId, { stop }); let applied = false;- try { const o = r && (typeof r.output === 'string' ? JSON.parse(r.output) : r.output || r); applied = !!(o && (o.ok || o.count)); } catch (e) {}+ try { const o = r && abPayload(r); applied = !!(o && (o.ok || o.count)); } catch (e) {} // v1.8.45: FLASHW_STOP only stops the PULSE — the Win11 attention tint persists // until the window is ACTIVATED. So on {stop} we ALSO clear the tint for real via an // off-screen activate-and-restore (no pop, focus handed straight back). This is the@@ -18484,47 +18293,46 @@ setInterval(async () => { // identity stamping is unsupported) and heartbeats so a dead sweep is visible in the log. global.__floatyHb = (global.__floatyHb || 0) + 1; if (global.__floatyHb % 15 === 1) console.log(`[floaty] sweep heartbeat #${global.__floatyHb} (${sessions.size} session(s), identitySupported=${_identitySupported})`);+ // v2.0.230 (John: "find code that loops way too much and chews up wasteful cpu"). This sweep used+ // to make THREE CDP round-trips per tab every 4s: harvest the pencil click, harvest a captured+ // login, and run a title-cleanup blob that was a measured NO-OP (both call sites pass an empty+ // owner, so the in-page code returns immediately). At ~10 tabs that was 7.5 renderer wakeups per+ // second, forever, on an idle machine. Now: ONE evaluate per tab returning both harvests, the+ // dead title sweep deleted, and tabs are skipped entirely when neither feature can fire. try {+ const _capOn = pupSettings().captureLogins !== 'off'; for (const [sidF, sF] of [...sessions]) {- if (sF._lostBrowser || !(sF.tabs || []).length || sF._annotateArmed && Date.now() - sF._annotateArmed < 4000) continue;+ if (sF._lostBrowser || !(sF.tabs || []).length) continue;+ if (sF._annotateArmed && Date.now() - sF._annotateArmed < 4000) continue; for (const tF of (sF.tabs || [])) { if (!tF || !tF.page) continue;- let clickedF = null;- try { clickedF = await Promise.race([tF.page.evaluate(() => { const c = window.__pupFloatyClick; window.__pupFloatyClick = null; return c || null; }), new Promise(r => setTimeout(() => r(null), 2500))]); } catch (e) {}- if (clickedF) { sF._annotateArmed = Date.now(); console.log(`[floaty] "${sidF}" pencil clicked — arming capture watcher on ${tF.tabId}`); startAnnotation(sF, sidF, 'user-floaty', { tool: 'pen', tabId: tF.tabId }).catch(() => {}); break; }- }- }- } catch (e) {}- // v2.0.110: login-capture harvest — read-and-clear any credential the capture rail recorded on- // submit, store it in the OS keychain vault, and stage a hint for the owning thread's next- // response. The password is handed straight to keytar and never logged (we log host+user only).- try {- if (pupSettings().captureLogins === 'off') throw 0;- for (const [sidC, sC] of [...sessions]) {- if (sC._lostBrowser || !(sC.tabs || []).length) continue;- for (const tC of (sC.tabs || [])) {- if (!tC || !tC.page) continue;- let cap = null;- try { cap = await Promise.race([tC.page.evaluate(() => { const c = window.__pupCapturedLogin; window.__pupCapturedLogin = null; return c || null; }), new Promise(r => setTimeout(() => r(null), 2000))]); } catch (e) {}- if (cap && cap.host && cap.p) {+ let harvest = null;+ try {+ harvest = await Promise.race([+ tF.page.evaluate((wantCap) => {+ const out = {};+ const c = window.__pupFloatyClick; if (c) { window.__pupFloatyClick = null; out.click = c; }+ if (wantCap) { const l = window.__pupCapturedLogin; if (l) { window.__pupCapturedLogin = null; out.login = l; } }+ return (out.click || out.login) ? out : null;+ }, _capOn),+ new Promise(r => setTimeout(() => r(null), 2500)),+ ]);+ } catch (e) {}+ if (!harvest) continue;+ if (harvest.login && harvest.login.host && harvest.login.p) {+ const cap = harvest.login; try { await credentialVault.setCredential({ host: cap.host, username: cap.u || '', password: cap.p });- console.log(`[cred] captured login for ${cap.host}${cap.u ? ` (${cap.u})` : ''} on "${sidC}" → keychain vault`);- sC._pendingCredHint = `pup CAPTURED this login for "${cap.host}"${cap.u ? ` (username ${cap.u})` : ''} into the OS keychain (encrypted at rest; never logged, never echoed). Next visit auto-fills and signs in with no Chrome dropdown and no foregrounding. This is how the shared adom-you identity grows — every login you do makes future ones automatic. Manage: pup_credentials {} to list, credential_delete {host} to remove, pup_configure {captureLogins:"off"} to stop capturing.`;+ console.log(`[cred] captured login for ${cap.host}${cap.u ? ` (${cap.u})` : ''} on "${sidF}" → keychain vault`);+ sF._pendingCredHint = `pup CAPTURED this login for "${cap.host}"${cap.u ? ` (username ${cap.u})` : ''} into the OS keychain (encrypted at rest; never logged, never echoed). Next visit auto-fills and signs in with no Chrome dropdown and no foregrounding. This is how the shared adom-you identity grows — every login you do makes future ones automatic. Manage: pup_credentials {} to list, credential_delete {host} to remove, pup_configure {captureLogins:"off"} to stop capturing.`; } catch (e) { console.log(`[cred] capture store failed for ${cap.host}: ${e.message}`); } }- }- }- } catch (e) {}- // v2.0.102: title CLEANUP sweep (the 2.0.98 owner re-assert inverted). Strips the prefix any- // elder keeper still applies on pages the attach-time cleanup missed. Idempotent via guard.- try {- const tjClean2 = titleJs('');- for (const [, sT] of [...sessions]) {- if (sT._lostBrowser) continue;- for (const tT of (sT.tabs || [])) {- if (!tT || !tT.page) continue;- try { await Promise.race([tT.page.evaluate(tjClean2), new Promise(r => setTimeout(r, 1500))]); } catch (e) {}+ if (harvest.click) {+ sF._annotateArmed = Date.now();+ console.log(`[floaty] "${sidF}" pencil clicked — arming capture watcher on ${tF.tabId}`);+ startAnnotation(sF, sidF, 'user-floaty', { tool: 'pen', tabId: tF.tabId }).catch(() => {});+ break;+ } } } } catch (e) {}@@ -18545,7 +18353,11 @@ setInterval(async () => { // silently clear it. Every 60s per window, void the paint memory so the block below // repaints unconditionally — one cheap ab call per window per minute buys "the badge you // see is the badge we believe", instead of memory drifting from the screen forever.- if (s2._appOverlay && Date.now() - (s2._overlayReassertAt || 0) > 60000) {+ // v2.0.230: was every 60s per window — one ab/PowerShell taskbar round-trip per window per+ // minute, forever, purely as a safety net against shell-side clears. Stretched to 5 minutes:+ // the real clears (button rebuild, progress bar, TaskbarCreated) all trigger a repaint by+ // EVENT already, so this only has to catch the exotic case, and it can be lazy about it.+ if (s2._appOverlay && Date.now() - (s2._overlayReassertAt || 0) > 300000) { s2._overlayReassertAt = Date.now(); s2._lastPaintSig = null; s2._appOverlay = false; s2._overlayReassertQuiet = true; // v2.0.48: periodic re-assert, not a real bare-heal — log quietly@@ -18581,7 +18393,7 @@ setInterval(async () => { // bare-heal loudly, and the periodic re-assert only once per ~10 min per window. if (s2._overlayReassertQuiet) { s2._overlayReassertQuiet = false;- if (!s2._reassertLogAt || Date.now() - s2._reassertLogAt > 600000) { s2._reassertLogAt = Date.now(); console.log(`[overlay] "${sid2}" periodic ground-truth re-assert (60s cycle; logged at most every 10 min)`); }+ if (!s2._reassertLogAt || Date.now() - s2._reassertLogAt > 600000) { s2._reassertLogAt = Date.now(); console.log(`[overlay] "${sid2}" periodic ground-truth re-assert (5 min cycle; logged at most every 10 min)`); } } else console.log(`[overlay] "${sid2}" ENFORCED (was bare) — every pup window must wear its overlay`); } }).catch(() => {});@@ -18685,13 +18497,22 @@ setInterval(async () => { // just reconnected; then a slow periodic net. Spares every live session BY PROFILE, so it can only // ever hit true orphans — never a session's own renderer children. setTimeout(() => { reapOrphanPupChromes().catch(() => {}); }, 15000);- setInterval(() => { reapOrphanPupChromes().catch(() => {}); }, 180000);+ // v2.0.230: skip the PowerShell process enumeration entirely when there is nothing that could+ // have leaked (no live sessions AND no session files on disk) — it was ~20 PowerShell spawns an+ // hour on a totally idle machine.+ setInterval(() => {+ if (!sessions.size) { try { if (!listSessionFiles().length) return; } catch (e) {} }+ reapOrphanPupChromes().catch(() => {});+ }, 180000); // v2.0.129: reap LEAKED isolated profile DIRECTORIES (iso-* throwaways whose window is long gone). // Boot pass waits 60s so session recovery has fully settled first (never delete a profile an iso // session is mid-recovery on); then a slow 15-min net drains the backlog a few at a time. setTimeout(() => { reapLeakedIsolatedProfiles().catch(() => {}); }, 60000);- setInterval(() => { reapLeakedIsolatedProfiles().catch(() => {}); }, 15 * 60 * 1000);+ setInterval(() => {+ if (!sessions.size) { try { if (!listSessionFiles().length) return; } catch (e) {} }+ reapLeakedIsolatedProfiles().catch(() => {});+ }, 15 * 60 * 1000); // Periodic cleanup: every 5 minutes, remove stale session files with dead PIDs setInterval(async () => {@@ -19371,7 +19192,10 @@ async function tabRecordStartImpl({ console.log(`[${recordingId}] FIRST screencast frame (${buf.length} bytes)`); } const fname = `f${String(state.frameCount).padStart(6, '0')}.jpg`;- try { fs.writeFileSync(path.join(state.framesDir, fname), buf); }+ try { // v2.0.230: was writeFileSync at up to 60fps — every frame blocked the event loop and stalled+ // every other session's CDP traffic for the length of a recording. The ack below is the+ // backpressure, so the write can be async.+ fs.promises.writeFile(path.join(state.framesDir, fname), buf).catch(() => {}); } catch (e) { console.error(`[${recordingId}] frame write failed: ${e.message}`); } // ACK so Chrome continues sending. Without this, only frame 1 arrives. try { await cdp.send('Page.screencastFrameAck', { sessionId: ackId }); }