adom-desktop v1.4.7 follow-ups — three small fixes

Paste this into Claude on the desktop where the adom-desktop Rust + plugins/puppeteer source lives. Three loosely-related items I hit while integrating the v1.4.7 features into chip-fetcher today.


Background

I'm running adom-desktop from inside an Adom container. Just verified v1.4.7's two new features end-to-end via the WAGO datasheet recipe:

  • browser_input_dispatch type:"click" with {x, y} coords ✅ (trusted click, isTrusted=true, framework handler fires)
  • browser_list_tabs showing opener:"popup" for target=_blank form-submit popups ✅

These work. Three small refinements would have saved me ~20 min of debugging:


Item 1 — browser_input_dispatch with selector: should disambiguate or error on ambiguity

What bit me

WAGO's modal has a Generate data sheet button at the bottom of the modal. The button's class is wg-button wg-button--primary. I called:

adom-desktop browser_input_dispatch '{
  "sessionId":"chip-fetcher",
  "type":"click",
  "selector":"button.wg-button--primary"
}'

The relay returned {"ok":true, "dispatched":"click"}. I assumed the click landed on the Generate button. It didn't — the modal stayed up. After a browser_eval to inspect, the page actually had four button.wg-button--primary elements:

  1. The cookie/consent dismiss button (54x54, no text, top-right corner)
  2. "Add to shopping cart" (visible)
  3. "Add to shopping cart" (display:none duplicate)
  4. "Generate data sheet" (the one I wanted, at y≈779)

Puppeteer's page.click(selector) resolves to the first match in document order, so the cookie X button got clicked. The relay reported success — because, mechanically, a trusted click was dispatched — but it wasn't the click I wanted.

What I want

Either of these would have saved the debug round:

Option A — error on ambiguity by default:

{"ok":false,"error":"selector matched 4 elements; pass exactNth or use coords","matched":4}

With an opt-in firstMatch:true arg if the caller knows the first match is correct.

Option B — pick smartly (preferred):

Filter the matched set to visible elements with non-zero text content before picking, and tie-break by largest visual area or by being inside the topmost stacking context (modals/dialogs). This is what a human would do — "click the visible Generate button," not "click the first DOM-order match." Most call sites benefit; ambiguity is usually the cookie-banner-still-up + button-of-the-same-class case I hit.

Either way, return the matched count + the rect of the element actually clicked so the caller can sanity-check:

{
  "ok": true,
  "dispatched": "click",
  "matchedCount": 4,
  "clickedRect": {"x":890, "y":803, "w":193, "h":48},
  "clickedText": "Generate data sheet"
}

Workaround in the meantime

I'm computing rects via browser_eval and passing {x, y} coords to browser_input_dispatch instead of selectors. Works fine but verbose; selectors should "just work" for the obvious case.


Item 2 — fetching a PDF from a Chrome PDF-viewer popup tab returns the viewer HTML, not the binary

What bit me

WAGO's "Generate Datasheet" form-submit POSTs to wago.priintcloud.com/datasheets/<MPN>/<lang> with target="_blank". The response is Content-Type: application/pdf. Chrome opens it as a popup tab, which v1.4.7's browser_list_tabs correctly tracks as opener:"popup".

But when I do this against the popup tab to grab the PDF binary:

adom-desktop browser_eval '{
  "sessionId":"chip-fetcher",
  "tabId":"tab-2",
  "expr":"(async()=>{const r=await fetch(location.href,{credentials:\"include\"}); const ab=await r.arrayBuffer(); /* base64-encode and return */})()",
  "awaitPromise":true
}'

…I get a 194 KB stub HTML wrapper that starts with newlines, not the multi-MB PDF binary. Confirmed: document.contentType reports application/pdf but the in-page fetch is being intercepted by Chrome's PDF Viewer Plugin shell rather than hitting the network.

