# 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:

```bash
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:**

```json
{"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:

```json
{
  "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:

```bash
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}`.

```bash
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.

```bash
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:

```json
{
  "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
