name: installing-the-extension description: "How WE (this repo) install the Adom browser extension into the user's real Chrome/Edge in DEVELOPER MODE ("Load unpacked"), driving adom-desktop's GENERIC OS verbs. This is OUR orchestration, not an adom-desktop feature - AD 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-browser-extension."

Part of adom-browser-extension. 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. adom-desktop 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). AD 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>\adom-browser-extension\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 AD'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 adom-desktop, so AD guarantees its Node (v1.9.63+ runtime contract: system Node if on PATH, else a portable no-UAC copy AD provisions). But the native-messaging host is launched by CHROME, not AD, so it does NOT inherit AD'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 AD 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: an AD 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 AD for the managed Node path and drop it in yourself -

adom-desktop 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 - AD 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 (AD 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 AD 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>\\adom-browser-extension\\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 AD 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 - AD 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.