The PDF binary IS reachable via direct POST replay (I did that as a workaround — read the bundled JS, extracted the priintcloud endpoint + form params, ran the POST inside a regular tab via fetch() and got 3.8 MB of real PDF). But that defeats the purpose of the popup feature — the whole point of the popup was to avoid the bundle-reading reverse-engineering.

What I want

A way to grab the popup tab's actual served bytes without hitting the PDF viewer wrapper. Options:

Option A — browser_save_tab_as <path>: Pup uses puppeteer's page.pdf() for HTML pages, or for application/pdf tabs, simply re-issues the original request via the network layer with the tab's session cookies and saves the response body. Returns {ok, savedTo, bytes, contentType}.

adom-desktop browser_save_tab_as '{
  "sessionId":"chip-fetcher",
  "tabId":"tab-2",
  "saveTo":"/path/in/container/wago.pdf"
}'

Option B — browser_fetch_url <url>: Generic "fetch this URL with the active session's cookies and return base64 binary." Caller passes the popup's URL (read via browser_eval tabId:"tab-2", expr:"location.href"). Implementation: puppeteer-side page.context.request.get(url, {headers:{...}}) returns the body without going through the rendered tab.

adom-desktop browser_fetch_url '{
  "sessionId":"chip-fetcher",
  "url":"https://wago.priintcloud.com/datasheets/2601-3105/en/abc?attempt=1&signature=...",
  "saveTo":"/tmp/wago.pdf"
}'
# returns {ok:true, bytes:3798336, contentType:"application/pdf"}

Option B is more general (also handles the case where the click doesn't open a popup but just downloads). Option A is more focused on "I have this popup tab and want its bytes." Either solves the WAGO case cleanly.

Either way: include Content-Type + size in the result so the caller can validate before writing to disk.

Workaround

For now I'm reading the JS bundle, finding the underlying API the form submit hits, and replaying the POST directly. Brittle (bundle hashes change every deploy) but works.


Item 3 — document node.exe in the bridge-stale-after-upgrade list

What bit me

After v1.4.6 → v1.4.7 desktop upgrade, the relay rejected browser_input_dispatch with "Unknown command" even though the new Linux CLI binary's strings output had the command and --version was correct. Restarting adom-desktop serve on the container side didn't help — the relay was fine; the forwarded-to puppeteer bridge (node.exe on Windows) was stale, still loaded the old plugins/puppeteer/server.js from before the upgrade.

The fix was taskkill //f //im node.exe on the desktop. That's documented in CLAUDE.md for the KiCad / Fusion bridges (python.exe) but node.exe (puppeteer) wasn't mentioned in that list.

What I want

Add node.exe to whatever auto-graceful-restart logic / docs the python.exe bridges use. If there's an installer post-step that does taskkill on stale bridges, include node.exe there. If it's documented in CLAUDE.md / README, mention node.exe next to python.exe.

A user-facing message in the relay would also help: when the relay sees the bridge return "Unknown command" for a command that's in the relay's known-commands table, it could automatically respond:

{
  "ok": false,
  "error": "puppeteer bridge appears stale (returned Unknown command for a known action)",
  "hint": "On Windows: taskkill /f /im node.exe; the bridge will auto-respawn on next browser_* call",
  "stale_bridge_suspected": true
}

That self-diagnostic + hint would have saved most of the debug time.


Suggested PR shape

  • Item 1 — add visibility-filter + ambiguity-error to browser_input_dispatch's selector path in plugins/puppeteer/server.js. Return matchedCount + clickedRect + clickedText in the response so callers can verify.
  • Item 2 — add browser_fetch_url action that uses puppeteer's BrowserContext.request (or page.context().request) to fetch arbitrary URLs with the session's cookies, bypassing PDF-viewer interception. Optional saveTo for direct disk write; otherwise base64 in the response.
  • Item 3 — add node.exe to the bridge-respawn docs + ideally the relay-side stale-bridge self-diagnostic hint.

Once shipped, please bump the version + remind me to refresh /usr/local/bin/adom-desktop from the wiki + taskkill //f //im node.exe on first run.

— sent from john's container running chip-fetcher v0.1.x