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.294: Caption timing + dark-mode example images (John). (1) THE TIMER BUG: PUP_CAPTION_MAX_MS=2000 silently clamped EVERY caption to 2s regardless of the caller's stated duration (assisted-click 9s/7s/5s, screenshot 4s, the user's own slider above 2s when maxMs was omitted) - John: 'i don't think you're following your own timer'. He was right. Cap raised to 12s; durations now scale with WORD COUNT via captionMsFor (slider = the floor, ~0.35s/word + 0.9s, 12s ceiling); test button shows the scaled truth. (2) fgCaptionMs slider range 0.5-8s; sliders get custom track/thumb CSS so the thumb travels the FULL track; per-slider labels + units (the pencil-size slider no longer says 'caption duration 0.0s'). (3) Settings example images recaptured in DARK MODE on ConfRoomROG (matching what Adom users actually run): real dark taskbar progress/flash/off states, and the toolbar example now shows the real adom-you AVATAR identity tile (John: 'you are missing the adom-you profile icon') plus a clean crop.
4 files changed
+46−23
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.293",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.293/adom-bridge-puppeteer-v2.0.293.zip",- "sha256": "fb7915fc4ea568839fef9efec22985eeed687465f810b9e2290871e298cb9759",- "size": 1090862,+ "version": "2.0.294",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.294/adom-bridge-puppeteer-v2.0.294.zip",+ "sha256": "804d85423ee49072708f1017f50dbbb8ecc49f5496fd104fb0bb482fbc5a0918",+ "size": 1085173, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-08-23T12:08:51.544442Z"+ "released_at": "2026-08-23T12:24:13.428021Z" }
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.293+2.0.294
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.293",+ "version": "2.0.294", "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+39−16@@ -2507,7 +2507,12 @@ async function showPupCursor(page, x, y, { click = true, label = null } = {}) { // read — he caught a 15s one live. The cap is enforced HERE, at the single choke point, so no caller // can violate it regardless of what it requests. A caption is a glance, not a dialog: if something // genuinely needs the user's attention for longer, use a toast (notify_user), which they dismiss.-const PUP_CAPTION_MAX_MS = 2000;+// v2.0.294 (John: "you put them up so fast, i don't think you're following your own timer"): he was+// right — this cap was 2000 and silently OVERRODE every caller's stated duration (assisted-click's+// 9s/7s/5s narration, the 4s screenshot caption: all actually showed 2s). The anti-rudeness cap now+// matches the reading-time model's ceiling; callers state honest durations (word-scaled via+// captionMsFor) and this only stops a runaway from squatting past 12s.+const PUP_CAPTION_MAX_MS = 12000; async function pupCaption(text, expiresInMs = PUP_CAPTION_MAX_MS, id = 'pup-assist', maxMs) { // maxMs: a USER-CHOSEN duration (settings slider) may exceed the 2s anti-rudeness cap — the cap // exists to stop the AI outstaying its welcome, not to override the user's own preference.@@ -2574,7 +2579,7 @@ async function waitForUserReady(sessionId, hwnd, { maxMs = 120000 } = {}) { // prevent. Gated on the user's fgCaption pref and honoring their fgCaptionMs duration (the call site // already showed an initial notify_user toast + caption, so 'off' never leaves the user unaware). const _capOn = pupSettings().fgCaption === 'on';- const _capMs = Math.max(500, Math.min(5000, Number(pupSettings().fgCaptionMs) || 2000));+ const _capMs = Math.max(500, Math.min(12000, Number(pupSettings().fgCaptionMs) || 2000)); const MAX_CAPS = 3; // initial reminder + at most 2 more, then stop while (Date.now() - start < maxMs) { const fg = await osGetForegroundWindow();@@ -5793,10 +5798,18 @@ const _fgToastAt = new Map(); // only visible if you went looking for it. Now the reason appears ON SCREEN the moment the window // arrives, so the justification is accountable in real time. Deliberately NOT gated by minimalTouch: // this is a user-protection signal, not cosmetic machinery.+// v2.0.294 (John: "time your ab captions... the timing is just too fast. we need a longer amount of+// time here, or you need to count the words and adjust"): caption duration scales with how much+// there is to READ. The fgCaptionMs slider is the FLOOR (short captions hold at least that long);+// longer text extends at reading speed (~350ms/word + 900ms to notice it), capped at 12s so a+// runaway reason can never squat on the screen.+function captionMsFor(text) {+ const base = Math.max(500, Math.min(12000, Number(pupSettings().fgCaptionMs) || 3500));+ const words = String(text || '').trim().split(/\s+/).filter(Boolean).length;+ return Math.max(base, Math.min(12000, 900 + words * 350));+} async function captionForegroundReason(sessionId, reason, verb, thread) {- // v1.9.259: user prefs — toggle + duration slider (500..5000ms, default 2000). if (pupSettings().fgCaption !== 'on') return;- const _capMs = Math.max(500, Math.min(5000, Number(pupSettings().fgCaptionMs) || 2000)); try { const why = String(reason || '').trim(); if (!why) return;@@ -5805,7 +5818,9 @@ async function captionForegroundReason(sessionId, reason, verb, thread) { const _s = sessions.get(sessionId); const _th = (thread && String(thread).trim()) || (_s && (_s.owner || _s._ownerThread)) || null; const who = _th ? `AI thread "${_th}"` : 'Adom';- await pupCaption(`${who} brought "${sessionId}" to the front. Reason: "${why}"`, _capMs, 'pup-fg-reason', _capMs);+ const msg = `${who} brought "${sessionId}" to the front. Reason: "${why}"`;+ const _capMs = captionMsFor(msg);+ await pupCaption(msg, _capMs, 'pup-fg-reason', _capMs); } catch (e) {} } @@ -12376,7 +12391,7 @@ select,input{background:#111b23;color:#e6edf3;border:1px solid #23303c;border-ra .tgl.is-on::after{left:20px;background:#05221f} .sub{margin-left:26px;padding:8px 0 2px;display:flex;align-items:center;gap:10px} .sub.sdis{opacity:.4;pointer-events:none}-.sub input[type=range]{flex:1 1 auto;accent-color:#00b8b1}+.sub input[type=range]{flex:1 1 auto;accent-color:#00b8b1;-webkit-appearance:none;appearance:none;height:18px;background:transparent;margin:0}.sub input[type=range]::-webkit-slider-runnable-track{height:6px;border-radius:3px;background:#2a3038}.sub input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;border-radius:50%;background:#00b8b1;margin-top:-5px;box-shadow:0 0 0 1px #0d1117} .sub .sval{color:#c9d1d9;font-family:"JetBrains Mono",monospace;font-size:12px;min-width:44px;text-align:right} .sub select{background:#0d1117;color:#c9d1d9;border:1px solid #30363d;border-radius:6px;padding:4px 8px} #taskbar{position:sticky;top:44px;z-index:9;display:flex;gap:6px;align-items:center;padding:8px 18px;background:#0a0f14;border-bottom:1px solid #1d2833;overflow-x:auto}@@ -12670,14 +12685,14 @@ const SETTINGS_DEF=[ {k:'toolbarExtension',l:'Adom toolbar in Chrome',d:'The Adom Pup extension button in the browser toolbar (annotate, snip, view toggle, dashboard, owner, close). Requires the one-time extension install.',img:'toolbarext'}, {k:'tbSnipHotkey',l:'',d:'Alt+Shift+A starts annotate (red pen) on the current pup window.',dep:'toolbarExtension',subtgl:1}, {k:'tbFloatyPencil',l:'',d:'The tiny pencil in the upper-right corner of each page: one click starts annotate. Off by default: the corner hover zone can fight buttons in app headers, and the toolbar pen button covers discovery.',dep:'toolbarExtension',subtgl:1},- {k:'floatyHitPx',l:'',d:'Pencil corner hit area (px): how big the invisible top-right target is that reveals the pencil. Applies to open pages immediately.',slider:[6,120,2],dep:'tbFloatyPencil'},+ {k:'floatyHitPx',l:'',d:'',sliderLabel:'pencil corner hit area',unit:'px',slider:[6,120,2],dep:'tbFloatyPencil'}, // v2.0.293 (John: "why did you make progress bar vs orange flash a child of Favicon overlay // badges? that hierarchy doesn't make sense"): taskbarActivity is its OWN top-level row here in // the activity/foreground section, NOT inside the taskbar-identity cluster where its position // between the badge/AUMID rows rendered it as a sub-setting of overlay badges. {k:'taskbarActivity',l:'Taskbar activity signal',d:'The single control for how pup signals it is working on a window, on that window’s taskbar button. "progress" (default): a determinate progress bar sized to how long the operation usually takes (applying the window icon, taking a screenshot) plus a strip while an AI actively drives the window; pup keeps a per-operation running average and fills the bar over it. "flash": the older orange Windows attention flash, when a window opens and when an AI changes it. "off": neither. Windows only. The example switches with your choice.',sel:['progress','flash','off'],selLabels:{'progress':'progress bar (default)','flash':'orange flash (old)','off':'off'},imgBy:{progress:'tbactprogress',flash:'tbactflash',off:'tbactoff'}}, {k:'fgCaption',l:'Caption on AI foreground',d:'When an AI brings a window to your screen, show its stated reason as an on-screen caption. The AI is always forced to give a reason; this shows it to you.'},- {k:'fgCaptionMs',l:'',d:'',slider:[500,5000,250],dep:'fgCaption',test:1},+ {k:'fgCaptionMs',l:'',d:'',sliderLabel:'minimum caption time (longer text auto-extends at reading speed, 12s max)',unit:'s',slider:[500,8000,250],dep:'fgCaption',test:1}, {k:'sleepRecovery',l:'Sleep and crash recovery',d:'After the laptop wakes or Chrome dies, automatically bring every pup window back at its last page, signed in. Never restarts a live Chrome.'}, {k:'autoLogin',l:'Auto-fill saved logins',d:'Fill a matching saved login when a login page loads in a pup window (background, no Chrome dropdown). pup never submits on its own: submitting a stored password is reason-gated and confirms the live domain first, to guard against lookalike/phishing sites.'}, {k:'captureLogins',l:'Capture logins to the vault',d:'As you sign into sites in pup windows, store the username and password in the OS keychain (encrypted at rest, never logged) so return visits auto-fill. This is how the shared adom-you identity grows over time.'},@@ -12744,9 +12759,12 @@ function renderDlg(){ if(def.slider){ const on=st[def.dep]==='on'; const v=Number(st[def.k])||3500;- return '<div class="sub'+(on?'':' sdis')+'" id="row-'+def.k+'"><span class="sdesc">caption duration</span>'- +'<input type="range" min="'+def.slider[0]+'" max="'+def.slider[1]+'" step="'+def.slider[2]+'" value="'+v+'" data-slider="'+def.k+'" aria-label="caption duration">'- +'<span class="sval" id="val-'+def.k+'">'+(v/1000).toFixed(1)+'s</span>'+ const lbl=def.sliderLabel||'caption duration';+ const unit=def.unit||'s';+ const disp=unit==='px'?v+'px':(v/1000).toFixed(1)+'s';+ return '<div class="sub'+(on?'':' sdis')+'" id="row-'+def.k+'"><span class="sdesc">'+lbl+'</span>'+ +'<input type="range" min="'+def.slider[0]+'" max="'+def.slider[1]+'" step="'+def.slider[2]+'" value="'+v+'" data-slider="'+def.k+'" data-unit="'+unit+'" aria-label="'+lbl+'">'+ +'<span class="sval" id="val-'+def.k+'">'+disp+'</span>' +(def.test?'<button class="btn sm" data-testcap="1" title="show a sample caption on your screen right now, at the configured duration">test</button>':'')+'</div>'; } if(def.subtgl){@@ -12966,7 +12984,7 @@ document.addEventListener('change',ev=>{ }); document.addEventListener('input',ev=>{ const r=ev.target.closest('[data-slider]');- if(r){_q('#val-'+r.dataset.slider).textContent=(Number(r.value)/1000).toFixed(1)+'s';}+ if(r){_q('#val-'+r.dataset.slider).textContent=r.dataset.unit==='px'?r.value+'px':(Number(r.value)/1000).toFixed(1)+'s';} }); document.addEventListener('change',ev=>{ const r=ev.target.closest('[data-slider]');@@ -13116,8 +13134,10 @@ const server = http.createServer(async (req, res) => { } if (act === 'test-caption') { // v1.9.262 (John: "give them a test button so they can see the caption").- const ms = Math.max(500, Math.min(5000, Number(pupSettings().fgCaptionMs) || 3500));- pupCaption(`This is what an AI-foreground caption looks like. Reason: "you clicked Test in pup settings" (${(ms / 1000).toFixed(1)}s)`, ms, 'pup-fg-reason', ms).catch(() => {});+ // v2.0.294: same word-count scaling as real captions, so the test shows the truth.+ const _msg = 'This is what an AI-foreground caption looks like. Reason: "you clicked Test in pup settings"';+ const ms = captionMsFor(_msg);+ pupCaption(`${_msg} (${(ms / 1000).toFixed(1)}s)`, ms, 'pup-fg-reason', ms).catch(() => {}); return done({ ok: true, ms }); } if (act === 'import-scan') {@@ -16300,7 +16320,8 @@ const server = http.createServer(async (req, res) => { // ALWAYS. (The idle reading is kept only for the log.) let _idle = -1; try { _idle = await osIdleMs(); } catch (e) {} const _thread = session.owner || session._ownerThread || 'an AI thread';- pupCaption(`${_thread} · screenshotting`, 4000, 'pup-screenshot').catch(() => {});+ const _scapMs = captionMsFor(`${_thread} · screenshotting`);+ pupCaption(`${_thread} · screenshotting`, _scapMs, 'pup-screenshot', _scapMs).catch(() => {}); console.log(`[screenshot] "${resolvedId}" came to the FOREGROUND during capture (user idle ${_idle}ms) — captioned "${_thread} · screenshotting"`); } }@@ -18957,7 +18978,9 @@ const _onPupBound = async () => { if (_idle != null && _idle < 1500) { console.log(`[fg-watch] "${sid2}" surfaced with recent user input (${_idle}ms idle) — assuming USER raised it, no caption`); break; } globalThis._lastNoFaultCaptionAt = Date.now(); console.log(`[fg-watch] "${sid2}" surfaced with no pup raise and no recent input (idle ${_idle}ms) — no-fault caption (2s, deduped)`);- pupCaption(`This window was NOT foregrounded by Adom: JavaScript, a focus change, Chrome itself, or your own click raised "${sid2}". Not me!`, 2000, 'pup-nofault').catch(() => {});+ const _nmMsg = `This window was NOT foregrounded by Adom: JavaScript, a focus change, Chrome itself, or your own click raised "${sid2}". Not me!`;+ const _nmMs = captionMsFor(_nmMsg);+ pupCaption(_nmMsg, _nmMs, 'pup-nofault', _nmMs).catch(() => {}); break; } catch (e) {} }