---
name: installing-the-extension
description: "How WE (this repo) install the Adom Native Browser Extension into the user's real Chrome/Edge in DEVELOPER MODE (\"Load unpacked\"), driving Bridge's GENERIC OS verbs. This is OUR orchestration, not a Bridge feature - ab only exposes primitives (desktop_navigate / desktop_find_control / desktop_ui_click / desktop_ui_set / desktop_screenshot); the recipe, hardening, and recovery live here. This is the DAY-ONE install path: Chrome Web Store / Edge Add-ons review is ~2 months out, so until then dev-mode load-unpacked is how a real user gets the extension. Use when the user says \"install the extension\", \"install it for me\", \"load the extension into my browser\", \"set up the browser extension\", \"the extension isn't loaded yet\". For the low-level click mechanics (popups, the PWA guard, uninstall/reload), see driving-the-desktop. Part of the adom-native-browser."
---
Part of **adom-native-browser**. Low-level OS-driving mechanics: **driving-the-desktop**. Safety rules: **native-browser-safety**.

# Installing the extension (dev-mode Load unpacked) - the orchestration WE own

> **Whose code is this?** Ours. Bridge stays **generic** - it only gives us OS primitives
> (`desktop_navigate`, `desktop_find_control`, `desktop_ui_click`, `desktop_ui_set`,
> `desktop_screenshot`). The sequence that turns those into "the Adom extension is now loaded and
> connected" is **this skill** (plus the repo's `install/` scripts). ab never grows a "dev-mode
> install" verb; we own and harden the recipe. **This is the day-one path** until the Chrome Web Store /
> Edge Add-ons listings clear review (budget ~2 months for the broad-permission set), so it has to be
> clean and repeatable, and we keep iterating it.

## What makes this tractable: the extension ID is deterministic
The extension's `manifest.json` pins a `key`, so its ID is the **same on every machine**
(`nncjgceobmlenfliamnibncjojpedbcc`). That means the native-messaging **host manifest's
`allowed_origins` is known ahead of time** (`host/host-manifest.json` already hardcodes it) - we do
**not** have to capture the ID at install time to wire the host. So the only genuinely hard part is
getting "Load unpacked" to pick the right folder; everything else is deterministic.

## Preconditions (do these once, before driving any UI)
1. **The `extension/` folder is on the laptop** at a known path (e.g. `C:\Users\<user>\nb-bridge\extension`). Get it there from the package/repo (`push_file` the folder, or the user clones it). Load-unpacked reads the **on-disk** build, so this path is what the picker must select.
2. **The native-messaging host is registered** (`install/install.ps1`, or its steps): host manifest written with `allowed_origins` = the pinned ID, and `HKCU\...\NativeMessagingHosts\inc.adom.native_browser` pointing at it (Chrome and/or Edge). No UAC.
3. **The bridge is installed** in ab's cache (`bridge_install`, once). See the install-flow doc.

### ⚠ The host's Node - do NOT assume system Node exists (SDK runtime contract)
The bridge (`server.js`) is spawned by Bridge, so **ab guarantees its Node** (the SDK runtime
contract: system Node if on PATH, else a portable no-UAC copy ab provisions). **But the
native-messaging host is launched by CHROME, not ab**, so it does NOT inherit ab's managed-runtime
PATH. A machine with **no system Node** would then fail to start the host (`'node' is not recognized`)
even though the bridge is healthy - the extension would connect and immediately drop.

This is handled automatically, and you usually do nothing:
- **The bridge publishes the exact Node ab gave it** (`process.execPath`) to
  `~/.adom/bridges/native-browser/node-path.txt` every time it starts (bridge >= 0.0.22).
- **`native-host.bat` reads that file first**, falls back to `node` on PATH only if it's absent. Since
  the bridge always runs before the extension is installed, the file is already there by first host
  launch. Self-healing: a Bridge Node upgrade rewrites it on the next bridge respawn.

**If you ever need to write it explicitly** (e.g. a repair, or the host somehow launched before the
bridge ever ran): ask ab for the managed Node path and drop it in yourself -
```
adom-bridge-cli runtimes                     # -> runtimes.node.exe (source: system|cache), e.g. C:/Program Files/nodejs/node.exe
# then write that absolute path (one line, no newline games) to:
#   %USERPROFILE%\.adom\bridges\native-browser\node-path.txt
```
Never `winget install node` / prompt the user to install Node - ab already has one; reuse it.

