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.344: Detect native JS dialogs instead of hanging: a blocking alert, confirm, prompt or beforeunload is now reported as dialog_open with the dialog text and answered explicitly with the new pup_dialog verb, rather than surfacing as three different meaningless timeouts.
4 files changed
+110−9
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.343",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.343/adom-bridge-puppeteer-v2.0.343.zip",- "sha256": "22eaa50b4c79ed02c5e0f2c34bca742b6318ef180eb6a765715b2856ad06dc8b",- "size": 1516780,+ "version": "2.0.344",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.344/adom-bridge-puppeteer-v2.0.344.zip",+ "sha256": "a589bb3b75fda6497737dc9961fa4c29a6f978e61fe139550c5f800294ac661a",+ "size": 1519062, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-08-25T13:47:39.219065Z"+ "released_at": "2026-08-25T13:53:10.716135Z" }
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.343+2.0.344
src/bridge.json+2−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.343",+ "version": "2.0.344", "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.",@@ -32,6 +32,7 @@ "pup_back", "pup_forward", "pup_reload",+ "pup_dialog", "pup_click", "pup_type", "pup_press_key",
src/server.js+102−2@@ -853,6 +853,16 @@ async function decryptBrowserCreds(argsObj) { } return last; }+// The one pending native dialog on a session, if any. A modal blocks the WHOLE window, so it does+// not matter which tab owns it: the first one found is the one the caller is stuck behind.+function findPendingDialog(session) {+ if (!session || !Array.isArray(session.tabs)) return null;+ for (const t of session.tabs) {+ if (t && t.pendingDialog) return { ...t.pendingDialog, tabId: t.tabId };+ }+ return null;+}+ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user', openerTabId = null } = {}) { const tabId = nextTabId(session); const errors = [];@@ -1190,7 +1200,31 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user } } catch (e) { console.error(`tab-close recording cleanup failed: ${e.message}`); } });- session.tabs.push({ tabId, page, errors, opener, openerTabId });+ // NATIVE JS DIALOGS (v2.0.344, John: "do you detect these dialogs and tell the ai and let them+ // agree to the reload?"). Until now pup registered NO dialog handler at all, and the consequences+ // were worse than cosmetic: an alert/confirm/prompt, or a beforeunload "Reload site? Changes you+ // made may not be saved", BLOCKS the whole window. Measured before this fix, on a page with an+ // edited textarea: pup_reload sat for 25s and returned "Navigation timeout of 25000 ms exceeded",+ // pup_screenshot returned "page.screenshot timed out after 20000ms", and even pup_close_window+ // timed out. Three different verbs, three timeout messages, and not one of them said the true+ // cause: a modal is up, waiting for a human who is never coming.+ //+ // We deliberately do NOT auto-dismiss. Puppeteer's default (dismiss when nobody listens) would+ // silently answer "Cancel" to a question about the USER'S UNSAVED WORK, and silently discarding+ // someone's data is worse than blocking. So the dialog is RECORDED and the AI is told, which is+ // exactly what John asked for: detect it, surface it, let the driver decide.+ const tabRec = { tabId, page, errors, opener, openerTabId, pendingDialog: null };+ page.on('dialog', (dialog) => {+ tabRec.pendingDialog = {+ dialog,+ type: dialog.type(), // alert | confirm | prompt | beforeunload+ message: dialog.message(),+ defaultValue: dialog.defaultValue(),+ at: Date.now(),+ };+ console.error(`[${sessionId}] tab ${tabId}: ${dialog.type()} dialog opened and is BLOCKING: ${JSON.stringify(dialog.message()).slice(0, 160)}`);+ });+ session.tabs.push(tabRec); if (makeActive) session.activeTabId = tabId; // v1.6.3+: enable scripted downloads on this page by default. Pup is a@@ -11551,7 +11585,8 @@ const VERB_META = { pup_navigate: { hint: 'Drives the active tab to url and waits for load. Verify with a screenshot before asserting success.', related: ['pup_screenshot', 'pup_wait', 'pup_reload'], pitfalls: ['navigation can race SPA routing — confirm with pup_screenshot', 'some sites need pup_wait for a selector after navigate'] }, pup_back: { hint: 'History back on the active tab.', related: ['pup_forward', 'pup_navigate'], pitfalls: [] }, pup_forward: { hint: 'History forward on the active tab.', related: ['pup_back', 'pup_navigate'], pitfalls: [] },- pup_reload: { hint: 'Reloads the active tab.', related: ['pup_navigate', 'pup_wait'], pitfalls: [] },+ pup_reload: { hint: 'Reloads the active tab.', related: ['pup_navigate', 'pup_wait', 'pup_dialog'], pitfalls: ['A page with unsaved changes raises a native "Reload site?" dialog that BLOCKS the window: the reload appears to time out and so does everything else. Answer it with pup_dialog.'] },+ pup_dialog: { hint: 'Answer a native JS dialog (alert/confirm/prompt/beforeunload) that is blocking the window: {action:"accept"|"dismiss", promptText?}.', related: ['pup_reload', 'pup_navigate'], pitfalls: ['pup NEVER answers these for you. A beforeunload is a question about the user\'s unsaved work, so accept and dismiss each destroy something different. Read the page with pup_eval first if the work matters.'] }, pup_click: { hint: 'Clicks by CSS selector (preferred) or by x,y coords. Selector is more robust across layouts.', related: ['pup_type', 'pup_input_dispatch', 'pup_screenshot'], pitfalls: ['selector must match exactly one visible element — verify with pup_evaluate first', 'coords are viewport-relative; a scrolled element may be off-screen'] }, pup_type: { hint: 'Types into the focused or selector-targeted field.', related: ['pup_click', 'pup_press_key'], pitfalls: ['focus the field first (pup_click the input) or pass selector', 'does not clear existing text — select-all + delete if you need a clean field'] }, pup_press_key: { hint: 'Presses a key or chord (Enter, Ctrl+A, Tab).', related: ['pup_type', 'pup_input_dispatch'], pitfalls: ['chord syntax is "Ctrl+A" — verify the page has focus on the intended element'] },@@ -18653,6 +18688,50 @@ const server = http.createServer(async (req, res) => { }); return; }+ // ANSWER a blocking native dialog (v2.0.344). This is the verb the dialog_open error+ // points at. It is deliberately a SEPARATE, explicit call rather than an auto-answer:+ // a beforeunload is a question about the user's unsaved work, and pup answering it on the+ // AI's behalf would silently destroy data in one direction or the other.+ case 'pup_dialog': {+ let session, resolvedId;+ try { ({ session, resolvedId } = await recoverOrRelaunchSession(args)); }+ catch (e) { sendSessionResolveError(res, e); return; }+ const pending = findPendingDialog(session);+ if (!pending) {+ sendJSON(res, {+ status: 'ok', handled: false, dialog: null,+ _hint: 'No dialog is open on this window, so there was nothing to answer. If a verb is timing out, the cause is something else.',+ });+ return;+ }+ const action = (args && args.action) || '';+ if (action !== 'accept' && action !== 'dismiss') {+ sendJSON(res, {+ status: 'error', errorCode: 'dialog_action_required',+ error: `a ${pending.type} dialog is open; pass action:"accept" or action:"dismiss"`,+ dialog: { type: pending.type, message: pending.message, defaultValue: pending.defaultValue },+ _hint: 'accept = press the confirming button (Reload / OK). dismiss = press Cancel. pup will not choose for you.',+ });+ return;+ }+ const tab = session.tabs.find(t => t && t.pendingDialog);+ try {+ if (action === 'accept') await pending.dialog.accept(args && args.promptText !== undefined ? String(args.promptText) : undefined);+ else await pending.dialog.dismiss();+ } catch (e) {+ if (tab) tab.pendingDialog = null; // a stale handle cannot be answered twice+ sendJSON(res, { status: 'error', error: `could not answer the dialog: ${e.message}`, errorCode: 'dialog_answer_failed' });+ return;+ }+ if (tab) tab.pendingDialog = null;+ sendJSON(res, {+ status: 'ok', handled: true, action,+ dialog: { type: pending.type, message: pending.message },+ _hint: `Answered "${action}". The window is unblocked; the verb that timed out can be retried now.`,+ });+ return;+ }+ case 'pup_use': { // Pin (or clear) the browser pup drives by default. browser: 'chrome' | 'edge' // | 'cft' | 'auto' (or pass an explicit executablePath). 'cft' fetches Chrome@@ -19293,6 +19372,27 @@ const server = http.createServer(async (req, res) => { // renderer CRASHED, do not return a useless generic error. The crash event often lands right as // the op times out, so re-check here and return the structured crash error with the // reload-to-recover path — turning a 20s silent hang into an actionable answer.+ // A BLOCKING DIALOG LOOKS EXACTLY LIKE A HANG, so say which it is (v2.0.344). Every verb that+ // touches a page with a modal up returns some flavour of "timed out", and three different+ // timeout messages taught the AI nothing about the real cause. If a dialog is pending on this+ // session, the timeout is EXPLAINED rather than reported bare.+ try {+ const _a2 = (typeof args !== 'undefined' && args) || {};+ const _s2 = _a2.sessionId && sessions.get(_a2.sessionId);+ const _d = _s2 && findPendingDialog(_s2);+ if (_d && /timed out|timeout|detached|Target closed|Session closed/i.test(e.message || '')) {+ sendJSON(res, {+ status: 'error',+ error: `a ${_d.type} dialog is open and BLOCKING this window: ${JSON.stringify(_d.message)}`,+ errorCode: 'dialog_open',+ dialog: { type: _d.type, message: _d.message, defaultValue: _d.defaultValue, tabId: _d.tabId },+ _hint: _d.type === 'beforeunload'+ ? `The page is asking to confirm leaving with UNSAVED CHANGES. pup will not answer this for you, because "Cancel" silently discards the user's work and "Reload" silently loses it. Decide, then call pup_dialog {"action":"accept"} to go ahead with the reload/navigation, or {"action":"dismiss"} to stay on the page. If the work matters, read it out with pup_eval FIRST.`+ : `Answer it with pup_dialog {"action":"accept"} or {"action":"dismiss"}${_d.type === 'prompt' ? ', optionally with {"promptText":"..."} for a prompt' : ''}. Nothing else on this window will respond until you do.`,+ }, 200);+ return;+ }+ } catch (de) {} try { const _a = (typeof args !== 'undefined' && args) || {}; const _sess = _a.sessionId && sessions.get(_a.sessionId);