---
name: hd-open-url
description: >
  The decision guide for "open a URL / open a website" inside Hydrogen Desktop.
  When the user says "open ti.com" / "pull up the datasheet page" / "show me
  that site", HD has SIX distinct ways to do it — a Hydrogen webview tab, an HD
  browser window, Pup (AI-drivable Chrome), a native desktop browser profile,
  the Browser Picker chooser, or headless curl/fetch — each with different
  trade-offs. READ THIS to pick the right one and to explain the options back to
  the user instead of silently guessing. For the interception/plumbing behind the
  picker see hd-browser-picker; to drive Pup see pup; for webview tabs see
  adom-workspace-control. Trigger words — open url, open website, open the site,
  open ti.com, open a link, pull up the page, show me the website, view this url,
  open in browser, which browser, how do I open, open the datasheet page, open in
  a tab, open in pup, open in chrome, headless fetch, ways to open a url, open
  link options, wv, open in wv, open as wv, webview tab, wv tab, webview,
  show me in webview, open in webview, in a webview, native webview. NEVER
  satisfy a webview request with VS Code's built-in Simple Browser — see
  hd-webview for the exact command.
---

# 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-url` — `http://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):
```bash
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.

```bash
# 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.
```bash
adom-desktop browser_eval '{"sessionId":"default","js":"document.title"}'
```

**6. Headless content** — if you only need to *read* the page, skip the browser
entirely:
```bash
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):
```bash
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").

```bash
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.

## Related skills
- [hd-browser-picker](../hd-browser-picker/SKILL.md) — 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](../hd-runtime-mode/SKILL.md) — none of this changes between the WSL2 and Docker runtimes