## ⭐ Verify by SCREENSHOT, not by control state (the rule for the whole flow)
`desktop_find_control` / UIA state is a fast **hint** and it lies often enough to burn you: a toggle can
read ambiguously, the folder picker can select the wrong directory silently, and **Load unpacked can
"succeed" with a broken manifest** (the card shows a red **Errors** button) - none of which a
`find_control` "is it there?" check catches. So at **every** checkpoint below, take a real screenshot
and **actually read it** before you believe the step worked:
```
desktop_screenshot_window {hwnd:<W>}     # WGC, BACKGROUND - works on the privileged chrome:// page that
                                         # the extension's own CDP cannot shoot (and it isn't loaded yet)
```
Then look at the image: is Developer mode visibly ON? did the picker show the right path? is the Adom
card in the list, **Enabled, with NO Errors button**? Do not assert success off control state alone.
**Tip (verified):** shooting the BROWSER hwnd returns a top-level `screenshots[]` array of its **owned
popups** too - so when the folder picker is open, `desktop_screenshot_window {hwnd:<browser>}` hands
back the page AND the picker (`screenshots[0]` = the `#32770` dialog) in **one call**; you don't need
the picker's hwnd to read it.

## ⭐ Narrate to the user with on-screen captions (hand-hold them)
This runs on the USER's screen and some steps are fragile or need them. Keep them oriented with
`desktop_caption {text:"..."}` - a large, always-on-top, click-through desktop banner - at each step:
what you're doing ("Adom: turning on Developer mode", "Adom: selecting the extension folder...") and
especially **what THEY must do** ("Click Select Folder to finish", "Paste the path with Ctrl+V"). Clear
it when done. Treat the install as a guided, narrated process, never a silent one.

## ⛔⛔ NEVER force the browser to the foreground (rude AND it corrupts your own work)
Foregrounding the user's browser - `desktop_bring_to_front`, OR any SendInput verb (`desktop_type`,
`desktop_press_key`, `desktop_click`) which foreground the target first - is **disruptive and actively
dangerous while the user is working**. It steals focus, so **the user's very next keystroke lands in
whatever control YOU just focused** - e.g. the extensions **Search box** - which silently changes your
filter and **hides the card you were acting on**. Learned the hard way: while foregrounded, the user kept
typing and the search became `Adoms` / `Adom for Chromeing`, so the Adom card vanished and the flow
derailed. It is also just rude - they are mid-task on their own machine.

Rules, in order:
- **Drive entirely with BACKGROUND verbs** - `desktop_ui_click` (UIA Invoke), `desktop_ui_set` (SetValue),
  `desktop_navigate`, `desktop_screenshot_window`. **None steal focus**, so the user keeps typing safely
  and your control state can't be corrupted by their keystrokes. This covers almost the entire flow.
- The only steps that might tempt a foreground are the **folder-picker path-fill** and the
  **Remove-confirm bubble**. Do NOT reach for `bring_to_front`/SendInput by default - use the background
  or clipboard paths (below) first.
- **If a step truly cannot be done in the background**, treat foreground as a last resort: `desktop_caption`
  the user to pause typing for a moment, do the single action, and return immediately. Never hold the
  foreground, never do it silently, and never do it while the user is actively typing if there is any
  other way.
- ✅ **`desktop_navigate` is now focus-neutral (ab 1.9.34+).** It restores your prior foreground window
  after committing and returns an honest `navigated` signal (+ `titleChanged`). Verified live: a
  `chrome://extensions` navigate committed (`navigated:true`, tab title "Extensions") while my window
  stayed at `z:1` and the user's window kept `z:0`. (On OLDER ab it foregrounded - the general principle
  still holds: judge foreground by the **outcome**, `desktop_list_windows` `z:0` = topmost = you surfaced
  it.) Still screenshot-verify the page actually rendered, not just the `navigated` flag.

