---
name: native-browser-control
description: >
  Drive the user's REAL logged-in Chrome or Edge from the cloud via the Adom
  browser extension (CDP), in the background, no focus steal. Use to click/scroll/
  read a page in their actual browser, upload a file, or log them in through OAuth
  using their existing sessions. Trigger words: nbrowser, drive my chrome, drive
  edge, real browser, logged-in browser, upload to a website, sign in with google,
  oauth login, CDP eval browser.
---

# Native browser control (nbrowser)

`nbrowser_*` drives the user's **real, logged-in Chrome/Edge** through the
`adom-browser-extension` over CDP. Because it's their actual browser, their cookies
and logins are already there, so a JLCPCB/Autodesk/Google flow "just works" with no
credentials handed over. It runs in the **background** (no focus steal).

## Core verbs

```bash
adom-desktop nbrowser_status '{}'                                 # {ok, ext, browser:"chrome"|"edge"}
adom-desktop nbrowser_list_tabs '{}'                             # all tabs (tabId, url, windowId, active)
adom-desktop nbrowser_open_tab '{"url":"https://..."}'          # opens a DEDICATED tab, returns tabId
adom-desktop nbrowser_eval '{"tabId":"<id>","expr":"<js>"}'     # run JS in that tab (background)
adom-desktop nbrowser_screenshot '{"tabId":"<id>"}'            # page PNG as base64 (background)
adom-desktop nbrowser_navigate '{"tabId":"<id>","url":"..."}'
```

## Chrome vs Edge, this trips everyone

`nbrowser` defaults to **Chrome**. To drive Edge, pass `"browser":"edge"` on EVERY
verb. `nbrowser_list_tabs` without it shows only Chrome tabs.

```bash
adom-desktop nbrowser_list_tabs '{"browser":"edge"}'
adom-desktop nbrowser_eval '{"browser":"edge","tabId":"<id>","expr":"location.href"}'
```

## Gotchas (each cost a real debugging session)

- **It refuses to drive PWA / installed-app tabs** (e.g. a taskbar Gmail/Chat PWA) to
  avoid hijacking a standalone app. Open a **dedicated tab** with `nbrowser_open_tab`.
- **A first `eval` can return `null`** right after opening a tab (timing). Retry; it works.
- **No frame-targeting.** `eval` runs in the TOP frame only. Params like `allFrames`,
  `frameUrl`, `frame` are **ignored** (they silently return the top frame). You CANNOT
  reach into a **cross-origin iframe** (`iframe.contentDocument` throws / `accessible:false`).
  This is why deeply-nested webviews (the Hydrogen Claude panel) are unreachable, see
  [hydrogen-web-control](../hydrogen-web-control/SKILL.md).
- `nbrowser_eval` runs in the PAGE context, so JS can click, scroll, read, and set
  inputs, but only same-origin.

## Upload a file (DataTransfer, no OS picker)

CDP can't touch the native file dialog, so inject the bytes and set the
`<input type=file>` directly. Base64 the file, **stream it into the page in <40k
chunks** (the CLI arg cap), then build a `File` and set it via `DataTransfer`:

```bash
# per chunk: comma operator = ONE expression (nbrowser_eval requires a single expression)
adom-desktop nbrowser_eval '{"tabId":"<id>","expr":"(window.__b += \"<chunk>\", window.__b.length)"}'
# then decode + set the input:
adom-desktop nbrowser_eval '{"tabId":"<id>","expr":"(function(){var s=atob(window.__b),u=new Uint8Array(s.length);for(var i=0;i<s.length;i++)u[i]=s.charCodeAt(i);var f=new File([u],\"board.zip\",{type:\"application/zip\"});var inp=document.querySelectorAll(\"input[type=file]\")[0];var dt=new DataTransfer();dt.items.add(f);inp.files=dt.files;inp.dispatchEvent(new Event(\"change\",{bubbles:true}));return inp.files[0].name})()"}'
```

The input often **clears itself right after** the change event (the site read the file
already), that is NORMAL, don't treat the empty input as failure. Verify by screenshot.

## Drive an OAuth login yourself (NEVER ask the user)

You can log the user in end to end. Proven on JLCPCB "Sign in with Google":

1. Click the page's "Sign in with Google" button (`nbrowser_eval` `.click()`).
2. On `accounts.google.com`, **CDP eval WORKS** (it's not blocked). Click the user's
   account row, then the OAuth **Continue** button, both via `eval` `.click()`.
3. It redirects back logged in; any file you already uploaded persists in the session.

This is one-time per site; afterwards the session stays logged in and every run is
fully background. Pick the user's **primary work** Google account if there's a chooser.
NEVER ask the user to click or log in, you can do all of it.

## When CDP eval can't reach a control

If a page genuinely blocks eval on an element, screenshot the window, map the pixel,
and fall back to a foregrounded OS click, see [desktop-ui-control](../desktop-ui-control/SKILL.md).
But try eval first, it's background and doesn't disturb the user.

Related: [pup-browser-control](../pup-browser-control/SKILL.md) (anonymous browsers),
[screen-recording](../screen-recording/SKILL.md) (recording a live drive).
