← Commit history

v2.0.353: Native dialog handling actually works now: detection scanned only the legacy sessionId so it never fired for the zero-id and window-label callers that everyone uses, and a modal blocks EVERY pup window rather than its own, so pup_dialog now finds and answers the dialog wherever it is and the skill warns about it up front.

John Lauer ·1dfa5f182a ·2mo ago ·parent f66951b
5 files changed +75−19
SKILL.md+35
@@ -194,6 +194,41 @@ issue #14 did. Renew a still-needed lease with `pup_highfps_extend {sessionId, m keep rendering while unfocused, and the lease makes that self-limiting. Do NOT reach for `highFps` to "fix" a window that merely LOOKS frozen — just focus it (or tell the user to click it). +## A native dialog FREEZES EVERY pup window — this is the #1 thing that looks like "pup is dead"++If a page raises `alert()`, `confirm()`, `prompt()`, or the **"Reload site? Changes you made may not+be saved."** bar, that modal blocks the BROWSER, not just its own window. pup's windows share one+Chrome, so **one dialog wedges every window you have open**, including windows that have nothing to+do with the page that raised it. Verbs on unrelated windows start timing out, and foregrounding+stops working. It reads exactly like pup has died.++**Symptom:** several verbs return timeouts with different wordings, on more than one window, at once.+`pup_reload` says "Navigation timeout", `pup_screenshot` says "page.screenshot timed out",+`pup_close_window` says "Request timed out". None of them is the real problem.++**What to do:**++```bash+adom-bridge pup_dialog '{"action":"dismiss"}'   # keep the page, cancel the navigation+adom-bridge pup_dialog '{"action":"accept"}'    # go ahead (a reload DISCARDS unsaved changes)+```++You do not need to know which window holds it. `pup_dialog` finds the one pending dialog wherever it+is, answers it, and unblocks everything. The blocked verb can then be retried.++**pup will never answer it for you, on purpose.** A `beforeunload` is a question about the user's+UNSAVED WORK, and the two answers destroy different things: `dismiss` blocks the navigation you asked+for, `accept` throws their edits away. Puppeteer's default is to auto-dismiss any dialog nobody is+listening for, which would silently answer that question on the user's behalf. pup refuses. **If the+unsaved work might matter, read it out with `pup_eval` before you accept.**++**Prevention beats recovery:** a page you are about to reload or navigate away from may have unsaved+state. On a form-heavy or editor-like page, check before you navigate rather than after you are stuck.++**Last resort:** if the bridge is upgraded (`bridge_install`) the modal is cleared, because pup+re-adopts its windows through a restart. That costs you only the window that was stuck; the others+come back at their URLs.+ ## Under heavy load or a crash, pup self-heals — do NOT panic-kill or declare it dead  pup is built to stay usable when the shared Chrome is under heavy load, and to recover on its own if
adom-bridge-puppeteer-manifest.json+5−5
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "2.0.352",-  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.352/adom-bridge-puppeteer-v2.0.352.zip",-  "sha256": "5209b1b35ae077f1e0f03530d0b2d2e41800fdf226fe1981661fa42e96a2e37e",-  "size": 1525174,+  "version": "2.0.353",+  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.353/adom-bridge-puppeteer-v2.0.353.zip",+  "sha256": "d28d9b890721f0cf88e0ef9c637e1d4a73b2c57ed3fc2b75ad67c1a0db9579bd",+  "size": 1525824,   "verbPrefixes": [     "browser_",     "desktop_recorder_",@@ -12,5 +12,5 @@   ],   "healthEndpoint": "/status",   "statusVerb": "browser_readiness",-  "released_at": "2026-08-25T16:09:47.112189Z"+  "released_at": "2026-08-26T20:08:07.664225Z" }
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-2.0.352\ No newline at end of file+2.0.353\ No newline at end of file
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "2.0.352",+  "version": "2.0.353",   "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+33−12
@@ -853,6 +853,19 @@ async function decryptBrowserCreds(argsObj) {   }   return last; }+// ANY pending dialog, anywhere (v2.0.353). John, watching it happen: "it stops my other pup windows+// from even foregrounding so i get stuck on all pup windows cuz of one lame javascript alert dialog+// box." He is right, and it changes the shape of this: pup's windows share ONE Chrome for the+// adom-you profile, so a modal does not block its own window, it blocks the BROWSER. The window+// holding the dialog is therefore usually NOT the window whose verb just failed, and scanning only+// the caller's session finds nothing while every window is stuck.+function findAnyPendingDialog() {+  for (const [sid, session] of sessions) {+    const d = findPendingDialog(session);+    if (d) return { ...d, sessionId: sid };+  }+  return null;+} // 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) {@@ -18767,7 +18780,11 @@ const server = http.createServer(async (req, res) => {           let session, resolvedId;           try { ({ session, resolvedId } = await recoverOrRelaunchSession(args)); }           catch (e) { sendSessionResolveError(res, e); return; }-          const pending = findPendingDialog(session);+          // Fall back to whatever window actually holds it: a modal blocks the whole browser, so the+          // caller commonly targets the window they were driving rather than the one that raised it.+          let pending = findPendingDialog(session);+          let holder = resolvedId;+          if (!pending) { const any = findAnyPendingDialog(); if (any) { pending = any; holder = any.sessionId; } }           if (!pending) {             sendJSON(res, {               status: 'ok', handled: false, dialog: null,@@ -18785,7 +18802,8 @@ const server = http.createServer(async (req, res) => {             });             return;           }-          const tab = session.tabs.find(t => t && t.pendingDialog);+          const holdSession = sessions.get(holder) || session;+          const tab = holdSession.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();@@ -18796,9 +18814,9 @@ const server = http.createServer(async (req, res) => {           }           if (tab) tab.pendingDialog = null;           sendJSON(res, {-            status: 'ok', handled: true, action,+            status: 'ok', handled: true, action, window: holder,             dialog: { type: pending.type, message: pending.message },-            _hint: `Answered "${action}". The window is unblocked; the verb that timed out can be retried now.`,+            _hint: `Answered "${action}" on window "${holder}". EVERY pup window is unblocked now, not just that one, and the verb that timed out can be retried.`,           });           return;         }@@ -19455,19 +19473,22 @@ const server = http.createServer(async (req, res) => {     // 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.+    // v2.0.353: scan EVERY session, not args.sessionId. The first cut looked the dialog up on+    // `args.sessionId`, which is the LEGACY id: pup's documented addressing is zero-id ({} resolves+    // to your thread's window) or {window:"..."}, so args.sessionId is normally undefined and this+    // whole branch never ran. The feature worked only for the one calling convention almost nobody+    // uses, which is why it looked shipped and behaved exactly as if it had never been written.     try {-      const _a2 = (typeof args !== 'undefined' && args) || {};-      const _s2 = _a2.sessionId && sessions.get(_a2.sessionId);-      const _d = _s2 && findPendingDialog(_s2);+      const _d = findAnyPendingDialog();       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)}`,+          error: `a ${_d.type} dialog in window "${_d.sessionId}" is BLOCKING EVERY pup 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.`,+          dialog: { type: _d.type, message: _d.message, defaultValue: _d.defaultValue, tabId: _d.tabId, window: _d.sessionId },+          _hint: `EVERY pup window is stuck until this is answered, including windows that have nothing to do with it, because pup's windows share one browser and a modal blocks the browser. The dialog is in window "${_d.sessionId}".` + (_d.type === 'beforeunload'+            ? ` It is asking to confirm leaving a page with UNSAVED CHANGES. pup will not answer it for you: "Cancel" keeps the page and blocks your navigation, "Reload" throws the changes away. Decide, then call pup_dialog {"window":"${_d.sessionId}","action":"accept"} to proceed, or "dismiss" to stay. If the unsaved work might matter, read it out with pup_eval FIRST.`+            : ` Answer it with pup_dialog {"window":"${_d.sessionId}","action":"accept"} or "dismiss"${_d.type === 'prompt' ? ', optionally with promptText' : ''}.`),         }, 200);         return;       }