## The flow (background; never foreground the user's work)
Open a fresh background window you own, get its `hwnd`, and drive **that** (never the user's tabs):
```
nbrowser_open_window {sessionId:"install", thread:"<you>", purpose:"driving chrome://extensions to load the extension", profile, url:"about:blank"}   # your own window; note hwnd <W>
desktop_navigate {hwnd:<W>, url:"chrome://extensions"}             # edge://extensions for Edge - scheme MUST match the browser
desktop_screenshot_window {hwnd:<W>}                               # verify the page loaded (posted Enter is app-specific)
```

### Step A - ensure Developer mode is ON (verify it VISUALLY)
The page's **Developer mode** is a toggle (top-right on Chrome, bottom-left on Edge).
```
desktop_find_control {hwnd:<W>, contains:"Developer mode"}         # fast hint + best.rect of the toggle
desktop_screenshot_window {hwnd:<W>}                               # GROUND TRUTH - read it: is the toggle ON?
```
⚠️ **Verified:** the "Developer mode" control is `invokable:false`, so `desktop_ui_click` will NOT flip
it. If the screenshot shows it OFF, click its **rect** with SendInput (foreground, brief):
```
desktop_click {x:<best.rect.centerX>, y:<best.rect.centerY>}      # toggle it (the label/toggle isn't UIA-invokable)
desktop_screenshot_window {hwnd:<W>}                              # confirm it flipped AND Load unpacked / Pack / Update appeared
```
Do not proceed until the screenshot shows Developer mode ON and the **Load unpacked** button visible. If
the row didn't appear, the toggle didn't flip - re-read and retry, don't proceed blind.

### Step B - Load unpacked -> the native folder picker (WORKING method, verified live 2026-06-28)
**Run this as a TIGHT SCRIPT** (back-to-back calls, minimal sleeps) - a slow step-by-step with a caption
"sitting there" while you idle is infuriating to the user. The picker fill is **agent-driven and fast**.
⛔ **Never ask the user to type or paste the path** (it is ~50 chars - awful UX), and **do not rely on
the clipboard** (see note). The whole trick is one line: **bring the picker to the front before typing.**

```
desktop_caption  {text:"Adom: installing - 2 seconds, please dont type"}    # warn ONCE, briefly
desktop_ui_click {hwnd:<W>, automationId:"loadUnpacked"}                    # opens the picker (#32770) - it appears BEHIND the user's windows
# ensure exactly one picker; grab its hwnd <PK> via desktop_list_windows (cancel extras)
desktop_bring_to_front {hwnd:<PK>}                                          # ⭐ THE KEY: picker opens at z:1/z:2; SendInput only lands once it is z:0
desktop_press_key {hwnd:<PK>, keys:"ctrl+l"}                                # focus the picker address bar
desktop_type      {hwnd:<PK>, text:"C:\\Users\\<user>\\nb-bridge\\extension"}   # agent types the path - fast
desktop_press_key {hwnd:<PK>, keys:"enter"}                                 # navigates INTO the folder
desktop_screenshot_window {hwnd:<PK>}                                       # VERIFY breadcrumb shows ...\extension BEFORE committing
desktop_ui_click  {hwnd:<PK>, automationId:"1"}                             # commit (Select Folder = IDOK) -> page toasts "Extension loaded"
desktop_caption  {text:""}                                                  # clear the caption immediately
```

⚠ **Select Folder may need a REAL click, not a UIA invoke (verified live on Edge / Win11, ADOMBASELINE
2026-07-03).** The picker is a Win32 common dialog (`#32770`), and on Edge its **Select Folder / IDOK
did NOT respond to `desktop_ui_click` (UIA Invoke) - the call returned `success:true` but the dialog
stayed open.** A foreground **SendInput** click closed it. So: after verifying the breadcrumb, try the
UIA click; if `desktop_list_windows` still shows the `#32770` open, fall back to
`desktop_find_control {hwnd:<PK>, contains:"Select Folder"}` -> `desktop_bring_to_front {hwnd:<PK>}` ->
`desktop_click {x:<best.rect.centerX>, y:<best.rect.centerY>}` (SendInput). ALWAYS confirm the picker is
gone (`#32770` count == 0) before believing it committed.

⚠ **The subfolder-drill trap.** After `ctrl+l` -> type path -> Enter you are now standing INSIDE the
extension folder (breadcrumb `...\extension`, Folder field = "extension", list shows the extension's own
`icons/`, `src/`, `manifest.json`). Commit from HERE. Do NOT let a subfolder get selected first - if the
Folder field or breadcrumb reads `...\extension\src`, Select Folder loads `src` (no manifest -> silent
no-load, or a manifest error). If you drilled too far, re-run `ctrl+l` + the full path + Enter to reset
to `...\extension`, re-screenshot, THEN commit.

**Edge vs Chrome are identical here** except the URL scheme (`edge://extensions`) and, on a fresh box, the
Developer-mode toggle is a shadow-DOM switch not exposed as settable - front the window and
`desktop_click` its screen coords (just right of the "Developer mode" label) to flip it.

**Why `bring_to_front` is the whole game:** the picker opens BEHIND the user's active windows (observed
`z:1`/`z:2`). SendInput without fronting it first sends the keys to the USER's foreground window (their
editor) - they never reach the picker (learned the hard way: a stray paste/Enter into the user's VS
Code). Front the picker (it goes `z:0`), and `ctrl+l` -> `type` -> `enter` -> `Select Folder` works
every time. This is a brief (~2s) **necessary** foreground - caption a "don't type" warning, do it fast,
clear the caption. (Step 1's `desktop_navigate` is focus-neutral on ab 1.9.34+, so this picker fill is
the ONLY foreground in the whole install.)

