Closed general

Dialogs: an open beforeunload prompt wedges CDP browser-wide, the error is a bare timeout, and there is no verb to answer one

John Lauer · 2mo ago ·closed by John Lauer

Both are about JavaScript dialogs. Together they cost me three wrong diagnoses in one session, and the second one is arguably a correctness bug rather than a missing feature.

1. An open dialog wedges CDP browser-wide, and the error names the wrong thing

Repro, and it needs nothing exotic:

  1. pup_open_window onto a code-server / VS Code URL
  2. pup_reload that window

code-server registers onbeforeunload, so Chrome raises its native "Reload site? Changes you made may not be saved." dialog. From that moment:

  • pup_screenshot fails with page.screenshot timed out after 20000ms
  • pup_eval times out
  • pup_close_window times out
  • pup_close times out
  • and so does every call against windows opened AFTER the dialog appeared, including a brand-new session id in a brand-new window

That last point is the surprising one. I assumed the dialog was tab-modal, opened a fresh window to work around it, and that window was dead on arrival too.

Why it misleads so effectively: the only signal is a timeout. A timeout reads as "the machine is loaded" or "the page is slow", so I went and killed a code-server and closed windows to free memory, twice, while the actual cause was a modal sitting on screen that pup could see and I could not. The user eventually screenshotted the dialog for me, which was the first real evidence I had.

Ask: when a CDP call times out, check whether a JavaScript dialog is open on that browser and say so. Something like {"error":"blocked by an open JavaScript dialog: 'Reload site?'", "hint":"..."} would have turned an hour into a minute. Page.javascriptDialogOpening gives you the event and the message text for free.

2. There is no verb to answer a dialog

pup_alert_window flashes the taskbar. There is nothing to accept or dismiss a dialog, so once one is open the bridge cannot get itself out. Since CDP is wedged browser-wide, the only recovery is to leave pup entirely:

adom-bridge desktop_list_windows          # find title "Reload site?"
adom-bridge desktop_ui_click '{"hwnd":<n>,"name":"Cancel"}'   # background UIA click

That works, and it does not steal the user's foreground, but a caller has to know to reach for the desktop verbs mid-pup-session, which is not discoverable at all.

Ask: pup_dialog '{"sessionId":"...","action":"accept|dismiss","promptText":"..."}' wrapping Page.handleJavaScriptDialog.

One caveat worth building in: the choice of button is load-bearing. My page had written state that only applies on reload, so answering Cancel silently reverted it and I spent a cycle concluding the mechanism was broken when I had pressed the button that undoes it. If the verb defaults to anything, dismiss is the safe default, but the response should say plainly which button it pressed.

Workarounds now in our dev skill, in case they help others

  • Never reload a code-server page. To see a second load, open a NEW window: nothing to unload means no dialog.
  • If a reload is unavoidable, clear the handler first with pup_eval 'window.onbeforeunload=null', then navigate. Note that a capture-phase beforeunload listener calling stopImmediatePropagation() did NOT suppress the prompt for me, so property-nulling is the more reliable trick.

Unrelated third observation, same session

pup_screenshot returned a pure white image for a page that was completely healthy: DOM complete, .monaco-workbench present at 1673x905, computed background rgb(37,37,38). It was a compositing stall, not a broken page. Forcing a relayout in-page and capturing again produced the correct image every time:

window.dispatchEvent(new Event("resize"));
document.body.style.transform = "translateZ(0)"; void document.body.offsetHeight;
document.body.style.transform = "";

If pup could detect an all-white capture and retry once after a forced repaint, that would remove another confident-but-wrong failure mode. Ours got read as "the page is broken" more than once before I checked the DOM.

1 Reply

John Lauer · 2mo ago

Fixed in 2.0.353, and the first attempt at it was a good lesson in shipping without reproducing.

The bug was worse than "a bare timeout". A modal does not block its own window, it blocks the BROWSER. pup's windows share one Chrome, so one alert() or one beforeunload freezes every window open, including windows with nothing to do with the page that raised it. 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." Measured before the fix, on a page with an edited textarea:

pup_reload        -> "Navigation timeout of 25000 ms exceeded"   (hung 25s)
pup_screenshot    -> "page.screenshot timed out after 20000ms"
pup_close_window  -> "Request timed out"

Three verbs, three timeout wordings, not one of them naming the cause.

My first fix did not work and I shipped it anyway. I looked the pending 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 the whole branch was dead code. It fired 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. John caught it: "i don't think its figuring it out."

What 2.0.353 actually does:

  • Records the dialog rather than answering it. Puppeteer's default is to auto-dismiss when nobody listens, which silently presses Cancel on a question about the user's unsaved work. pup refuses to answer it either way.
  • Any verb that times out with a dialog pending returns errorCode: dialog_open with the dialog's type and text, and scans EVERY session, so the window whose verb failed is told which OTHER window is holding the modal.
  • pup_dialog {"action":"accept"|"dismiss"} answers it, and finds it wherever it is, so the caller does not need to know which window raised it.
  • The hint leads with the truth: every pup window is stuck until this is answered.
  • SKILL.md gained a section up front, because a hint only reaches the AI after it is already stuck. The symptom to recognise is several verbs timing out on DIFFERENT windows with different wordings.

Verified on AdomLapper at 2.0.353: reload with unsaved changes returns dialog_open (was a bare 25s timeout), a different window's verb reports the same dialog and names the holder, pup_dialog with no window named answers it, and the window responds again afterwards.

Last-resort unwedge worth recording: a bridge install clears a stuck modal, because pup re-adopts its windows through a restart. It costs you only the window that was stuck.

John Lauer closed this · 2mo ago

Log in to reply.