# adom-desktop feature request: trusted-input dispatch + popup-tab tracking

Paste this whole document into Claude on your desktop (where the `adom-desktop` Rust source lives) and ask it to implement the two features below.

---

## Context

`adom-desktop` is the Rust CLI + WebSocket relay (default `/usr/local/bin/adom-desktop`) that lets container-side agents drive a real Chromium ("pup") on the user's laptop. Source layout:

- `Cargo.toml` + `src/` — main Rust binary (CLI + relay)
- `plugins/puppeteer/` — Node side that actually drives Chromium via puppeteer-core
- Existing browser commands: `browser_open_window`, `browser_open_tab`, `browser_navigate`, `browser_eval`, `browser_screenshot`, `browser_list_tabs`, `browser_list_windows`, `browser_close_tab`, `browser_close_window`, `browser_focus_window`, `browser_raise_os_window`, `browser_alert_window`, `browser_reload`, `browser_record_*`, `browser_switch_tab`

I'm using adom-desktop from inside an Adom container to run a tool called `chip-fetcher` that scrapes manufacturer / distributor sites for chip CAD bundles + datasheets. I just hit two gaps that block whole classes of vendor sites. Fixing them unblocks a lot.

---

## Feature 1 — `browser_input_dispatch` (trusted click / key / move)

### The gap

Page-side `dispatchEvent(new MouseEvent("click", {...}))` produces events with `isTrusted: false`. Modern frameworks (React, Vue, Svelte) and most anti-automation vendor JS gate handlers on `event.isTrusted === true`. So my synthetic clicks fire to the DOM but the registered click handler **never runs**.

Repro that bit me today: WAGO's "Generate Datasheet" button. Their bundled JS does:

```js
submit = async () => {
  const url = priintGridUrl + language;
  const body = new URLSearchParams({ALL_DATA: "true", language, productCode});
  formSubmit({url, method: "POST", target: "_blank"}, body);
};
```

That handler is wrapped behind an `isTrusted` check by Vue/the framework. My `MouseEvent`, `PointerEvent`, focus-then-Enter — all silently no-op. I had to read the bundle, extract the API, and replay the POST manually via `fetch()` with `credentials:"include"`. That works, but it's brittle (bundle hashes change every deploy) and uses no end-user UX.

### The fix

Expose Puppeteer's native CDP input dispatch. Puppeteer already wraps it as `page.mouse.click(x, y)`, `page.keyboard.type(text)`, `page.keyboard.press(key)`, `page.mouse.move(x, y)`. These produce events with `isTrusted: true` because they go through `Input.dispatchMouseEvent` / `Input.dispatchKeyEvent` at the Chromium browser level, not from page JS.

Suggested CLI surface:

```bash
# Click at coordinates
adom-desktop browser_input_dispatch '{
  "sessionId":"chip-fetcher",
  "type":"click",
  "x":860,"y":803
}'

# Click via selector (resolve coords from selector's bounding rect, then dispatch)
adom-desktop browser_input_dispatch '{
  "sessionId":"chip-fetcher",
  "type":"click",
  "selector":"button.wg-button--primary"
}'

# Type into a focused input
adom-desktop browser_input_dispatch '{
  "sessionId":"chip-fetcher",
  "type":"type",
  "text":"DMG2305UX-7"
}'

# Press a single key
adom-desktop browser_input_dispatch '{
  "sessionId":"chip-fetcher",
  "type":"key",
  "key":"Enter"
}'

# Mouse move (for human-like trajectories before clicks)
adom-desktop browser_input_dispatch '{
  "sessionId":"chip-fetcher",
  "type":"move",
  "x":400,"y":500
}'
```

### Implementation hints (Node side)

In `plugins/puppeteer/`:

```js
case "browser_input_dispatch": {
  const { sessionId, type, x, y, selector, text, key } = args;
  const page = sessions[sessionId].activePage;

  if (type === "click") {
    if (selector) {
      const handle = await page.$(selector);
      if (!handle) return { error: `selector not found: ${selector}` };
      await handle.click(); // puppeteer's .click() uses Input.dispatchMouseEvent under the hood
      return { ok: true };
    }
    await page.mouse.click(x, y);
    return { ok: true };
  }
  if (type === "type") {
    await page.keyboard.type(text);
    return { ok: true };
  }
  if (type === "key") {
    await page.keyboard.press(key);
    return { ok: true };
  }
  if (type === "move") {
    await page.mouse.move(x, y);
    return { ok: true };
  }
  return { error: `unknown type: ${type}` };
}
```