⛔ **Do NOT** use the clipboard for this: `desktop_clipboard_set` did **not** stick on a real machine
(`set` -> `get` returned `""`), so a "paste the path" handoff leaves the user with an empty clipboard.
Agent-types-it-after-fronting is the method. (Clipboard verbs exist - ab 1.9.35 - but are unreliable for
this; flagged.)

**Consent / handoff with notify (use it for the ONE gate, not the path):** before driving,
`notify_user {id, title, body, buttons:[{label:"Start"}], scenario:"reminder"}` (sticky toast + action
button) and poll `notify_response {id}` until they click Start. Verified round-trip. Use it to get a go
-ahead, then run the fast script above - never to ask them to type a path.

**Gotchas (live):**
- **Load unpacked can open MULTIPLE pickers** (saw 2). `desktop_list_windows`, ensure exactly one `#32770`
  "Select the extension directory.", cancel extras.
- **Cancel = `desktop_ui_click {hwnd:<PK>, contains:"Cancel"}`** (the name; `automationId:"2"` reported
  success but did NOT close it). Likewise prefer `contains:"Select Folder"` over `automationId:"1"`.
- **ALWAYS screenshot + READ the path BEFORE committing** - the wrong folder loads a broken/old build
  silently.

### Step C - verify it loaded clean AND connected (screenshot FIRST, three checks)
1. **Screenshot the extensions list and READ it** - this is the check that actually matters:
   ```
   desktop_screenshot_window {hwnd:<W>}     # then look at the image
   ```
   Confirm in the image: (a) the **Adom** card is present, (b) it is **Enabled** (toggle on), and (c) it
   shows **NO red "Errors" button**. ⛔ If an **Errors** button is present, the extension loaded
   **broken** (bad manifest / SW threw) - it is NOT installed successfully even though the card exists.
   Open it (`desktop_ui_click {hwnd:<W>, contains:"Errors"}`), screenshot, read the actual error, fix
   it, and reload. A card-present-with-errors is the most common false "success" - the screenshot is the
   only thing that catches it.
2. **Card present (fast secondary):** `desktop_find_control {hwnd:<W>, contains:"Adom"}`. Optional: read
   the card's ID and confirm it equals the pinned ID.
3. **Connected to the bridge:** poll `nbrowser_status` / `nbrowser_profiles` - the `chrome:`/`edge:`
   key appears (and the popup shows "connected") within ~1-3s once the SW starts. A loaded-but-not-
   connected extension usually means the profile has no open window (SW asleep) or the host manifest /
   HKCU registration is missing or has the wrong ID - check the preconditions.

### Step D - first useful action
Drive a real verb in a window **you** open (see `native-browser-safety`): e.g.
`nbrowser_open_window {sessionId:"verify", thread:"<you>", purpose:"verify the extension drives", url:"https://www.digikey.com"}` -> `nbrowser_screenshot`.

## Chrome AND Edge
They are independent. Repeat the flow per browser (matching scheme: `chrome://` vs `edge://`; the toggle
sits top-right on Chrome, bottom-left on Edge). The host is registered per browser, so each connects its
own host. Load into whichever the user wants, or both.

## Recovery / common failures
- **Load unpacked missing** -> Developer mode is off (Step A) or the page didn't navigate (re-`desktop_navigate`, verify by screenshot).
- **Picker selected the wrong folder** -> the silent killer; always screenshot the picker path before Select.
- **Card present but never connects** -> SW asleep (open a window in that profile), or host not registered / wrong `allowed_origins` ID (re-run `install/install.ps1`).
- **"refusing to drive a PWA / installed-app window"** -> you grabbed an installed-app window; open a fresh `normal` window (see the PWA guard in `driving-the-desktop`).

## Honest status
The end-to-end recipe works; the **folder-picker path-fill** is the part we are actively making robust
(a deterministic helper instead of "remembers the last dir"). Until the Web Store listings clear review,
this dev-mode flow is the real install path, so getting it clean is a priority - report any picker
flakiness so we can harden the helper.
