Download

HD — Opening a URL (your options)

⭐ THE GOVERNING RULE — background by default, foreground only through a justified gate

Whenever YOU (the AI) open a browser window, open it in the BACKGROUND so you never disturb what the user is doing. A window jumping to the foreground steals their focus and covers Hydrogen Desktop. This applies to EVERY path below that opens a real window (HD Native Open, the HD browser window, pup, the native browser).

To bring a window to the FOREGROUND you must pass a foregroundReason — a short justification for why foregrounding is warranted right now. No reason = background. Legit reasons: an AUTH/consent page the user must click NOW; the user explicitly said "bring it up" / "show me now". "It's more convenient" is NOT a reason. HD logs the reason so foregrounding is auditable.

  • POST /open-in-profile / POST /open-url: omit foregroundReason → background (the safe default). Pass foregroundReason:"<why>" to foreground. (/auth/intent URLs are inherently justified and foreground automatically.)
  • When you DO foreground for the user, prefer telling them (an AD notify / a toast) rather than yanking their screen unannounced.

Everything below inherits this rule.

When a user says "open the ti.com website" (or any URL), HD has several paths and a Browser Picker — but don't hand the user the picker to decide. Pick the best destination for them by use-case and open it directly, then offer the alternatives in one line. The right path depends on what they're doing — look at it, have you drive it, keep it as a tab, or just read it. See "Decide for the user — DON'T hand them the picker" below for the rule.

The six ways

# Option Best when… What the user sees AI drives it?
1 Hydrogen webview tab (aka wv) the user wants the site inside HD, next to their work, and to keep it around a panel/tab in the HD workspace partly (reload/route)
2 HD Browser Window a quick popout window owned by HD, not a full browser a standalone HD-owned window no
3 Pup (Puppeteer Chrome) you need to act on the page — screenshot, click, scrape, fill a form, verify a real Chrome window on their desktop yes, fully
4 Native desktop browser real logins/sessions, downloads, "just open it for me", auth flows their own Chrome/Edge profile no
5 Browser Picker you're not sure, or it's an auth/OAuth link — let the user choose (5s auto-default) a chooser dialog n/a (routes to one of the above)
6 curl / fetch (headless) you only need the content, not to show anything (read a datasheet, scrape JSON) nothing — runs in the workspace yes

"wv" = webview — the shortcut (spread it like pup)

⛔ NEVER: VS Code's "Simple Browser" is NOT a webview

If the ask is "show me X in webview", the ONLY correct destination is option 1 (a native Hydrogen webview tab, adom-cli hydrogen webview open-or-refresh). Never open VS Code's built-in Simple Browser / any editor-internal browser page — that happened once from a setup prompt (2026-07-16) and it's a banned outcome. The dedicated hd-webview skill has the exact command + verification.

wv is the short alias for "webview" — recognize it everywhere a user might say "webview" (open it in wv, wv tab, shot the wv, wv mode). Treat wv and webview as identical.

Help it catch on: the first time in a session a user types the long form "webview", open it as normal and add a one-line nudge — "(btw you can just say wv — fewer keystrokes)." Say it once, not every time (don't nag). This is exactly how puppeteer → pup became universal among Adom folks; webview → wv is the same play. (If the user already says wv, they got the memo — skip the nudge.)

How you invoke each

All of these run from inside the HD workspace. The control API base URL is the live value in ~/.adom/hd-control-urlhttp://127.0.0.1:<dynamic> (WSL2 mirrored networking shares loopback with the Windows host; the port is dynamic per launch). Read it with BASE="$(cat ~/.adom/hd-control-url)" then hit "$BASE/<endpoint>".

1. Webview tab (wv) — add a web-view tab pointed at the URL (adom-workspace-control, or adom-cli hydrogen webview open-or-refresh --name <X> --url <url> --panel-id <id>). Good for "keep this open while we work." ⚠️ open-or-refresh needs --panel-id to CREATE a new tab — without it you get "Tab 'X' not found and --panel-id not provided" (verified live 2026-06-14; get a panel id from adom-cli hydrogen workspace get). Re-running with the same --name once it exists just navigates/refreshes (no panel-id needed). Same adom-cli on web-Hydrogen and HD — the consistent surface.

2 / 4 / 5. Through the Browser Picker — the single unifying entry point. POST the URL and HD routes it (auto-picking a sensible default after a 5-second countdown, or showing the chooser):

BASE="$(cat ~/.adom/hd-control-url)"   # http://127.0.0.1:<dynamic>

# Auto-route (picker picks the best default in 5s — hands-free)
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"url":"https://www.ti.com"}' \
  "$BASE/open-url"