### Acceptance test

After implementation, this should fire the WAGO submit handler and the popup tab actually opens:

```bash
# Open the WAGO product page
adom-desktop browser_open_window '{
  "sessionId":"test-trusted",
  "profile":"test-trusted",
  "url":"https://www.wago.com/global/p/2601-3105"
}'

# Wait for page to load, then open the modal
sleep 6
adom-desktop browser_eval '{
  "sessionId":"test-trusted",
  "expr":"Array.from(document.querySelectorAll(\"button, a\")).find(e => /^generate datasheet$/i.test((e.textContent||\"\").trim())).click()"
}'

# After modal opens, dispatch a TRUSTED click on the green "Generate data sheet" button
sleep 3
adom-desktop browser_input_dispatch '{
  "sessionId":"test-trusted",
  "type":"click",
  "selector":"button.wg-button--primary"
}'

# Within ~5 seconds, browser_list_tabs should show a NEW tab on wago.priintcloud.com
sleep 5
adom-desktop browser_list_tabs '{"sessionId":"test-trusted"}'
# Expected: tabCount: 2, second tab url contains "priintcloud.com/datasheets/2601-3105"
```

If browser_list_tabs still shows tabCount: 1 after the trusted click, **see Feature 2** — the popup is opening but isn't being tracked.

---

## Feature 2 — auto-track popup tabs in `browser_list_tabs`

### The gap

`browser_list_tabs` only returns tabs that the agent created via `browser_open_tab`. Tabs that the page itself spawns (via `window.open()`, `target="_blank"` form submits, `<a target="_blank">` clicks) are real Chromium targets but invisible to the pup tab list. Result: even if a click does open a new tab, my container-side scraper has no way to find it, switch to it, or eval against it.

### The fix

Listen for `Target.targetCreated` and `Target.targetDestroyed` events from Chromium and keep an internal table of all top-level page targets per session, keyed by tabId. Include them all in `browser_list_tabs` output, with a flag indicating provenance.

Suggested response shape:

```json
{
  "sessionId": "chip-fetcher",
  "activeTabId": "tab-1",
  "count": 2,
  "tabs": [
    {
      "tabId": "tab-1",
      "url": "https://www.wago.com/global/p/2601-3105",
      "title": "PCB terminal block (2601-3105) | WAGO",
      "active": true,
      "opener": "user"
    },
    {
      "tabId": "tab-popup-1",
      "url": "https://wago.priintcloud.com/datasheets/2601-3105/en/abc...",
      "title": "Datasheet - WAGO 2601-3105",
      "active": false,
      "opener": "popup",
      "openerTabId": "tab-1"
    }
  ]
}
```

### Implementation hints (Node side)

In `plugins/puppeteer/sessions/<sessionId>.js`:

```js
// After creating the browser:
browser.on("targetcreated", async (target) => {
  if (target.type() !== "page") return;
  const page = await target.page();
  if (!page) return;
  const tabId = `tab-popup-${++this.popupCounter}`;
  this.tabs.set(tabId, {
    page,
    url: page.url(),
    opener: target.opener() ? "popup" : "user",
    openerTabId: target.opener() ? this.findTabIdForTarget(target.opener()) : null,
  });
});

browser.on("targetdestroyed", (target) => {
  // Remove from this.tabs
});

// list_tabs response builds from this.tabs.values()
```

### Acceptance test

```bash
# After Feature 1 lands, the trusted click on WAGO's Generate button
# should produce a NEW tab visible to browser_list_tabs:

adom-desktop browser_list_tabs '{"sessionId":"chip-fetcher"}'
# Expected: tabCount: 2, with tab-popup-1 entry showing priintcloud.com URL
```

Then I can `browser_eval` against the popup tab's tabId, fetch the PDF binary via in-page `fetch()`, base64 it back, and save — without any API-replay reverse-engineering.

---

## Suggested file/PR structure

- `Cargo.toml` — bump version
- `src/cli.rs` (or wherever the dispatch table lives) — register `browser_input_dispatch` action
- `plugins/puppeteer/index.js` (or per-session module) — implement the input handler + target-tracking listeners
- `plugins/puppeteer/types.d.ts` — extend the tab response shape with `opener` / `openerTabId`
- One integration test per feature (the acceptance tests above are good shapes for those)

A single PR with both features is fine — they go together (popup tracking is most useful when you can produce trusted clicks that spawn popups in the first place).

Once it ships, please bump the version + remind me to upgrade my container's binary so the chip-fetcher skill picks up the new commands.

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