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.228: Taskbar icon drift now heals itself: a new audit compares pup's model against the shell's own button names, classifies every drift we have shipped (no button, pending rebuild, and art the shell baked before the thread tile existed), repairs each by re-registering and re-stamping and rebuilding the button, then repaints the badges; it runs after every mode switch and brand sweep, twelve seconds after a thread tile first renders, and every four minutes in steady state, and pup_audit_icons exposes it on demand.
4 files changed
+143−7
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.227",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.227/adom-bridge-puppeteer-v2.0.227.zip",- "sha256": "8b5611b3be56a59a22b1a58d169bde7ed2128766ad5888251b51a4fa61774bab",- "size": 943878,+ "version": "2.0.228",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.228/adom-bridge-puppeteer-v2.0.228.zip",+ "sha256": "539166554327804d4808917bbeed6a64995acad56ae30a9eecf434b835621740",+ "size": 946876, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-08-18T10:18:59.867486Z"+ "released_at": "2026-08-18T10:38:56.341060Z" }
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.227+2.0.228
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.227",+ "version": "2.0.228", "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+136@@ -4424,6 +4424,10 @@ const _threadTileInflight = new Map(); // name -> Promise (dedupe concurrent r async function renderThreadTileIco(page, name) { const icoPath = path.join(THREAD_TILE_DIR, `${threadSlug(name)}.ico`); try { if (fs.existsSync(icoPath) && fs.statSync(icoPath).size > 100) return icoPath; } catch (e) {}+ // v2.0.228: a FIRST render means every button of this thread was built before its art existed+ // (Windows bakes a button's icon at creation) — audit shortly after so those get rebuilt. This is+ // the exact defect John's audit found on "ah build thread".+ scheduleIconAudit(`tile rendered for "${name}"`, 12000); if (_threadTileInflight.has(name)) return _threadTileInflight.get(name); const job = (async () => { if (!page) { console.log(`[thread-tile] "${name}" NO page passed`); return null; }@@ -4761,6 +4765,7 @@ public static class SW { console.log(`[thread-tile] "${sessionId}" nudge still skipped after ${NUDGE_RETRY_DELAYS.length} retries — the window has been focused throughout; leaving it pending for the next stamp`); } }+ if (!err && r === 'NUDGED' && _sk) { _sk._buttonBuiltAt = Date.now(); } // v2.0.228: stale-art anchor if (!err && r === 'NUDGED' && _sk && _sk._nudgePending) { console.log(`[thread-tile] "${sessionId}" deferred nudge LANDED after ${_sk._nudgePending.attempts} retr(ies) — button rebuilt with its real identity`); _sk._nudgePending = null;@@ -4885,8 +4890,117 @@ function _startModeApply(val) { } if (_modeApply !== my) return; my.steps[2].state = 'done'; my.done = true;+ scheduleIconAudit('after mode switch'); // v2.0.228: self-heal whatever the switch left crooked })(); }+// v2.0.228 — THE TASKBAR ICON AUDIT (John, 2026-08-18: "whenever i ask you to audit your taskbar+// icons ... i mean detect whether you're off on your aumid tracking, find out why, fix your code,+// then reapply all aumid icons. the goal is that i never have to ask you to audit again").+// So the audit is not a report — it is a SELF-HEAL, and it runs on its own. It compares pup's model+// against the SHELL'S OWN button names (UIA ground truth, the only thing that cannot lie), finds+// every drift class we have ever shipped, repairs each, and re-verifies. Companion dev skill:+// dev-skills/pup-icon-audit.+let _lastAudit = null;+let _auditBusy = false;+async function taskbarButtonNames() {+ if (process.platform !== 'win32') return [];+ const ps = `Add-Type -AssemblyName UIAutomationClient+$bars = [System.Windows.Automation.AutomationElement]::RootElement.FindAll([System.Windows.Automation.TreeScope]::Children, (New-Object System.Windows.Automation.OrCondition((New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ClassNameProperty,'Shell_TrayWnd')), (New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ClassNameProperty,'Shell_SecondaryTrayWnd')))))+foreach($bar in $bars){ $btns=$bar.FindAll([System.Windows.Automation.TreeScope]::Descendants,(New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ControlTypeProperty,[System.Windows.Automation.ControlType]::Button))); foreach($b in $btns){ Write-Output $b.Current.Name } }`;+ return await new Promise((resolve) => {+ try {+ execFile('powershell', ['-NoProfile', '-Command', ps], { windowsHide: true, timeout: 25000, maxBuffer: 1 << 20 }, (err, stdout) => {+ resolve(err ? [] : String(stdout || '').split(/\r?\n/).map(x => x.trim()).filter(Boolean));+ });+ } catch (e) { resolve([]); }+ });+}+async function auditTaskbarIcons(reason = 'manual', opts = {}) {+ const fix = opts.fix !== false;+ if (process.platform !== 'win32') return { ok: true, skipped: 'not win32' };+ if (_auditBusy) return _lastAudit || { ok: true, skipped: 'audit already running' };+ _auditBusy = true;+ try {+ const st = pupSettings();+ const mode = st.aumidIcons !== 'on' ? 'plain' : (st.threadTiles === 'on' ? 'tiles' : 'aumid');+ const names = await taskbarButtonNames();+ const findings = [];+ const live = [...sessions].filter(([sid, s]) => s && sid !== 'pup-dashboard');+ if (!names.length) { const r = { ok: true, mode, reason, skipped: 'UIA returned no buttons (locked screen / no session)', at: Date.now() }; _lastAudit = r; return r; }+ for (const [sid, sess] of live) {+ const thread = sess.owner || sess._ownerThread || '';+ if (mode === 'plain') {+ // plain: NOTHING of pup may appear as a button, and every window rides the stock group+ if (thread && names.some(n => n.startsWith(thread + ' ')) || names.some(n => n.startsWith('Adom Pup'))) {+ findings.push({ sid, thread, kind: 'leftover_identity', why: 'plain mode but a pup-branded button is still on the bar' });+ }+ continue;+ }+ const expect = mode === 'tiles' && thread ? thread : 'Adom Pup';+ const hasButton = names.some(n => n.startsWith(expect));+ if (!hasButton) {+ findings.push({ sid, thread, kind: 'no_button', why: `no taskbar button starts with "${expect}" — the identity never reached the shell (window grouped under the stock Chrome button)` });+ continue;+ }+ if (sess._nudgePending) {+ findings.push({ sid, thread, kind: 'nudge_pending', why: 'identity stamped but the button rebuild is still queued (window keeps being focused)' });+ continue;+ }+ // STALE ART: the button exists with the right NAME, but Windows baked its icon when the+ // button was created — if the icon file was written AFTER that, the glass shows old art.+ if (mode === 'tiles' && thread) {+ try {+ const ico = path.join(THREAD_TILE_DIR, `${threadSlug(thread)}.ico`);+ const mt = fs.existsSync(ico) ? fs.statSync(ico).mtimeMs : 0;+ const built = sess._buttonBuiltAt || 0;+ if (mt && built && mt > built + 1500) {+ findings.push({ sid, thread, kind: 'stale_art', why: `the tile icon was written ${Math.round((mt - built) / 1000)}s AFTER this button was last rebuilt — the shell is showing the icon it baked at creation` });+ } else if (mt && !built) {+ findings.push({ sid, thread, kind: 'unverified_art', why: 'this button has never been rebuilt by pup since the tile was rendered — cannot vouch for the art on glass' });+ }+ } catch (e) {}+ }+ }+ let repaired = 0;+ if (fix && findings.length) {+ console.log(`[icon-audit] (${reason}) ${findings.length} finding(s): ${findings.map(f => f.sid + ':' + f.kind).join(', ')} — REPAIRING`);+ // force ab to re-read every icon: bump the registration epoch so cacheKeys change+ try { savePupSettings({ _stripEpoch: Number(pupSettings()._stripEpoch || 0) + 1 }); } catch (e) {}+ for (const f of findings) {+ const sess = sessions.get(f.sid);+ if (!sess) continue;+ try {+ if (f.kind === 'leftover_identity') { await stripAllPupIdentities('icon-audit repair (plain)'); repaired++; break; }+ // re-register (fresh icon handle) + re-stamp + rebuild the button+ const appId = sess._curAppId;+ if (appId) { try { _registeredPupAumids.delete(appId); } catch (e) {} }+ sess._curAppId = null; sess._frozenAppId = null;+ await stampPupIdentity(f.sid, { _commit: true });+ if (!sess._hwnd) { try { await resolveSessionHwndByBounds(sess, f.sid); } catch (e) {} }+ if (sess._hwnd) nudgeTaskbarButton(sess._hwnd, f.sid);+ sess._appOverlay = false; sess._lastPaintSig = null;+ repaired++;+ } catch (e) { console.log(`[icon-audit] repair of "${f.sid}" failed: ${e && e.message}`); }+ }+ // let the shell settle, then repaint badges onto the rebuilt buttons+ setTimeout(() => { for (const [sid2, s2] of [...sessions]) { try { applyAppOverlay(s2, sid2).catch(() => {}); } catch (e) {} } }, 2500);+ }+ const result = { ok: findings.length === 0, mode, reason, at: Date.now(), buttons: names.length,+ sessions: live.length, findings, repaired, fixed: fix };+ if (!findings.length) console.log(`[icon-audit] (${reason}) CLEAN — ${live.length} session(s) all match the shell in ${mode} mode`);+ _lastAudit = result;+ return result;+ } finally { _auditBusy = false; }+}+// Self-healing cadence: a quiet steady-state check every 4 minutes, plus a pass ~25s after any+// identity churn settles (that is when the classic "button baked before the icon existed" lands).+function scheduleIconAudit(reason, delayMs = 25000) {+ try { if (global.__auditTimer) clearTimeout(global.__auditTimer); } catch (e) {}+ global.__auditTimer = setTimeout(() => { auditTaskbarIcons(reason).catch(() => {}); }, delayMs);+}+if (process.platform === 'win32') {+ setInterval(() => { if (sessions.size) auditTaskbarIcons('steady-state').catch(() => {}); }, 4 * 60 * 1000);+} async function refreshAllJumplists() { if (process.platform !== 'win32' || _jumplistSupported === false) return; const off = pupSettings().jumpLists !== 'on';@@ -5549,6 +5663,7 @@ async function brandSweep(reason) { } catch (e) {} if (process.platform !== 'win32') return;+ scheduleIconAudit('after brandSweep: ' + reason); let n = 0; for (const [sid, sess] of sessions) { if (_sweepGen !== _identityGen) { console.log(`[identity] brandSweep(${reason}) ABORTED mid-loop (superseded by a newer mode switch)`); break; }@@ -7729,6 +7844,17 @@ async function windowTarget(session, sessionId) { if (session && !session._hwnd) { await resolveSessionHwndByBounds(session, sessionId); } if (session && session._hwnd && await hwndBelongsToPup(session._hwnd, session)) return { hwnd: Number(session._hwnd) }; } catch (e) {}+ // v2.0.228: titleTag defaults OFF (since the 2.0.20 hotfix), so this fallback CANNOT match any+ // window — it produced the mystifying 'No visible window with title containing "(session: X)"'+ // that broke pup_lower_os_window during the icon audit. Say so out loud once per session instead+ // of letting the caller stare at an impossible-looking lookup.+ if (pupSettings().titleTag !== 'on') {+ const _s = sessions.get(sessionId);+ if (_s && !_s._titleFallbackWarned) {+ _s._titleFallbackWarned = true;+ console.log(`[windowTarget] "${sessionId}" has NO hwnd and titleTag is OFF — falling back to a title lookup that cannot match; the window op will fail. Root cause is the missing handle, not the title.`);+ }+ } return { titleContains: `(${TITLE_LABEL}: ${sessionId}` }; } async function resolveSessionHwndByBounds(session, sessionId) {@@ -17497,6 +17623,15 @@ const server = http.createServer(async (req, res) => { }); return; }+ case 'pup_audit_icons': {+ // v2.0.228: detect drift between pup's model and the SHELL's own buttons, repair it, and+ // report what was wrong. fix:false makes it a dry run.+ const _r = await auditTaskbarIcons(args.reason || 'pup_audit_icons verb', { fix: args.fix !== false });+ sendJSON(res, { success: true, audit: _r,+ _hint: _r.ok ? 'Every live window matches the shell in this mode.'+ : `${_r.findings.length} drift finding(s), ${_r.repaired} repaired — re-run in ~30s to confirm the glass settled.` });+ return;+ } case 'pup_status': { // v1.9.172 (jam fix): same resilience as pup_list_windows — parallel + per-session // timeout + in-memory fallback, so a busy window can never hang pup_status into null.@@ -17511,6 +17646,7 @@ const server = http.createServer(async (req, res) => { // them on the HTTP /status chip only — the wrong surface for remote no-shell boxes). version: BRIDGE_VERSION, enumWindows: { total: _enumCalls, lastMin: (Date.now() - _enumWindow.at < 60000 ? _enumWindow.n : 0) },+ iconAudit: _lastAudit ? { ok: _lastAudit.ok, at: _lastAudit.at, mode: _lastAudit.mode, findings: _lastAudit.findings.length, repaired: _lastAudit.repaired, reason: _lastAudit.reason } : null, settings: (() => { const st = pupSettings(); return { taskbarMode: st.aumidIcons !== 'on' ? 'plain' : (st.threadTiles === 'on' ? 'tiles' : 'aumid'), aumidIcons: st.aumidIcons, threadTiles: st.threadTiles,