# Force the chooser dialog (no auto-timer — make the user pick)
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"url":"https://www.ti.com","force":true}' \
  "$BASE/open-url"

# Route through the picker with the FRESH-WINDOW toggle pre-checked (the picked
# native browser opens in a new --new-window that auto-foregrounds — use for
# auth/OAuth so the consent page pops to the front by itself):
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"url":"https://claude.ai/","fresh":true,"force":true}' \
  "$BASE/open-url"
# …or bypass the picker and open directly+fresh in a specific browser:
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"url":"https://claude.ai/","browser":"edge","profileDir":"Default","fresh":true}' \
  "$BASE/open-in-profile"

⭐ HD Native Open — the LOGGED-IN native browser, in the BACKGROUND (no extension needed)

When you need the user's REAL, logged-in browser (adom-google auth, opening a Google Sheet after auth, driving chip-fetcher or any signed-in site BEFORE the Adom extension/abe is installed), use HD Native Open: POST /open-in-profile in a specific profile so it carries their login, picker-silent, and, crucially, background:true so the window sinks behind HD instead of covering it.

# 1. find the right logged-in profile (never assume — the user may have several accounts)
curl -s "$BASE/browser-profiles"     # → [{browser, profileDir, email, ...}]

# 2. open it in the BACKGROUND in that profile (does NOT steal focus / cover HD)
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"url":"https://docs.google.com/spreadsheets/d/<id>","browser":"chrome","profileDir":"Default","background":true}' \
  "$BASE/open-in-profile"
  • Background is the DEFAULT (per the governing rule) — you don't even need to pass background:true. Omit foregroundReason and the window opens behind HD. A native browser jumping to the foreground over HD is jarring (it happened on setup). To foreground, pass foregroundReason:"<why>" (HD logs it). AUTH consents (/auth/intent, oauth/authorize, claude.ai/login) and a user-chosen picker open foreground automatically — no reason needed.
  • ALWAYS pass a profileDir from /browser-profiles so the window carries the user's login. browser:"default" with no profile lands them logged-OUT.
  • NEVER open a signed-in page with a bare OS-default open (open::that) or the VS Code Simple Browser — those lose the profile/login and can't be backgrounded. HD Native Open is the correct tool.
  • Distinct from pup (logged-OUT, HD-owned Chromium) and abe/nbrowser (logged-in but needs the extension, not present during setup).

⚠️ /open-url often replies "HD timed out after 5s" even when it SUCCEEDED (verified live 2026-06-14: {"url":"https://www.google.com"} reported the timeout, but the page opened in Edge). Don't treat that as failure — confirm with GET "$BASE/browser-picker/last-open"{destination, url, fresh, ts}. fresh:false = routed to a remembered/default destination (no picker shown — e.g. browser:edge:Default); fresh:true = the picker was shown. (So if every domain has a remembered default, opens are silent — the picker only pops for un-remembered domains.)

The picker's auto-default heuristic: *.claude.ai/*.claude.com and Adom /auth/intent → the user's work browser profile; unknown/right-click/iframe → Pup; otherwise the user's last choice for that domain. See hd-browser-picker.

