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.289: Taskbar PROGRESS BARS instead of the orange flash (John): determinate bar on a window's taskbar button sized to the running-average duration of the operation (persisted per-op timing). Built-in for icon-apply (open until the icon lands) and screenshots; withTaskbarProgress() wraps any op. New setting taskbarActivity=progress|flash|off (default progress) gates the auto-flash. New skill pup-taskbar-progress.
4 files changed
+105−9
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.288",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.288/adom-bridge-puppeteer-v2.0.288.zip",- "sha256": "6eba77915f65a9c42756e34218aec61089dbab38ade47f348b109bf33b725f1f",- "size": 968770,+ "version": "2.0.289",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.289/adom-bridge-puppeteer-v2.0.289.zip",+ "sha256": "f62aa77cd9ee41deba30818d5b928dd06902df8d666cc828a0a9dea9ea8442bd",+ "size": 971202, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-08-22T22:33:20.277111Z"+ "released_at": "2026-08-22T23:04:11.887313Z" }
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.288+2.0.289
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.288",+ "version": "2.0.289", "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+98−2@@ -3579,6 +3579,11 @@ function pupSettings() { // on via the !== 'off' checks, but the dialog rendered the unset key as an OFF toggle: a display // lie). Legacy keys get explicit defaults so the dialog reflects actual behavior. D.autoLogin = 'on'; D.reportIssues = 'on'; D.agentActivity = 'on';+ // v2.0.289 (John): how pup signals activity on a window's taskbar button. 'progress' = a DETERMINATE+ // progress bar sized to the running-average time of the operation (icon-apply, screenshot, ...) —+ // the new default, replacing the orange flash. 'flash' = the old orange taskbar highlight+ // (openFlash/autoFlash). 'off' = neither. Windows-only; stored-but-inert elsewhere.+ D.taskbarActivity = 'progress'; // v1.9.275 (John: 'shouldn't there be a setting for whether to show the full jump list or not?') D.jlViewTasks = 'on'; D.jlCloseAll = 'on'; // v2.0.42 (John: "can you add a jump list to open the pup dashboard?")@@ -4288,6 +4293,7 @@ async function stampPupIdentity(sessionId, opts) { if (!ok) console.log(`[identity] "${sessionId}" stamp NOT applied (appId=${appId}): ${(raw || '').slice(0, 140)}`); if (ok) { console.log(`[identity] "${sessionId}" stamped appId=${appId}: ${(data.applied || []).join(',') || 'ok'}`);+ try { markIconApplied(sessionId); } catch (e) {} // v2.0.289: end the icon-apply taskbar progress bar if (!sess) _earlyStamped.set(sessionId, appId); // v2.0.25: pre-registration stamp — see above if (sess && prevAppId !== appId) { _stampLandedThisBoot = true;@@ -5697,6 +5703,83 @@ async function setSessionProgress(sessionId, busy) { // may be gone: void the paint memory and let the 4s enforcer sweep repaint (dedup makes it cheap). try { const _s3 = sessions.get(sessionId); if (_s3) { _s3._lastPaintSig = null; _s3._appOverlay = false; _s3._overlayEnforceAt = 0; } } catch (e) {} }+// ============================================================================+// v2.0.289 VERB-TIMING METRICS + DETERMINATE TASKBAR PROGRESS (John's request: replace the orange+// flash with a taskbar PROGRESS BAR sized to how long the operation usually takes). Keep a running+// average per operation, fill the bar over that estimate, start at the EARLIEST point and end at the+// LATEST. Wired into icon-application (until the taskbar icon actually lands) and screenshots; any+// call site can wrap work with withTaskbarProgress(). Setting: taskbarActivity = progress|flash|off.+// ----------------------------------------------------------------------------+const VERB_TIMING_FILE = path.join(require('os').homedir(), '.adom', 'pup-verb-timing.json');+let _verbTiming = {}; // opName -> { n, avgMs } — persisted so the averages survive restarts+try { _verbTiming = JSON.parse(fs.readFileSync(VERB_TIMING_FILE, 'utf8')) || {}; } catch (e) {}+let _verbTimingDirty = false;+function recordTiming(opName, ms) {+ if (!opName || !(ms >= 0) || ms > 120000) return;+ const cur = _verbTiming[opName] || { n: 0, avgMs: ms };+ cur.avgMs = cur.n === 0 ? Math.round(ms) : Math.round(cur.avgMs * 0.75 + ms * 0.25); // weight recent runs+ cur.n = Math.min((cur.n || 0) + 1, 100000);+ _verbTiming[opName] = cur; _verbTimingDirty = true;+}+setInterval(() => {+ if (!_verbTimingDirty) return; _verbTimingDirty = false;+ try { fs.mkdirSync(path.dirname(VERB_TIMING_FILE), { recursive: true }); fs.writeFileSync(VERB_TIMING_FILE, JSON.stringify(_verbTiming)); } catch (e) {}+}, 30000);+const _DEFAULT_EST_MS = { 'icon-apply': 2200, 'screenshot': 900, 'navigate': 2500, 'open': 3200 };+function estimateMs(opName) { const t = _verbTiming[opName]; return (t && t.avgMs) || _DEFAULT_EST_MS[opName] || 1200; }+function taskbarActivityMode() { const m = pupSettings().taskbarActivity; return (m === 'flash' || m === 'off') ? m : 'progress'; }+async function _pushProgress(target, value) { try { await chrome.abCommand('desktop_taskbar', { ...target, progress: value == null ? { state: 'none' } : { state: 'normal', value } }, { timeoutMs: 3000 }); } catch (e) {} }+// A determinate bar that fills over the running-average estimate for `opName`, held ON for the life of+// `work`, then completed and cleared. Records the real duration to sharpen the estimate. No-op unless+// the setting is 'progress'. The bar caps at 92% until the work actually finishes so it never lies.+async function withTaskbarProgress(sessionId, opName, work) {+ const runOne = () => (typeof work === 'function' ? work() : work);+ if (process.platform !== 'win32' || taskbarActivityMode() !== 'progress' || minimalTouch()) return runOne();+ const sess = sessions.get(sessionId);+ const target = sess && await windowTarget(sess, sessionId).catch(() => null);+ if (!target || !target.hwnd) return runOne(); // no handle yet — nothing to paint on+ const est = estimateMs(opName), started = Date.now(); let done = false;+ await _pushProgress(target, 2);+ const timer = setInterval(() => { if (done) return; const f = Math.min(0.92, (Date.now() - started) / Math.max(200, est)); _pushProgress(target, Math.max(2, Math.round(f * 100))).catch(() => {}); }, 180);+ try { return await runOne(); }+ finally {+ done = true; clearInterval(timer); recordTiming(opName, Date.now() - started);+ await _pushProgress(target, 100);+ setTimeout(() => { _pushProgress(target, null).catch(() => {}); try { if (sess) { sess._lastPaintSig = null; sess._appOverlay = false; sess._overlayEnforceAt = 0; } } catch (e) {} }, 220);+ }+}+// ICON-APPLY progress: a window's taskbar icon is applied ASYNC (birth -> hwnd -> stamp -> tile render+// -> nudge). Show a determinate bar the whole time — start at open (EARLIEST), end when the icon+// actually lands (LATEST) — timed to the running average of past icon-applies. Self-contained: a poll+// resolves the hwnd once it exists, then fills the bar until markIconApplied() fires or it times out.+const _iconApplyRunning = new Set();+function startIconApplyProgress(sessionId) {+ if (process.platform !== 'win32' || taskbarActivityMode() !== 'progress' || minimalTouch()) return;+ if (_iconApplyRunning.has(sessionId)) return;+ _iconApplyRunning.add(sessionId);+ const est = estimateMs('icon-apply'), started = Date.now();+ (async () => {+ let target = null;+ try {+ for (let i = 0; i < 40; i++) { // up to ~8s: wait for the handle, then fill the bar+ const sess = sessions.get(sessionId);+ if (!sess) break;+ if (sess._iconApplied) break;+ if (!target) target = await windowTarget(sess, sessionId).catch(() => null);+ if (target && target.hwnd) {+ const f = Math.min(0.92, (Date.now() - started) / Math.max(200, est));+ await _pushProgress(target, Math.max(2, Math.round(f * 100)));+ }+ await new Promise(r => setTimeout(r, 200));+ }+ } catch (e) {}+ if (target && target.hwnd) { await _pushProgress(target, 100); setTimeout(() => _pushProgress(target, null).catch(() => {}), 220); }+ recordTiming('icon-apply', Date.now() - started);+ _iconApplyRunning.delete(sessionId);+ })().catch(() => { _iconApplyRunning.delete(sessionId); });+}+function markIconApplied(sessionId) { const s = sessions.get(sessionId); if (s) s._iconApplied = true; }+ // v1.9.166 (John, repeatedly: 'use an AD toast to get a human's attention'). Whenever pup FOREGROUNDS // a window (a reveal / demo / 'show me'), fire an AD toast FIRST so the user's eyes are drawn to what // just appeared — never silently push a window onto their screen. Best-effort; debounced per session.@@ -9604,6 +9687,11 @@ async function flashViaAD(sessionId, opts) { if (minimalTouch() && !(opts && opts.force)) return { _skipped: 'minimal-touch' }; opts = opts || {}; if (process.platform !== 'win32' || !sessionId) return null;+ // v2.0.289 (John): taskbarActivity governs the orange flash. When it is NOT 'flash' (default+ // 'progress', or 'off'), suppress AUTOMATIC flashes — the determinate progress bar replaces them. A+ // STOP always runs (so a leftover flash can be cleared) and an explicit user-clicked flash+ // (opts.force) still goes through; only pup's own auto-flashes are gated.+ if (!opts.stop && !opts.force && taskbarActivityMode() !== 'flash') return { _skipped: 'taskbar-activity-not-flash' }; // v1.9.242 (John: "i just hit 'clear flashes' and it didn't clear all your flashes"). Same disease // as the badge saga: targeting by TITLE tag silently no-ops on any window whose title can't carry // it (PDF viewer, retitled pages) — and FlashWindowEx leaves the button highlighted until a STOP@@ -10838,7 +10926,11 @@ async function launchSession(sessionId, url, { freshProfile = false, profile, st // via sessions.get() at flash time always missed — single-tab windows read flash:0 while the // orange really fired (John's audit column caught it). Credit it here, where the session exists. if (_flashState.has(sessionId) && Date.now() - _flashState.get(sessionId) < 60000) session._flashCount = (session._flashCount || 0) + 1;+ session._iconApplied = false; // v2.0.289: reset so the icon-apply progress bar runs for this open sessions.set(sessionId, session);+ // v2.0.289 (John): show a determinate taskbar progress bar from window open until its taskbar icon+ // actually lands (the async birth->hwnd->stamp->tile->nudge flow), sized to the running average.+ try { startIconApplyProgress(sessionId); } catch (e) {} // v2.0.25: adopt the create-time identity stamp so the post-park re-brand no-ops instead of // recreating the taskbar button a second time with the identical appId. try {@@ -12531,6 +12623,7 @@ const SETTINGS_DEF=[ {k:'overlayBadges',l:'Favicon overlay badges',d:'Paint each pup taskbar icon with the favicon of its active tab plus the Adom mark.'}, {k:'overlaySize',l:'Badge size',d:'Percent of full size for the badge. Smaller keeps it clear of the tile\u2019s text.',sel:['50','66','75','100'],selLabels:{'50':'50% \u2014 smallest, maximum text clearance','66':'66%','75':'75% \u2014 balanced (default)','100':'100% \u2014 full size'},dep:'overlayBadges'}, {k:'taskbarGrouping',l:'AUMID grouping',d:'Only applies when AUMID icons are on. split = one taskbar button per window. grouped = windows grouped BY CATEGORY when icon-style is "category" (up to 5 buttons: signed-in wiki, public wiki, open web, Adom app, plain — e.g. all your signed-in-wiki windows collapse under the one padlock button); with icon-style "standard" grouped collapses everything under a single Adom Pup button.',sel:['split','grouped'],depMode:'aumid',imgBy:{split:'groupsplit',grouped:'groupgrouped'}},+ {k:'taskbarActivity',l:'Taskbar activity signal',d:'How pup shows it is working on a window, on that window’s taskbar button. "progress" = a DETERMINATE progress bar sized to the running-average time of the operation — applying the window icon, taking a screenshot, etc. (pup keeps a per-operation average and fills the bar over it) — the default. "flash" = the older orange taskbar highlight (openFlash/autoFlash). "off" = neither. Windows only.',sel:['progress','flash','off'],selLabels:{'progress':'progress bar (default)','flash':'orange flash (old)','off':'off'}}, {k:'aumidIconStyle',l:'AUMID icon style',d:'Only applies when AUMID icons are on: standard = one pup tile for every window; category = the custom set (wiki signed-out, wiki signed-in padlock, world icon for the open web, adom app icon, mixed).',sel:['standard','category'],depMode:'aumid',imgBy:{standard:'iconstylestd',category:'iconstylecat'}}, {k:'jumpLists',l:'Jump lists',d:'The menu Windows shows when you right-click a pup taskbar icon (real screenshot below, captured on this machine). Requires AUMID icons on. Off by default. Quirk: the FIRST right-click can blip the menu closed — right-click again ~1s later and it holds (a Windows shell timing race, not a pup bug).',img:'jumplist'}, {k:'jlViewTasks',l:'',d:'Wiki view tasks: Switch to logged-in view / Switch to public view on every wiki window.',dep:'jumpLists',subtgl:1,img:'jlviewtasks'},@@ -13051,6 +13144,7 @@ const server = http.createServer(async (req, res) => { openFlash: ['on', 'off'], sleepRecovery: ['on', 'off'], agentActivity: ['on', 'off'], reportIssues: ['on', 'off'], autoLogin: ['on', 'off', 'submit', 'fill'], captureLogins: ['on', 'off'], minimalTouch: ['on', 'off'], taskbarGrouping: ['split', 'grouped'],+ taskbarActivity: ['progress', 'flash', 'off'], aumidIconStyle: ['standard', 'category'], threadTiles: ['on', 'off'], jlViewTasks: ['on', 'off'], jlCloseAll: ['on', 'off'], jlDashboard: ['on', 'off'], jlAnnotate: ['on', 'off'], annCaptionBar: ['on', 'off'], toolbarExtension: ['on', 'off'], tbOwnerBadge: ['on', 'off'], tbSnipHotkey: ['on', 'off'], tbFloatyPencil: ['on', 'off'],@@ -16143,14 +16237,16 @@ const server = http.createServer(async (req, res) => { // must be told WHY — the missing caption is exactly why uncaptioned foregrounds slipped past. // Snapshot the OS foreground before the capture so we can detect+caption it after. const _fgBefore = await osGetForegroundWindow().catch(() => null);- let screenshotBuf = await withTimeout(+ // v2.0.289: show a determinate taskbar progress bar for the length of the capture, sized to+ // the running-average screenshot time (John: progress bars instead of the flash).+ let screenshotBuf = await withTaskbarProgress(resolvedId, 'screenshot', () => withTimeout( // v2.0.186 (#50): captureBeyondViewport defaults TRUE in current puppeteer, which renders // from the document ORIGIN — so a viewport screenshot ignored the page's live scroll // position (scroll via pup_eval, screenshot still showed the top). Viewport shots now // capture the compositor's real viewport; fullPage keeps beyond-viewport ON (it needs it). raceCrash(tab, resolvedId, tab.page.screenshot({ type: 'png', fullPage, captureBeyondViewport: fullPage })), 20_000, 'page.screenshot'- );+ )); // If the capture foregrounded THIS window AND there was no recent user input (so it was pup, // not the user clicking it), caption "<thread> · screenshotting" so the user knows what put it // on their screen. No-op on the common CDP path, which never foregrounds (measured 0/101).