Fresh window ("fresh":true) — the native-browser path (#4) can open the URL either as a tab in an already-running browser (default) or as a brand-new window/ process (--new-window). fresh is about window-vs-tab; it is NOT a foreground switch — the governing rule still applies, so an AI fresh open is demoted to the background unless it's an auth consent or you passed a foregroundReason. (Windows tries to auto-foreground a freshly-launched window; HD sinks it back down when the open isn't justified.) So for auth flows the user must complete (Claude/Adom sign-in) the consent comes to the front by itself (auth is the justified case); for everything else, fresh without a reason stays background. The Browser Picker defaults the "fresh window" toggle ON for auth URLs. On /open-url, fresh pre-seeds that toggle (the picker still shows; add direct:true to skip it); /open-in-profile opens fresh directly. See hd-browser-picker.

3. Pup — open and then drive it with adom-desktop browser_* verbs (browser_open_window, browser_navigate, browser_screenshot, browser_eval, browser_reload, …). Either open the URL directly via Pup, or let the picker route to Pup. See the pup skill for the full verb set.

adom-desktop browser_eval '{"sessionId":"default","js":"document.title"}'

6. Headless content — if you only need to read the page, skip the browser entirely:

curl -sL https://www.ti.com/...   # or fetch from your own code

Decide for the user — DON'T hand them the picker

Default behavior: pick the destination by use-case and open it DIRECTLY (bypass the picker), then give a one-line heads-up + easy undo. Do NOT fire a plain POST /open-url {url} and let the picker's generic 5-second auto-default decide — that default is luck-of-the-draw and often wrong for the use-case (a vendor-login site could land in Pup where the user has no login). Users don't want to make this choice; make the smart one for them. (Mechanics: /open-in-profile opens a specific native browser+profile directly; /open-url {direct:true} bypasses the picker; Pup and webview-tab are direct calls. Reserve the picker only as below.)

Use case Open it in How (bypass the picker)
Login-likely site — vendor/account/portal/cart: TI, Mouser, DigiKey, any "my…"/sign-in the user's native browser + WORK profile (their saved login/cookies live there) GET /browser-profiles → find the work profile (non-gmail) → POST /open-in-profile {url, browser, profileDir, fresh:true}
Building/testing their own app, or you need to drive/screenshot/scrape the page Pup (AI-drivable) open via Pup (browser_*) / route /open-url {url, direct:true} to Pup
Glance at content alongside their work Hydrogen webview tab adom-workspace-control add web-view tab
Genuinely ambiguous, OR an OAuth/sign-in the user must complete themselves let them choose POST /open-url {url, force:true} — the one time the picker is right

Then say it in one line, with the undo: "Opened TI in your Chrome work profile (where your TI login lives) — say the word for a tab here, Pup, or a different profile." Decide, act, offer alternatives — don't ask first.

Example — "open the texas instruments website" (login-likely → native work profile):

BASE="$(cat ~/.adom/hd-control-url)"
curl -s "$BASE/browser-profiles"     # pick the work profile (non-gmail email)
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"url":"https://www.ti.com","browser":"chrome","profileDir":"<work profileDir>"}' \
  "$BASE/open-in-profile"

Govern routing policy — the picker is now a true FALLBACK (HD prefs API)

The picker's per-domain memory is a first-class API (HD app behavior), so the AI OWNS URL routing — the picker only pops for a domain with no stored policy AND no direct call. Use this to set policy the user would otherwise click through the Manager for ("always open DigiKey in my work profile", "stop auto-opening X in Edge").

BASE="$(cat ~/.adom/hd-control-url)"
# READ the policy map
curl -s "$BASE/browser-picker/prefs"
#   → {ok, prefs:{domains:{"<host>":{destination,label}, …}}}
# SET / override one domain
curl -s -X PUT "$BASE/browser-picker/prefs" -H 'Content-Type: application/json' \
  -d '{"domain":"digikey.com","destination":"browser:edge:Default","label":"Edge — Default"}'
# FORGET one domain  (e.g. "stop auto-opening X in Edge")
curl -s -X DELETE "$BASE/browser-picker/prefs/digikey.com"
# CLEAR all
curl -s -X DELETE "$BASE/browser-picker/prefs"
  • Stored value = {destination, label} — NOT profileDir/fresh.
  • destination is an ID:
    • internal: hydrogen-tab (a webview/wv tab) · webview-window · pup-window
    • native: browser:<browser>:<dir> — e.g. browser:edge:Default, browser:chrome:Profile 1. The profile dir is encoded in the ID (no separate field).
  • Discover valid native IDs via GET $BASE/browser-profiles (maps each detected profile → its browser+dir); the 3 internal IDs are fixed above.
  • fresh is NOT stored in prefs — it's a per-OPEN flag (/open-url {fresh:…} / /open-in-profile {fresh:…}). Don't try to persist it here.
  • Writes hit the live Manager UI immediately (the backend writes hd-url-routing-prefs localStorage + dispatches hd-url-routing-prefs-changed; an open Browser Picker Manager re-reads at once, and re-reads on open anyway).

Net: for a one-off open, decide and call the direct endpoint (/open-in-profile, /open-url {direct:true}, or a wv/pup call). For a standing preference, PUT the domain's policy so even user-initiated link clicks route there silently. The picker is now only a fallback for the genuinely-undecided.

  • hd-browser-picker — the interception layers + picker dialog behind #2/#4/#5
  • pup — driving Pup browser windows (#3): navigate, screenshot, eval, reload
  • adom-workspace-control — adding/managing Hydrogen webview tabs (#1)
  • hd-runtime-mode — none of this changes between the WSL2 and Docker runtimes