bootstrap
Adom Hydrogen (Windows / WSL2)
Public Made by Adomby adom
The Windows (WSL2) platform layer for Hydrogen.
← Commit history
Publish 0.3.12
33 files changed
+3680−334
install.sh+2−2package.json+2−2page.json+2−2skills/hydrogen-ai-threads-api/SKILL.md+288skills/hydrogen-ai-threads/SKILL.md+64skills/hydrogen-api/._SKILL.mdskills/hydrogen-api/SKILL.md+412skills/hydrogen-browser-picker/SKILL.md+479skills/hydrogen-captions/._SKILL.mdskills/hydrogen-captions/SKILL.md+135skills/hydrogen-container-stats/SKILL.md+29−49skills/hydrogen-container/SKILL.md+28−29skills/hydrogen-desktop-sse/._SKILL.mdskills/hydrogen-desktop-sse/SKILL.md+241skills/hydrogen-file-transfer/SKILL.md+92skills/hydrogen-golden-image/SKILL.md+86skills/hydrogen-notifications/._SKILL.mdskills/hydrogen-notifications/SKILL.md+125skills/hydrogen-open-url/._SKILL.mdskills/hydrogen-open-url/SKILL.md+199skills/hydrogen-permissions/._SKILL.mdskills/hydrogen-permissions/SKILL.md+69skills/hydrogen-ports/SKILL.md+42−6skills/hydrogen-recording/SKILL.md+408skills/hydrogen-screen-lock/SKILL.md+89skills/hydrogen-self-screenshot/._SKILL.mdskills/hydrogen-self-screenshot/SKILL.md+236skills/hydrogen-setup-steps/SKILL.md+92−63skills/hydrogen-setup/SKILL.md+98−102skills/hydrogen-ui/._SKILL.mdskills/hydrogen-ui/SKILL.md+231skills/hydrogen-wake-repair/SKILL.md+144skills/hydrogen-workspace-monitoring/SKILL.md+87−79install.sh+2−2@@ -16,10 +16,10 @@ HERE="$(cd "$(dirname "$0")" && pwd)" # 0. Deploy the bundled WSL2-specific hd-* skills into ~/.claude/skills/. if [ -d "$HERE/skills" ]; then- log "deploying bundled WSL2 hd-* skills"+ log "deploying bundled Windows/WSL2 skills (hydrogen-* windows layer + hd-* legacy)" install -d -m 0755 "$HOME/.claude/skills" count=0- for d in "$HERE"/skills/hd-*/; do+ for d in "$HERE"/skills/*/; do [ -f "${d}SKILL.md" ] || continue name="$(basename "$d")" install -d -m 0755 "$HOME/.claude/skills/${name}"
package.json+2−2@@ -4,7 +4,7 @@ "type": "bootstrap", "title": "Adom Hydrogen (Windows / WSL2)", "brief": "The Windows (WSL2) platform layer for Hydrogen.",- "version": "0.3.11",+ "version": "0.3.12", "description": "The Windows platform layer for Hydrogen, where the workspace runs in a WSL2 Ubuntu distro. Depends on adom/hydrogen-bootstrap (the platform-generic layer, which bundles the generic hydrogen-* skills) and adds everything specific to the WSL2 runtime: the WSL2/networking/lifecycle hydrogen-* skills (BUNDLED here by design, John+Kyle 2026-07-16) and the WSL2 code-server workbench seed. Sibling adom/hydrogen-mac-bootstrap carries the parallel mac skills. This is the package the golden WSL2 image installs.", "org": "adom", "platform": "linux",@@ -19,7 +19,7 @@ "install": "./install.sh" }, "dependencies": {- "adom/hydrogen-bootstrap": "^0.4.0",+ "adom/hydrogen-bootstrap": "^0.5.0", "adom/adom-shotlog": "^0.27.1", "adom/adom-bridge": "^2.0.0" },
page.json+2−2@@ -4,7 +4,7 @@ "slug": "hydrogen-windows-bootstrap", "title": "Adom Hydrogen (Windows / WSL2)", "brief": "The Windows (WSL2) platform layer for Hydrogen.",- "version": "0.3.11",+ "version": "0.3.12", "author": { "name": "John Lauer", "email": "[email protected]"@@ -27,7 +27,7 @@ "install": "./install.sh" }, "dependencies": {- "adom/hydrogen-bootstrap": "^0.4.0",+ "adom/hydrogen-bootstrap": "^0.5.0", "adom/adom-shotlog": "^0.27.1", "adom/adom-bridge": "^2.0.0" },
skills/hydrogen-ai-threads-api/SKILL.mdadded+288@@ -0,0 +1,288 @@+---+name: hydrogen-ai-threads-api+description: >+ How to open and drive AI conversations in Hydrogen from your own app, panel, or script:+ open a new thread (letting the user's preferred AI decide), list what is open, put text+ in a chat box, send a prompt and confirm it landed, reopen a past conversation, and close+ a tab. Plain HTTP on Hydrogen's control API, no Tauri IPC and no special privileges.+ Read this before shelling out to a CLI or driving the editor UI yourself. Trigger words -+ open an AI thread, new claude thread, new codex thread, new kimi thread, ai threads api,+ send a prompt to an AI, put text in the chat box, list open AI conversations, reopen a+ conversation, close an AI tab, default AI, which AI should I use, dock opens a thread.+---++# Driving Hydrogen's AI threads from your own app++Hydrogen keeps every open AI conversation (Claude, Codex, Kimi) in one live tree and exposes+it over its control API. **Anything that can reach that port can do everything the AI Threads+panel does** — the panel itself is a plain HTTP client with no privileged access, so there is+no capability you are missing.++## Find the API++The control port is dynamic (it drifts when 47084 is busy), so never hardcode it.++Inside the workspace:++```bash+BASE="$(cat ~/.adom/hydrogen-control-url)" # e.g. http://127.0.0.1:55038+curl -s "$BASE/ai-threads" | jq .+```++From a Hydrogen panel or webview, ask your own origin: `fetch('/ports')` returns the resolved+`control` port. If neither is available, the discovery responder answers on the first+bindable of `47080, 48080, 49180, 50280` and returns the live `control_url`.++## From Adom Bridge or the Windows host++The same discovery works OUTSIDE the workspace (an Adom Bridge caller, a dock card, a+wiki page acting through ab): read `%APPDATA%\hydrogen-desktop\ports.json` for the live+`control` port, or probe the discovery responder (`GET http://127.0.0.1:<c>/` for c in+47080, 48080, 49180, 50280; the first answer carries `control_url`). Then call the same+`/ai-threads/*` routes. Requires Adom Hydrogen 1.0.141 or newer. Note: while the AI+Threads panel is off (its default), the first driving call wakes the machinery for that+session; read-only census calls while parked return `ok:false` by design.++## Open a new thread++```bash+curl -sX POST "$BASE/ai-threads/new" -H 'content-type: application/json' -d '{}'+```++**Omit `type` and the user's own preference decides.** That is the point: a caller like the+dock knows which APP the user clicked, not which AI they like, and hardcoding one imposes a+choice that is not yours to make. The default is **claude**, the workhorse every Adom user is+required to have, so the fallback is always something the user actually has installed.++```jsonc+{"ok": true, "type": "claude", "from_default": true, "result": { "via": "keybinding", ... }}+```++- `type` is the AI that was actually used.+- `from_default: true` means it came from the setting rather than your request.+- `via: "keybinding"` means it opened without flashing the command palette at the user.++**THE RESPONSE CARRIES NO THREAD ID, EVER** (`sid: null, tid: null`, explicit since+1.0.143): a new thread has no identity until its first submitted prompt, and the tab+itself settles in a background task after this call returns. Do NOT open-then-target:+`new` followed by `run-prompt {sid}` always fails because there is no sid to pass.+To open a thread AND deliver a first prompt, use the ONE-CALL composite:++```bash+curl -sX POST "$BASE/ai-threads/run-prompt" -H 'content-type: application/json' \+ -d '{"new": true, "prompt": "Search Mouser for STM32G474", "submit": true,+ "name": "Parts - say it", "tags": ["dock:my-app"]}'+```++`name` and `tags` stage exactly as they do on `/new` (1.0.143+) and attach when the+sid appears; the response verifies delivery against the on-disk transcript+(`delivered_to`). To find a thread you opened earlier, poll `GET /ai-threads` for your+staged name/tags to attach, never guess by position.++**Client timeouts: 45 seconds for targeted calls, 120 for the `new:true` composite.**+Opening drives the editor (extension command, dual detection, tab placement) and its+internal create cap alone is 60s on a busy workbench; every CDP touch also serializes+through one gate, so calls queue behind each other rather than failing. A 5-10s client+timeout manufactures phantom failures. On a create-timeout error, follow the response's+`_hints`: poll GET /ai-threads for your staged name/tags (a late tab claims them), then+deliver by `tid`; a blind retry opens a duplicate.++**Adopt semantics (1.0.143+):** when an idle EMPTY Claude tab already exists, the+composite delivers into it (`adopted_empty_tab: true`) instead of asking the editor for+a new tab, which is deterministic where the editor's own "new conversation" may reuse an+arbitrary tab. Every /ai-threads response also now carries `_hints` telling the calling+AI what happened and how to recover; surface that text in your failure UI.++Name one explicitly only when your feature is genuinely AI-specific:++```bash+curl -sX POST "$BASE/ai-threads/new" -H 'content-type: application/json' \+ -d '{"type":"codex","prompt":"review the diff in this repo"}'+```++`prompt` is optional and seeds the conversation's first message.++`tags` is optional and marks the thread as YOURS before it even has an id:++```bash+curl -sX POST "$BASE/ai-threads/new" -H 'content-type: application/json' \+ -d '{"tags":["dock","app:kicad"],"prompt":"help me with this board"}'+```++A brand-new thread has no stable id until its first message creates the conversation, so+tags passed here STAGE and attach automatically the moment the id appears (the response+carries `tags_staged` confirming that). Staged tags expire after 10 minutes if no thread+materializes, so a failed open cannot mis-tag a stranger.++**Read or change the preference** (it is a normal setting, so let the user own it):++```bash+curl -s "$BASE/ai-threads/settings" | jq .default_ai # "claude"+curl -sX POST "$BASE/ai-threads/settings" -H 'content-type: application/json' \+ -d '{"default_ai":"kimi"}'+```++**Open the AI's CLI in a terminal tab instead** (the caret menu next to each toolbar+button does exactly this):++```bash+curl -sX POST "$BASE/ai-threads/new-tui" -H 'content-type: application/json' \+ -d '{"type":"claude"}'+```++A named terminal ("Claude TUI" / "Codex TUI" / "Kimi TUI") opens as an editor tab+running that AI's full CLI. TUI threads are FIRST-CLASS rows in the tree (marked+`tui: true`): they list alongside webview threads, and `/ai-threads/rename` and+`/ai-threads/close` work on them (close also answers VS Code's "terminate running+processes?" dialog). Delivery endpoints (`run-prompt`, `inject`) do not type into+TUIs; drive a TUI conversation in its terminal.++## See what is open++```bash+curl -s "$BASE/ai-threads" | jq '.groups[].threads[] | {tid, type, label, sid, instantiated}'+```++- `tid` is the CURRENT position (`g0t3`). It changes when tabs move, so do not store it.+- `sid` is the STABLE conversation id. Store this one.+- `instantiated: false` means the tab exists but VS Code has not materialized its webview+ yet. Text cannot be typed into it until it has been focused once.+- `summary.identity` reports how each row was identified, so you can tell a solid row from a+ guessed one.+- `tags` is the thread's durable tag list (see "Tag a thread" below). Filter on it to find+ the threads that belong to your app: `jq '.groups[].threads[] | select(.tags | index("dock"))'`.+- `mention` (claude rows with a live session) is the thread's cross-session @mention+ address, e.g. `"@project-d5"`: any other Claude session on the machine can message it by+ typing that name in its composer.++`GET /ai-threads/wait` long-polls and returns when the tree changes. Use it instead of+polling on a timer.++## Put text in a chat box, or send it++```bash+# stage it for the human to review, nothing is sent+curl -sX POST "$BASE/ai-threads/inject" -H 'content-type: application/json' \+ -d '{"tid":"g0t1","prompt":"summarize this file","submit":false}'++# send it, and confirm it actually landed in THAT conversation+curl -sX POST "$BASE/ai-threads/run-prompt" -H 'content-type: application/json' \+ -d '{"sid":"<stable id>","prompt":"what model are you?"}'+```++Two guarantees worth relying on:++- **`submit: false` really does not send.** Prefer it when a human should see the text first.+- **`run-prompt` confirms delivery by reading the AI's own message list**, not by assuming+ the keystrokes went somewhere. If it says it delivered, it checked.++Typical latency is single-digit to low-double-digit milliseconds against a loaded thread.+A thread whose webview has never been materialized costs a few seconds the first time while+VS Code creates it, so if you care about that, `POST /ai-threads/focus {tid}` first.++## Tag a thread, and find it again forever++Tags are durable strings keyed to the conversation's STABLE id, so they survive restart,+drag, rename, close, and reopen. This is how an external app (the dock) re-finds ITS+threads without guessing from titles.++```bash+# tag an existing thread (ai + sid, or its current tid, or an explicit "<ai>:<sid>" key)+curl -sX POST "$BASE/ai-threads/tag" -H 'content-type: application/json' \+ -d '{"ai":"claude","sid":"<sid>","add":["dock","app:kicad"]}'++# remove one+curl -sX POST "$BASE/ai-threads/tag" -H 'content-type: application/json' \+ -d '{"ai":"claude","sid":"<sid>","remove":["app:kicad"]}'++# the whole store+curl -s "$BASE/ai-threads/tags"+```++Tags come back on BOTH surfaces, open and closed:++```bash+curl -s "$BASE/ai-threads" | jq '.groups[].threads[] | {label, sid, tags}'+curl -s "$BASE/ai-threads/history?tag=app:adom/fusion-bridge" # server-side filter+```++**Bring the user back to their thread, with a visual cue.** `highlight: true` on focus+foregrounds the tab AND pulses its header for about a second, so the user sees WHY this+tab just came forward ("this existing thread is why nothing new opened"):++```bash+curl -sX POST "$BASE/ai-threads/focus" -H 'content-type: application/json' \+ -d '{"sid":"<sid>","highlight":true}'+```++A thread with no conversation yet cannot be tagged here (there is no id to key on); pass+`tags` to `/ai-threads/new` instead and they attach when the id appears.++## Route wiki issues into a thread++Associate a thread with a wiki repo and Hydrogen's global notification watcher queues new+issues on that thread's row (amber count; the user injects with one click, or opts into+auto-inject in Settings):++```bash+curl -sX POST "$BASE/ai-threads/repo-link" -H 'content-type: application/json' \+ -d '{"ai":"claude","sid":"<sid>","repo":"adom/adom-shotlog"}' # repo:"" clears+curl -s "$BASE/ai-threads/repo-links"+curl -s "$BASE/ai-threads/issue-queue" # or ?key=<ai>:<sid>+curl -sX POST "$BASE/ai-threads/issue-inject" -H 'content-type: application/json' \+ -d '{"ai":"claude","sid":"<sid>"}' # injects the next queued issue+```++One thread per repo (a new association moves it and reports `replaced`). Injected issues+are framed as bug reports to evaluate, never as instructions, and only an injected+issue's notification is marked read.++## Reopen a past conversation++```bash+curl -s "$BASE/ai-threads/history" | jq '.items[] | {ai, id, title, updated_ms}'+curl -sX POST "$BASE/ai-threads/resume" -H 'content-type: application/json' \+ -d '{"ai":"codex","id":"01a01bfb-a89d-75e0-9f17-01a163ba399b"}'+```++History lists conversations on disk for all three AIs and EXCLUDES anything currently open,+so "in History" means "not open right now". Every AI reopens into a real editor tab.++## Close a tab++```bash+curl -sX POST "$BASE/ai-threads/close" -H 'content-type: application/json' -d '{"tid":"g0t1"}'+```++The conversation is not deleted. It moves to History and can be reopened.++## Rules that will save you time++1. **Store `sid`, never `tid`.** `tid` is a position and moves.+2. **Do not pick an AI on the user's behalf.** Omit `type` unless your feature is genuinely+ specific to one.+3. **Trust `ok`, and read the rest.** These endpoints verify against real state (a tab+ appeared, the composer holds exactly your text, the message reached the conversation) and+ report `ok: false` with a reason rather than claiming success.+4. **Do not drive the editor UI yourself.** Clicking tabs or typing into webviews from+ outside races Hydrogen's own sweep and will deliver to the wrong conversation. Everything+ you need is here.+5. **Do not hardcode the port.**++## Complete example: a dock icon opens a thread about its app++```javascript+const base = await fetch('/ports').then(r => r.json()).then(p => `http://127.0.0.1:${p.control}`);++async function openThreadFor(app) {+ const r = await fetch(`${base}/ai-threads/new`, {+ method: 'POST',+ headers: { 'content-type': 'application/json' },+ // No `type`: the user's default_ai decides. No prompt if you would rather they type.+ body: JSON.stringify({ prompt: `I just opened ${app.name}. Help me use it.` }),+ }).then(r => r.json());++ if (!r.ok) throw new Error(r.error || 'could not open a thread');+ return r; // {ok, type, from_default, result:{via, ...}}+}+```
skills/hydrogen-ai-threads/SKILL.mdadded+64@@ -0,0 +1,64 @@+---+name: hydrogen-ai-threads+description: >+ The AI Threads panel in Adom Hydrogen — the live tree of every AI conversation+ open in the editor (Claude, Codex, Kimi): which is in front, which are busy,+ drag to reorder, rename, tag, and terminal (TUI) threads that run an AI's full+ CLI in an editor tab. Covers what the panel shows, the BETA on-startup setting+ (off by default; nothing AI-Threads runs while it is off), opening it once from+ Add Panel, TUI threads via the toolbar carets, and how the panel relates to the+ /ai-threads API (see hydrogen-ai-threads-api for driving it from code). Trigger+ words: ai threads, ai threads panel, thread tree, my ai conversations, which ai+ is running, reorder threads, rename a thread, tui thread, claude in a terminal,+ codex tui, kimi tui, ai threads beta, enable ai threads, ai threads setting.+---++# hydrogen-ai-threads — the AI Threads panel++AI Threads is a panel that shows every AI conversation open in the editor as one+live tree, grouped the way the editor groups its tabs. Each row shows the+conversation's AI (Claude, Codex, Kimi), its name, whether it is the foreground+tab, and a busy indicator while that AI is generating. Click a row to bring its+conversation forward.++**It is BETA and off by default.** The setting is+`Settings > Hydrogen > Behavior > Ai Threads On Startup`. While it is off, nothing+related to AI Threads runs: no background scans, no editor probing, nothing. You+can still open the panel once from **Add Panel** (the `+` in any pane's tab bar)+without touching the setting; doing so wakes the machinery for that session only.++## What you can do in the panel++- **Reorder**: drag a row; the row ghosts while the move lands, then settles. The+ order IS the editor's real tab order, so reordering here reorders the tabs.+ Pressing Escape mid-drag abandons the drag and restores everything.+- **Rename**: right-click (or the row's menu) > Rename. The name is written into+ the conversation itself, so it survives restarts and shows on the editor tab.+ Renaming also works on TUI threads.+- **Tags**: threads can carry tags (the dock uses `dock:<app>` to mark threads it+ opened). Tags attach as soon as a brand-new thread earns its identity.+- **New thread buttons**: the toolbar has a button per AI. The small caret beside+ each opens that AI **in a terminal instead**: a named editor tab ("Claude TUI",+ "Codex TUI", "Kimi TUI") running the AI's full CLI. TUI threads appear in the+ tree like any other thread; drive the conversation in the terminal itself.+- **History / resume**: past conversations that are not open right now can be+ reopened into a real editor tab.++## Relationship to the API++The panel is a plain HTTP client of Hydrogen's control API and has no privileged+access: everything above is `POST /ai-threads/*` under the hood, and any app,+dock card, or AI can call the same routes. See **hydrogen-ai-threads-api** for+the full surface (open, list, deliver a prompt with verification, resume, rename,+close, tags) and how to find the control port from inside or outside the+workspace.++## Troubleshooting++- **Panel missing after restart**: the on-startup setting is off (its default);+ turn it on, or open the panel from Add Panel for this session.+- **API returns "AI Threads is not active"**: same cause; while parked, read-only+ census calls refuse by design. A driving call (`/ai-threads/new`, a delivery)+ wakes the machinery for the session.+- **A terminal row shows a shell name** (bash/zsh): that tab is a plain terminal,+ not an AI TUI; the tree deliberately excludes it from thread rows.
skills/hydrogen-api/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/hydrogen-api/SKILL.mdadded+412@@ -0,0 +1,412 @@+---+name: hydrogen-api+description: >+ Complete reference for Hydrogen control API endpoints. The control+ API runs on a dynamic port (find it in the Hydrogen log or via port discovery). Use+ these endpoints to drive Hydrogen programmatically: manage the workspace (the WSL2 `Adom-Workspace` distro), control+ Claude Code, manipulate VS Code, manage workspace tabs, run setup steps, trigger+ virgin reset, and more. ~200 endpoints. The COMPLETE, always-current catalog is+ GET /_manifest (every route with a category + description; curated entries are+ richer). The API Explorer window (Adom menu > API Explorer, or POST /api-explorer/open)+ renders it with a runnable Try per endpoint. GET /test/help lists the gated /test/*+ diagnostics. NEVER blind-call destructive endpoints (see the Safety section).+ Trigger words: Hydrogen API, control API, Hydrogen endpoint, container exec, claude api,+ iframe eval, workspace tabs, setup run-all, workspace status, reload vscode,+ browser profiles, open url, virgin reset api, reboot api, diagnostics,+ ab health, test window-tree, ai cursor, setup panel, screenshot mode.+---++# Hydrogen Control API Reference++The control API exposes **~159 endpoints**. Don't trust a static count or list in+this file to stay exhaustive — the API is **self-documenting**, with two live+sources of truth you should hit first:++| Source | What it gives you | Where |+|---|---|---|+| `GET /_manifest` | **THE complete catalog** — every registered route (`path`, `method`, `args`, `category`, `description`, `visibility`). Curated entries carry richer descriptions; the rest are auto-generated from the router (`scripts/gen-manifest.py`), so it never drifts. **Discover here first — don't guess paths.** | `hydrogen-control/src/lib.rs` `/_manifest` arm |+| **API Explorer window** | The human face of `/_manifest`: every endpoint grouped by category tab, with a runnable **Try** + Copy URL. Open from the Adom menu, or `POST /api-explorer/open`. | Adom menu ▸ API Explorer |+| `GET /test/help` | The `/test/*` diagnostics (window enumeration, UIA click, dialog classification, cascade reset). **Gated behind `HD_TEST_API_ENABLED=1`** — 404 by default (expected, not a bug). | `hydrogen-control/src/lib.rs` `/test/help` arm |++## ⛔ Safety — the API enforces this, and tells you in-band++The API itself now labels, warns about, and gates dangerous endpoints (single source of+truth: `safety.rs` in hydrogen-control). Three tiers:++- **`catastrophic`** — irreversibly destroys real user state: `/setup/virgin-reset`,+ `/setup/panel/(run-virgin-reset|wipe-keep-auth)`, `/wsl/unregister` (deletes the whole+ workspace distro), `/system/reboot` (reboots the+ user's COMPUTER), `/config-reset`, `/claude/wipe-creds`, `*-delete`.+ **Headless calls (no browser Origin header — i.e. YOU) get HTTP 409 `confirmation_required`+ and NOTHING happens** until you re-call with `?confirm=1`. Only add `?confirm=1` when the+ user explicitly asked for exactly that action — never to make an error go away.+- **`destructive`** — kills/restarts/re-runs real state (recoverable but real):+ `/app/(restart|relaunch-frontend)`, `/reload-vscode`,+ `/container(-stop|-restart)`, `/workspace/apply-updates`, `/setup/(run-all|run-step|state|+ force-close|trigger|countdown)`, `/claude/tabs/close-all`, `/updater/install`.+ Never call these speculatively or in a sweep.+- **`disruptive`** — the user SEES it (window opens, focus moves, typing lands in their+ real chat, screen recording starts): `/hbw/open` (and `/hbw/close` to close one by label), `/api-explorer/open`, `/open-url`,+ `/open-in-profile`, `/window/foreground`, `/ai-cursor/*`, `/recording/native/start`,+ `/ui/toast`, `/claude/(trigger-auth|authorize-in-browser|open-with-message|inject*|send|+ submit|uia-*|new-chat-and-send|type-and-submit)` (the send/submit ones SPEND a real user+ turn). Run only when the user actually wants that action, and clean up after yourself.+ (Calling `/claude/authorize-in-browser` blindly is how you spray OAuth windows into Edge.)++How the API tells you:+- **Before calling:** every `GET /_manifest` entry carries `safety` + `safety_note`. Check+ them before you POST anything unfamiliar. Unlabeled = safe.+- **When you call:** responses from these endpoints include a `_safety` block. READ IT —+ it says exactly what you just did to the user's machine.+- **The 409 gate** is your last chance to stop; it is not an error to route around.++A safe GET/POST returns within a couple seconds with a 2xx or a clean 4xx `{ok:false,...}`.+If you must probe an endpoint you're unsure about, POST an empty `{}` body and treat a+graceful validation error as "it works" — a 500 or a hang is the bug.++**Base URL:** `http://127.0.0.1:<dynamic>` — the same loopback address from inside the+workspace (WSL2 mirrored networking shares loopback with the Windows host) and on the+Windows host. The port is dynamic per launch; read the live URL from+`~/.adom/hydrogen-control-url` — see "Finding the Control Port" below.++## Finding the Control Port++The control API port is dynamic (default 47084, auto-resolves conflicts).++```bash+# From Hydrogen log+adom-bridge hd_log '{"tail":100}' | grep "control="++# From ports.json+type %APPDATA%\hydrogen-desktop\ports.json++# From inside the WSL2 workspace — USE THIS. Hydrogen writes its live control URL to a discovery+# file every launch (mirrored loopback), so you never hunt for the dynamic port:+CTRL="$(cat ~/.adom/hydrogen-control-url)"; curl -sf "$CTRL/health"++# No file access (remote tool, browser)? Probe the DISCOVERY responder: fixed candidate+# ports tried in order (a single fixed port is not bindable on every Windows box; WSL2+# mirrored networking claims ports invisibly). Take the FIRST that answers with+# service == "hydrogen-desktop-discovery"; its control_url is the live control API:+for p in 47080 48080 49180 50280; do+ R=$(curl -sf --max-time 2 "http://127.0.0.1:$p/") && echo "$R" | grep -q hydrogen-desktop-discovery && break+done+```++⭐ **From the workspace, read `~/.adom/hydrogen-control-url`** (e.g. `http://127.0.0.1:<control>`) — a+file, NOT an env var (your non-interactive Bash shells don't source `.bashrc`/`profile.d`). Hydrogen+rewrites it with the live dynamic port every launch. This is the channel for the UI command bus,+the live ports map, Claude control, etc. — distinct from `adom-cli` (`ADOM_HYDROGEN_URL` → SSE),+which is unchanged for web-hydrogen parity.++## Calling gotchas (learned the hard way)++- **Never hardcode the port.** It's dynamic (47084 is only the default and it auto-resolves+ conflicts). Read `~/.adom/hydrogen-control-url` from the workspace, or `GET /ports`. Hardcoding+ 47084 is what makes tools show Hydrogen as "down" when it's actually up on another port.+- **Calling from a REMOTE container (hw/cloud)? Use `adom-bridge hd_api`,** the relay's+ pass-through verb: `hd_api {"method":"POST","path":"/...","body":{...}}`. It auto-discovers+ the port; never shell curl through the relay (quoting gets mangled). Long-blocking calls+ (typed injection) can outlive the relay timeout: the work still completes on Hydrogen's side, so+ verify state instead of re-firing.+- **Getting files OUT (recordings, screenshots): streamed binary, never base64.** Endpoints+ return paths inside the WSL distro; a remote container pulls them with `adom-bridge-cli+ --target <desktop> pull_file {filePaths:["\\\\wsl$\\Adom-Workspace\\<path>"], saveTo:...}`+ (1 MiB binary chunks, sha256-verified, no practical size limit). `/recording/native/stop`+ returns the ready-made `fileWindows` UNC path. Full demo-video pipeline: the+ `hydrogen-demo-recording` skill.+- **Confirm it's really the control API, not the SPA.** In dev, the frontend file server+ answers 200 with HTML for *any* path — so a bare 200 doesn't prove you hit Hydrogen's API. Check+ `GET /health` returns JSON with `service:"hydrogen-desktop-control"`.+- **Driving Hydrogen from a page Hydrogen serves? Use relative fetches, not Tauri IPC.** Tauri `invoke`+ is unavailable on remote http origins; a same-origin `fetch('/ports')` / `fetch('/_manifest')`+ works and sidesteps port discovery entirely.+- **Driving from the CLOUD over the relay:** if two Adom Bridges are connected (laptop ++ a VM), `desktop_*` verbs need `--target <name>` (e.g. `AdomLapper`) or they no-route and+ look empty; `nbrowser_*`/`hd_*` auto-route. PowerShell emits **CRLF** — strip `\r` before+ string-comparing its output. Long calls can exceed the relay's per-call timeout — run+ detached + poll a marker (see `hydrogen-cloud-relay-patterns`).++## Health & Status++| Method | Path | Description |+|---|---|---|+| GET | `/health` | Hydrogen alive check — returns `{ok, port, service}` |+| GET | `/buildinfo` | Running build git SHA + build metadata (the only trustworthy "did my build ship?" check) |+| GET | `/ports` | The 4 editable PortConfig ports (proxy, control, cdp, code_server) |+| GET | `/ports/all` | LIVE full port map — every host + WSL2 port Hydrogen has bound, with status + mirrored-mode info |++## ab-health diagnostics (`/ab/*`)++(Canonical since 1.0.47; the old `/ad/*` spellings still answer as aliases.)++These verify the **adom-bridge-cli / relay / embedded-ab** half of Hydrogen is alive (the+bridge that connects this cloud container to Hydrogen). All GET, all return `{ok, ...}`.++| Method | Path | Description |+|---|---|---|+| GET | `/ab/health` | Overall embedded-ab health roll-up |+| GET | `/ab/process` | Is the adom-bridge process running |+| GET | `/ab/embedded` | Embedded-ab module status |+| GET | `/ab/workspace-direct` | ⭐ The **converged** check — direct path to the workspace relay (use this one) |+| GET | `/ab/relay` | Relay (WS/HTTP) reachability |+| GET | `/ab/liveness-beacon` | Liveness beacon freshness |+| GET | `/ab/direct-api` | Direct-API reachability |+| GET | `/ab/discovery-file` | Discovery file present + parseable |++## Driving Hydrogen's UI (command bus)++Open/close/toggle any Hydrogen menu, dialog, or panel — the first-class alternative to CDP clicking. See the `hydrogen-ui` skill.++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/ui/actions` | — | List every drivable UI action `{id,label,description,group}` (+ `_hints`) |+| POST | `/ui/invoke` | `{"id":"ports.open"}` | Run a UI action; returns `{ok,value}` only after the frontend ran it |+| GET | `/auth-token` | Hydrogen session token |+| GET | `/setup-status` | Overall setup completion state |+| GET | `/diagnostics/all` | Run all diagnostic checks |+| GET | `/diagnostics/{name}` | Run a single diagnostic check |++## Workspace Management++These endpoints manage the workspace: the WSL2 distro `Adom-Workspace`. The paths+keep their historical `container` spelling; there is no container.++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/container-status` | — | `runtime_available` (WSL2 usable), `workspace_installed` (distro present), workspace exists/running |+| GET | `/container-stats` | — | CPU, memory, disk usage |+| POST | `/container-restart` | — | Restart the workspace (5s timeout) |+| POST | `/container-stop` | — | Stop the workspace |+| POST | `/container-exec` | `{"command":"..."}` | Run command in the workspace as user `adom` |+| POST | `/container-delete` | — | Remove the workspace |+| GET | `/container/logs` | — | Workspace stdout/stderr |+| GET | `/container/env` | — | Workspace environment variables |+| GET | `/container/ports` | — | Forwarded port mappings |+| GET | `/container/disk` | — | Disk usage inside the workspace |+| POST | `/container/restart` | — | Alias for `/container-restart` |+| POST | `/direct/command` | `{...}` | Direct-connect command channel into the workspace |++> `GET /container-status` renamed two fields: `docker_available` → **`runtime_available`**+> and `image_pulled` → **`workspace_installed`**. Old field names are gone.++### Workspace / WSL endpoints++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/wsl/status` | — | WSL distro state (registered, running, code-server reachable) |+| GET | `/workspace/health` | — | Deep workspace health (distro + code-server + host reachability) |+| GET | `/virgin-reset-progress` | — | Live progress of an in-flight virgin reset |+| POST | `/workspace/terminal-profile` | — | Ensure the stable `Adom-Workspace` Windows Terminal profile (idempotent) |+| POST | `/workspace/terminal-profile/remove` | — | Remove the `Adom-Workspace` Windows Terminal profile(s) |+| POST | `/workspace/check-updates` | — | Check upstream for newer code-server + Claude extension; stage them |+| POST | `/workspace/apply-updates` | — | Apply staged code-server/extension updates + reload editor |++> ⛔ `POST /wsl/unregister` is **REFUSED as deprecated** — it was a headless+> destructive reset (deletes `/home/adom/project`). Use the Setup panel's Virgin+> Reset or `POST /setup/panel/run-virgin-reset` instead.++## WebView & VS Code++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/eval` | `{"js":"..."}` | JS eval in main Hydrogen webview |+| POST | `/iframe-eval` | `{"js":"...","contextIndex":N}` | JS eval in a specific iframe context (0-14) |+| POST | `/cdp-eval` | `{"expression":"..."}` | Direct Chrome DevTools Protocol eval |+| POST | `/click` | `{"x":N,"y":N}` | Click at coordinates in webview |+| POST | `/mouse-click` | `{"x":N,"y":N,"button":"left"}` | Mouse click with options |+| POST | `/query` | `{"selector":"..."}` | Query DOM elements |+| POST | `/reload-vscode` | — | Reload the VS Code iframe |+| POST | `/vscode/hide-activity-items` | — | Hide activity-bar items (persisted, overflow-proof) |+| GET | `/targets` | — | List CDP targets (all webview contexts) |+| GET | `/cdp-info` | — | CDP connection info (port, URL) |+| GET | `/devtools` | — | DevTools status |++### AI cursor (visible cursor for demos / clicking)++Distinct from `/click` and `/mouse-click` — this renders a **visible** AI cursor,+glides it, and can click. Use for demos or when the user should see where the AI clicks.++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/ai-cursor/move` | `{"x":N,"y":N}` | Move (glide) the visible AI cursor to coordinates |+| POST | `/ai-cursor/click` | `{"x":N,"y":N}` | Move the visible AI cursor there + click |+| POST | `/ai-cursor/hide` | — | Hide the visible AI cursor |++### Screenshot primitives++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/capture/viewport` | — | Capture the Hydrogen webview viewport (PNG) |+| POST | `/shot` | `{...}` | Screenshot primitive (see **hydrogen-self-screenshot** for the full workflow) |++### Port hints++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/adom-port-hints` | — | Read the workspace port-hints map |+| POST | `/adom-port-hints` | `{...}` | Write/merge the workspace port-hints map |++## Claude Code++### Status / auth state (GET)++| Method | Path | Description |+|---|---|---|+| GET | `/claude/status` | Install state, auth, active session, version |+| GET | `/claude/auth-check` | Deep auth check — file exists, token, expiry |+| GET | `/claude/auth-status` | Auth-status detail |+| GET | `/claude/auth-page-state` | Which auth screen the panel shows: `continue-in-browser` \| `subscription-button` \| `chat`; returns `oauth_url` + `at_browser_auth_gate` |+| GET | `/claude/detect-auth-failure` | Scan webview contexts for auth button/errors |+| GET | `/claude/sessions` | List active Claude sessions |++### Auth actions (POST)++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/claude/find-auth-button` | — | Find + highlight the sign-in button WITHOUT clicking (validation) |+| POST | `/claude/start-auth` | — | Find + highlight + CLICK the "Claude.ai Subscription" button to begin OAuth |+| POST | `/claude/trigger-auth` | — | Alias for start-auth |+| POST | `/claude/detect-state` | — | Detect current auth/panel state |+| POST | `/claude/authorize-in-browser` | `{"timeout_ms":N,"dry_run":B,"fresh":B,"oauth_url":"..."}` | Auto-click the "Authorize" button on the claude.ai OAuth page (unattended sign-in) |+| POST | `/claude/authorize-tab-enter` | — | Approve the Adom OAuth tab via Enter (branded picker flow) |+| POST | `/claude/inject-creds` | — | Push host backup credentials into the workspace |+| POST | `/claude/wipe-creds` | `{"backup":B}` | Wipe Claude credentials (destructive); `backup:false` keeps the host backup so reinject can restore |++### Panel / conversation control (POST unless noted)++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/claude/open` | — | Open Claude Code panel (`claude-vscode.editor.open`) |+| POST | `/claude/ensure-open` | — | Ensure the panel is open (strict readiness; recovery if not rendered) |+| POST | `/claude/open-with-message` | `{"message":"..."}` | Open + pre-fill message |+| POST | `/claude/focus` | — | Focus Claude panel |+| POST | `/claude/new-conversation` | — | Start a fresh conversation (shows sign-in panel when unauthenticated) |+| POST | `/claude/new-chat-and-send` | `{"text":"...","typed":B,"submit":B,"delay_ms":N}` | Open fresh conversation, wait for compose box, type + (default) submit |+| POST | `/claude/submit` | — | Find + click send button |+| POST | `/claude/send` | `{"message":"..."}` | Send message to Claude |+| POST | `/claude/inject` | `{"message":"..."}` | Inject text into the compose box |+| POST | `/claude/inject-message` | `{"message":"..."}` | Inject into conversation |+| POST | `/claude/type-and-submit` | `{"message":"...","delay_ms":N}` | Type char-by-char (human-paced) + submit |+| POST | `/claude/remote-control` | `{"name":"..."}` | Turn on Claude Code Remote Control (submits `/remote-control` slash command) |+| POST | `/claude/highlight-open-button` | `{"duration":N}` | Glow animation on Claude button |+| POST | `/claude/clear-open-highlight` | — | Clear the open-button highlight |++## OAuth & Auth++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/claude-auth` | — | Full PKCE OAuth flow — opens browser, sets up callback |+| GET | `/oauth/callback` | — | OAuth callback handler (receives auth code) |+| POST | `/auth/proxy` | varies | Proxy auth requests to carbon.adom.inc |+| POST | `/open-url` | `{"url":"..."}` | Open URL (triggers browser picker) |+| POST | `/start-oauth-proxy` | `{"port":N}` | Start OAuth callback proxy on a port |+| GET | `/browser-profiles` | — | List browsers + profiles with avatars |+| POST | `/open-in-profile` | `{"url":"...","browser":"...","profileDir":"..."}` | Open URL in specific browser profile |+| POST | `/browser-picker/opened` | `{...}` | Frontend reports the browser picker was opened |+| GET | `/browser-picker/last-open` | — | Last browser-picker open event |++## Setup Steps & Setup Panel++### State / trigger / step control++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/setup/state` | — | Get current setup state (`never-run` \| `in-progress` \| `complete`) |+| POST | `/setup/state` | `{"state":"..."}` | Set setup state |+| POST | `/setup/step/<id>` | `{"continue_after":B}` | ⭐ Run one step by id (forces panel visible first); `continue_after:true` runs this step + every step after it |+| POST | `/setup/force-close` | — | Force-close setup panel |+| POST | `/setup/countdown` | `{"seconds":N,"message":"..."}` | Show countdown overlay |+| GET | `/setup/trigger` | — | Read trigger file |+| DELETE | `/setup/trigger` | — | Delete trigger file |+| POST | `/config-reset` | — | Reset Hydrogen config |++> ⛔ **REFUSED as deprecated** (headless runs with no visible UI):+> `POST /setup/run-all`, `POST /setup/run-step`, `POST /setup/virgin-reset`,+> `POST /wsl/unregister`. They return a refusal pointing at the UI-visible path.+> To run steps: use `POST /setup/step/<id>` or relaunch Hydrogen to auto-run the cascade.++### Setup panel control++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/setup/panel/show` | — | Force the setup panel visible |+| POST | `/setup/panel/hide` | — | Hide the setup panel |+| POST | `/setup/panel/show-virgin-reset` | — | Show panel + expand the Virgin Reset section |+| POST | `/setup/panel/virgin/show` | — | Show the Virgin Reset section only |+| POST | `/setup/panel/virgin/hide` | — | Hide the Virgin Reset section |+| POST | `/setup/panel/set-virgin-options` | `{"options":{...}}` | Set the virgin-reset checkboxes |+| POST | `/setup/panel/run-virgin-reset` | — | ⭐ The **ONLY** allowed virgin-reset trigger — UI-visible by construction (shows panel + checkboxes + progress before deleting anything) |+| POST | `/setup/panel/wipe-keep-auth` | — | Synchronous wipe that KEEPS Adom + Claude tokens |+| POST | `/setup/screenshot-mode` | `{"enabled":B}` | Toggle per-step screenshot capture (default OFF) |+| GET | `/setup/screenshot-mode` | — | Whether per-step screenshot mode is on |++## Workspace Tabs++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/workspace/tabs` | — | List all workspace tabs |+| GET | `/workspace/tabs/find?name=X` | — | Find tab by name (404 if not found) |+| POST | `/workspace/tabs` | `{tab spec}` | Add a new tab |++## System++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/system/nonce` | — | Get nonce for authenticated reboot |+| GET | `/system/reboot-state` | — | Check reboot pending state |+| POST | `/system/reboot` | `{"nonce":"..."}` | Reboot Windows (nonce-authenticated) |+| POST | `/system/cancel-reboot` | — | Cancel pending reboot |++## App lifecycle & window++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/app/restart` | — | Restart the Hydrogen app (kills + respawns the binary) |+| POST | `/app/relaunch-frontend` | — | Relaunch the Hydrogen GUI (kill+respawn) while it is responsive |+| POST | `/window/foreground` | — | Bring the Hydrogen window to the foreground (unminimize + focus) |++## Diagnostics (`/test/*`)++Window enumeration, UIA clicking, native-dialog classification, and cascade+diagnostics. **Gated behind `HD_TEST_API_ENABLED=1`** (return 404 / "test API+disabled" otherwise). `GET /test/help` self-lists the whole cluster.++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/test/help` | — | Self-lists every `/test/*` endpoint |+| GET | `/test/window-tree` | — | Enumerate all visible top-level windows |+| POST | `/test/focus` | `{"window":"..."}` | Focus Hydrogen (`self`) or a window by title substring |+| POST | `/test/click-uia` | `{...}` | Poll UIAutomation for a Button by Name; Invoke when found |+| GET | `/test/setup-steps` | — | Raw setup-steps.json + derived summary (alias: `/test/install-steps`) |+| POST | `/test/dump-state` | `{...}` | Write a diagnostic bundle (logs, install-steps, workspace state, tasklist, window-tree) |+| POST | `/test/reset-cascade` | — | Delete install-steps.json + emit setup-reset event |+| POST | `/test/screenshot-hydrogen-window` | `{...}` | Capture Hydrogen's own window to a PNG file |+| GET | `/test/probe-dialogs` | — | Enumerate visible windows, classify against the cascade-blocker catalog (UAC, WSL2 update, MSI installer, …); returns `kind` + `is_blocking` per window |++## Key Patterns++### iframe-eval context discovery+Contexts 0-14 cover all webview frames. Context numbers shift when panels open/close.+Always scan, never hardcode. The Claude Code extension webview IS reachable through+these contexts.++```+POST /iframe-eval {"js":"document.body.innerText.substring(0,100)","contextIndex":3}+```++### container-exec+Runs as user `adom` inside the workspace. Use full paths (`/home/adom/`+not `~/`). Tilde doesn't expand through the exec chain.++```+POST /container-exec {"command":"ls /home/adom/project"}+```++### Re-run a setup step after completion+The trigger file poller STOPS when all steps are done. `POST /setup/run-all` is+**REFUSED as deprecated** — don't use it. Re-run one step by id (or relaunch Hydrogen to+auto-run the cascade):+```+POST /setup/step/<id>+```
skills/hydrogen-browser-picker/SKILL.mdadded+479@@ -0,0 +1,479 @@+---+name: hydrogen-browser-picker+description: Hydrogen's Browser Picker — the canonical handler for EVERY URL that gets opened inside Hydrogen. Click a link anywhere (main page, code-server iframe, webview panel, PortMappingsDialog, OAuth redirect, localhost link, external link) and Hydrogen intercepts it via 5 layers (on_new_window / on_navigation / NewWindowRequested / AddScriptToExecuteOnDocumentCreated / ContextMenuRequested) and routes it through the BrowserProfileDialog so the user (or an AI driving automation) picks where it opens — native browser profile, Hydrogen tab, Hydrogen window, or Pup. 5-second auto-countdown so AIs don't block waiting on a click. Read BEFORE adding a URL-opening feature, debugging dead links, fixing `shell.open not allowed`, or automating browser interactions from Hydrogen. Trigger words — browser picker, BrowserProfileDialog, hydrogen-open-url, hydrogen-open-url-force, open with browser picker, shell.open not allowed, shell:default, shell:allow-open, plugin-shell, url interception, link click, target blank, claude auth url, adom auth url, oauth callback in browser, default browser, browser profile, AI browser automation, countdown picker, auto-timer, shift click force, right-click open with, context menu, webview links, on_new_window, hydrogen-links (legacy name).+---++# Hydrogen — Browser Picker (URL Interception)++## The Problem++Hydrogen wraps a WebView2 that contains:+- The Hydrogen SvelteKit frontend (localhost:1420 in DEV mode only; prod Hydrogen serves it via the tauri protocol)+- The code-server iframe (VS Code + Claude Code) — code-server is host port 7380 (8080 is the internal workspace port)+- Webview panel iframes (any external URL like wiki, google, etc.)++When a link is clicked anywhere in this stack, Hydrogen needs to:+1. Catch it before it opens uncontrolled+2. Route it through the Browser Picker dialog+3. Let the user choose: native browser (Chrome/Edge), Hydrogen Tab (wv), Hydrogen Browser Window (short name: **hbw**), or Pup++## The 5 Interception Layers++Each layer catches a different type of URL event. All registered in `setup_frame_navigation_interception()` in `hydrogen-app/src/lib.rs` via `webview.with_webview()`.++### Layer 1: `on_new_window` (Tauri wrapper)+**Catches:** `window.open()`, `<a target="_blank">` from the TOP-LEVEL webview and direct child frames+**Action:** Deny ALL. Dispatch to Browser Picker via CustomEvent.+**Shift detection:** Yes — `GetAsyncKeyState(0x10)` checks if Shift is held, dispatches `hydrogen-open-url-force` for Browser Picker override.+**Debounce:** 500ms to prevent event loops.++### Layer 2: `on_navigation` (Tauri wrapper)+**Catches:** Top-level frame navigations (someone navigating the main webview to an external URL)+**Action:** Allow localhost/tauri/data/about. Block all external HTTPS. Dispatch to Browser Picker.++### Layer 3: `add_FrameNavigationStarting` (raw COM)+**Catches:** ALL child frame navigations (iframe navigating itself)+**Action:** Log only — don't block. Webview panels legitimately load external URLs in iframes.+**Why not block:** Blocking breaks webview panels (they need external URLs). Same-domain navigations (wiki → wiki/skills) work fine.++### Layer 4: `add_NewWindowRequested` (raw COM)+**Catches:** New window requests that Tauri's wrapper might miss+**Action:** `SetHandled(true)` for external URLs. No event dispatch (Tauri's handler does that).+**Purpose:** Safety net — prevents any uncontrolled window from being created.++### Layer 5: `AddScriptToExecuteOnDocumentCreated` (WebView2 API)+**Catches:** `<a target="_blank">` clicks inside cross-origin iframes+**Why needed:** Cross-origin iframe `target="_blank"` clicks don't fire `NewWindowRequested` on the parent WebView2. This is a WebView2 security boundary.+**How:** Injects a click listener into ALL frames (including cross-origin) that intercepts `target="_blank"` clicks and dispatches a CustomEvent to `window.top`.+**Survives:** Page reloads, iframe navigations — WebView2 auto-injects on every document load.++### Layer 6: `add_ContextMenuRequested` (raw COM, ICoreWebView2_11)+**Catches:** Right-click on links+**Action:** Adds "Open Link with Browser Picker" or "Open Page with Browser Picker" to the native WebView2 context menu.+**How:** Cast `ICoreWebView2` → `ICoreWebView2_11`, register handler. On right-click, check `HasLinkUri()`, create custom menu item via `ICoreWebView2Environment9.CreateContextMenuItem()`, register `add_CustomItemSelected` handler that dispatches `hydrogen-open-url-force`.++## What Each Click Type Triggers++| User action | Where | Layer that catches | Result |+|---|---|---|---|+| Left-click `<a target="_blank">` in webview panel iframe | Cross-origin iframe | Layer 5 (JS script) | Browser Picker |+| Left-click regular `<a href>` in webview panel iframe | Cross-origin iframe | Layer 3 (logged) | Navigates iframe normally |+| Right-click → "Open in new window" | Any iframe | Layer 1 (on_new_window) | Browser Picker |+| Right-click → "Open Link with Browser Picker" | Any link | Layer 6 (context menu) | Browser Picker (force) |+| Shift+click any link | Anywhere | Layer 1 + shift detection | Browser Picker (force, no auto-timer) |+| `window.open()` from code-server | Code-server iframe | Layer 1 (on_new_window) | Browser Picker |+| `vscode.env.openExternal()` from Claude Code | Code-server → window.open | Layer 1 (on_new_window) | Browser Picker |+| Claude Code auth button | Code-server extension | Layer 1 (on_new_window) | Claude-branded Browser Picker |++## Browser Picker Dialog++`src/lib/components/BrowserProfileDialog.svelte`++### 3 Branded Variants+1. **Claude auth** — Claude logo, "Connect to Claude Code" (domains: `*.claude.ai`, `*.claude.com`)+2. **Adom auth** — Adom logo, "Sign in to Hydrogen" (domains: `*.adom.inc` + `/auth/intent`)+3. **General link** — "Browser Picker" title, no logo++### Consistent Elements (all variants)+- "Browser Picker" title at top+- Native Browsers section with real Google profile avatars+- Hydrogen section (collapsible, collapsed for auth)+- Details section (collapsible, collapsed) — URL, remember checkbox, trigger info, build version+- 5-second auto-timer with progress bar (cancelled by mouse movement)+- Keyboard navigation (arrow keys + Enter)+- 800ms delayed tooltips++### Auto-Timer Behavior+- Picks best default: work browser profile (non-gmail email) for auth, Pup for right-click/iframe opens+- 5-second countdown with progress bar+- ANY mouse movement cancels the timer+- Shift+click (force-show) disables timer entirely+- Selected destination highlighted with teal border++### ⭐ Fresh-window option (first-class)+A **freshly-launched browser window/process auto-foregrounds on Windows** — focus-stealing+prevention does NOT block a process's own first window. A URL opened as a *tab* in an+already-running browser does NOT reliably come to the front. So "fresh" is the proven way+to make the **Claude/Adom consent page pop to the front by itself** (removes the manual+foreground step that used to block hands-free auth).++- Mechanism: launch the browser exe with `--new-window` (Chromium) / `-new-window`+ (Firefox) before the URL, instead of `open::that(url)` (shell-open can't force a window).+- UI: an **"Open in a fresh window"** checkbox above the Native Browsers list. **Defaulted+ ON for auth URLs** (Claude/Adom), OFF otherwise. Only native-browser destinations honor+ it; internal Hydrogen destinations ignore it. The dialog dispatches `select` with+ `fresh: dest.category==='native' ? freshWindow : false`; `onProfileSelect` threads it to+ `open_in_browser_profile` / `/open-in-profile`.+- `fresh` is honored by: Tauri `open_in_browser_profile` (direct), `POST /open-in-profile`+ (direct), and `POST /open-url` (where it **pre-seeds the picker's toggle**; add+ `direct:true` to open immediately instead). Direct paths echo+ `{"fresh":…, "method":"<exe … fresh-window>"}`.+- Dev Toolbar A/B debug buttons: **▶ Open URL — FRESH window** vs **▶ Open URL — normal**.++### URL Event Deduplication+Frontend deduplicates URL events within 1-second window (same URL) to prevent double-open from multiple layers firing for the same click. Force events (Shift) bypass dedup.++### Prefs Sync+Preferences re-read from localStorage on every dialog open. If user deletes a domain in Browser Picker Manager, the next dialog open reflects it immediately.++## Custom Context Menu Implementation++Uses raw COM API via `ICoreWebView2_11` (cast from `ICoreWebView2` using `windows::core::Interface::cast`).++Key COM types used:+- `ICoreWebView2_11` — has `add_ContextMenuRequested`+- `ICoreWebView2Environment9` — has `CreateContextMenuItem`+- `ICoreWebView2ContextMenuTarget` — `HasLinkUri()`, `LinkUri()`, `PageUri()`+- `ICoreWebView2ContextMenuItemCollection` — `Count()`, `InsertValueAtIndex()`+- `ICoreWebView2ContextMenuItem` — `add_CustomItemSelected()`+- `COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND` — command-type menu item++webview2-com 0.38 handler types used: `ContextMenuRequestedEventHandler`, `CustomItemSelectedEventHandler`++### How we got the context menu working (the full story)++**Initial assumption:** webview2-com 0.38 doesn't have context menu types. WRONG.++**First attempt:** Upgrade to webview2-com 0.39 which obviously has them. FAILED — 0.39 requires `windows 0.62` but Tauri pins `windows 0.61`. The upgrade broke 15+ API call sites due to `PCWSTR`/`HSTRING` type changes.++**Second attempt:** Write raw COM vtable bindings for ICoreWebView2_11 manually. UNNECESSARY.++**The discovery:** webview2-com 0.38 DOES export all the types. They're in `webview2_com::Microsoft::Web::WebView2::Win32::*` — the same module as everything else. The standard `.cast()` pattern works:++```rust+use windows::core::Interface;+use webview2_com::Microsoft::Web::WebView2::Win32::{+ ICoreWebView2_11,+ ICoreWebView2Environment9,+ COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND,+};++// Cast from base ICoreWebView2 — no raw vtable, no version upgrade+let core11: ICoreWebView2_11 = core.cast()?;+let env9: ICoreWebView2Environment9 = env.cast()?;+```++**The BOOL gotcha:** The one type that didn't work was `windows::Win32::Foundation::BOOL`. In windows 0.61, `BOOL(0)` isn't valid (it's not a tuple struct). `BOOL::default()` also fails (can't find it in the module). The fix is a raw cast:++```rust+let has_link = {+ let mut val = 0i32;+ target.HasLinkUri(&mut val as *mut i32 as *mut _)?;+ val != 0+};+```++COM `BOOL` is just an `i32` under the hood (0 = false, non-zero = true). Casting the pointer works perfectly.++**The full chain:**+```rust+// 1. Cast to ICoreWebView2_11+let core11: ICoreWebView2_11 = core.cast()?;++// 2. Register ContextMenuRequested handler+core11.add_ContextMenuRequested(&handler, &mut token)?;++// 3. In the handler: get the right-click target+let target = args.ContextMenuTarget()?;++// 4. Check if it's a link (BOOL workaround)+let mut val = 0i32;+target.HasLinkUri(&mut val as *mut i32 as *mut _)?;++// 5. Get the link URL+let mut uri = PWSTR::null();+target.LinkUri(&mut uri)?;+let url = uri.to_string().unwrap_or_default();++// 6. Create custom menu item via Environment9+let env9: ICoreWebView2Environment9 = env.cast()?;+let item = env9.CreateContextMenuItem(&label, None, KIND_COMMAND)?;++// 7. Add click handler+item.add_CustomItemSelected(&click_handler, &mut token)?;++// 8. Insert at top of context menu+let items = args.MenuItems()?;+items.InsertValueAtIndex(0, &item)?;+```++**Lesson:** Don't assume a crate is missing types because the docs are sparse. The `webview2_com::Microsoft::Web::WebView2::Win32::*` re-export brings in the ENTIRE WebView2 COM type library. If the COM interface exists in the WebView2 SDK, the Rust binding exists too — you just need to cast to the right version interface.++## AddScriptToExecuteOnDocumentCreated Script++```js+(function() {+ if (window.__hdLinkIntercept) return;+ window.__hdLinkIntercept = true;+ document.addEventListener('click', function(e) {+ var a = e.target.closest('a[target="_blank"]');+ if (!a) return;+ var href = a.href;+ if (!href || href.startsWith('javascript:')) return;+ if (new URL(href).hostname === window.location.hostname) return;+ e.preventDefault();+ e.stopPropagation();+ try {+ window.top.dispatchEvent(new CustomEvent('hydrogen-open-url', {+ detail: JSON.stringify({+ url: href,+ trigger: 'target_blank_click',+ shiftHeld: e.shiftKey+ })+ }));+ } catch(err) {+ window.parent.postMessage({+ type: 'hydrogen-open-url', url: href, trigger: 'target_blank_click'+ }, '*');+ }+ }, true);+})();+```++**Why same-domain is allowed:** `if (new URL(href).hostname === window.location.hostname) return;` — wiki links to other wiki pages should navigate the iframe normally. Only cross-domain links get intercepted.++## WebView2 Event Architecture — What We Learned++### How to access raw WebView2 COM APIs from Tauri++Tauri exposes the platform webview via `webview.with_webview(|platform_webview| { ... })`. The closure runs on the main (UI) thread where COM objects live.++```rust+let controller = platform_webview.controller(); // ICoreWebView2Controller+let env = platform_webview.environment(); // ICoreWebView2Environment+let core = controller.CoreWebView2().unwrap(); // ICoreWebView2+```++From the base `ICoreWebView2`, you can cast to higher interfaces:+```rust+use windows::core::Interface;+let core4: ICoreWebView2_4 = Interface::cast(&core).unwrap(); // for add_FrameCreated+let core11: ICoreWebView2_11 = core.cast().unwrap(); // for add_ContextMenuRequested+let env9: ICoreWebView2Environment9 = env.cast().unwrap(); // for CreateContextMenuItem+```++### WebView2 event handler types in webview2-com 0.38++The crate provides `*EventHandler::create(Box::new(|sender, args| { ... }))` factory methods:+- `NavigationStartingEventHandler` — for `add_NavigationStarting` / `add_FrameNavigationStarting`+- `NewWindowRequestedEventHandler` — for `add_NewWindowRequested`+- `ContextMenuRequestedEventHandler` — for `add_ContextMenuRequested`+- `CustomItemSelectedEventHandler` — for context menu item click+- `AddScriptToExecuteOnDocumentCreatedCompletedHandler` — callback for script registration+- `FrameCreatedEventHandler` — for `add_FrameCreated` (on ICoreWebView2_4)++All handlers register with a `&mut i64` token parameter (for later removal).++### The event token pattern+```rust+let mut token = 0i64;+core.add_FrameNavigationStarting(&handler, &mut token)?;+// token is now set — save it to call remove_FrameNavigationStarting later+```++### Which events fire at which iframe depth++| Event | Top-level | Direct child iframe | Cross-origin child iframe |+|---|---|---|---|+| `NavigationStarting` (on_navigation) | Yes | No | No |+| `FrameNavigationStarting` | N/A | Yes | Yes (same-domain only for some) |+| `NewWindowRequested` (on_new_window) | Yes | Yes (same-origin) | **NO** (cross-origin) |+| `ContextMenuRequested` | Yes | Yes | Yes |++The critical gap: **cross-origin iframe `target="_blank"` clicks don't fire `NewWindowRequested`** on the parent. This is a WebView2 security boundary, not a bug.++### AddScriptToExecuteOnDocumentCreated — the secret weapon++This is a WebView2 configuration API, not eval injection. Key properties:++1. **Registered once** on `ICoreWebView2`, applies forever+2. **Runs in ALL frames** — top-level, child iframes, cross-origin iframes, nested iframes+3. **Runs BEFORE page JavaScript** — your script is guaranteed to execute first+4. **Survives navigations** — when the frame navigates to a new page, the script runs again+5. **Survives reloads** — F5, location.reload(), everything+6. **Controlled by WebView2** — not by the page, not by JS, not by CORS++This is how Electron handles preload scripts. It's the ONLY reliable way to run code inside a cross-origin iframe without the iframe's cooperation.++```rust+let script = windows::core::HSTRING::from("(function() { /* your code */ })();");+let handler = webview2_com::AddScriptToExecuteOnDocumentCreatedCompletedHandler::create(+ Box::new(|_, _| Ok(()))+);+core.AddScriptToExecuteOnDocumentCreated(&script, &handler)?;+```++The script has access to:+- `window.top` — can dispatch CustomEvents to the top-level page (works even cross-origin in WebView2)+- `window.parent.postMessage()` — fallback if `window.top` access is blocked+- `document.addEventListener()` — full DOM access within the frame+- `window.location` — the frame's current URL++We use it to intercept `target="_blank"` clicks that would otherwise be invisible to the Rust layer.++### COM method signature patterns in webview2-com 0.38++The types use a mix of safe wrappers and raw out-parameters:++**Safe wrappers (return `Result<T>`):**+```rust+let target = args.ContextMenuTarget()?; // returns Result<ICoreWebView2ContextMenuTarget>+let items = args.MenuItems()?; // returns Result<ICoreWebView2ContextMenuItemCollection>+let item = env9.CreateContextMenuItem(&label, None, KIND_COMMAND)?; // returns Result<ICoreWebView2ContextMenuItem>+```++**Out-parameter methods:**+```rust+let mut has_link = windows::Win32::Foundation::BOOL(0);+target.HasLinkUri(&mut has_link)?; // writes to has_link+if has_link.as_bool() { ... }++let mut uri = windows::core::PWSTR::null();+target.LinkUri(&mut uri)?; // writes to uri+let url = uri.to_string().unwrap_or_default();++let mut count = 0u32;+items.Count(&mut count)?; // writes to count+```++**Event registration:**+```rust+let mut token = 0i64;+menu_item.add_CustomItemSelected(&handler, &mut token)?;+items.InsertValueAtIndex(count, &menu_item)?;+```++### Cargo.toml requirements++The `Win32_Foundation` and `Win32_System_Com` features must be enabled on the windows crate for BOOL and COM types:+```toml+[target.'cfg(windows)'.dependencies]+webview2-com = "0.38"+windows = { version = "0.61", features = [+ "Win32_Foundation",+ "Win32_System_Com",+ "Win32_System_WinRT",+ ...+] }+```++## Hard-Won Lessons++### `NewWindowResponse::Allow` must NEVER be returned; `Create` is the one sanctioned exception+Original code returned `Allow` for localhost URLs. Code-server's `openExternal` wraps URLs in localhost routes. Returning `Allow` created uncontrolled WebView2 windows: no Tauri label, no handlers, no way to close or navigation-police them. `Allow` stays banned, always, for every URL.++Since 1.0.137 (granted popups), `NewWindowResponse::Create {{ window }}` IS permitted for exactly two shapes: `about:blank` (VS Code "Open Terminal in New Window": the opener scripts the blank doc) and same-origin code-server-port URLs. Create is the opposite of the failure that motivated the ban: WebView2 gets a MANAGED `WebviewWindow` (`hd-popup-*`) with a label, an `on_navigation` that still routes externals to the Browser Picker, a concurrency cap (6), and Destroyed cleanup. Rules that keep the original lesson intact:+- The window MUST be a `WebviewWindow` built SYNCHRONOUSLY inside `on_new_window`, with `.window_features(features)` (applies the opener's WebView2 environment, mandatory on Windows; tauri-runtime-wry PANICS on a plain `Window` or an unregistered window).+- External http(s) NEVER gets Allow or Create: picker dispatch then Deny, unchanged.+- Any build failure: log `POPUP CREATE FAILED` and Deny. Never "fall back" to Allow.++### Cross-origin iframe `target="_blank"` is invisible to WebView2+No COM event fires on the parent WebView2 when a cross-origin child iframe has a `target="_blank"` click. Not `NewWindowRequested`, not `FrameNavigationStarting`, not `NavigationStarting`. The ONLY way to catch it: `AddScriptToExecuteOnDocumentCreated`.++### `on_new_window` features param isn't a string+Tauri's `on_new_window` callback receives `(Url, NewWindowFeatures)`. `NewWindowFeatures` is a struct with `size()` and `position()` methods, NOT a string. Can't call `.is_empty()`.++### `dispatch_url_event` needs 200ms delay+At 50ms, the webview eval sometimes doesn't process. 200ms is reliable.++### Domain check must only check the domain, not query params+URLs like `https://claude.com/oauth?redirect_uri=http://localhost:PORT` contain "localhost" in the query string. The `is_internal` check must parse the domain from `url.split('/').nth(2)`, not `url.contains("localhost")`.++### webview2-com 0.38 vs 0.39+0.38 is pinned by Tauri v2 (requires windows 0.61). 0.39 requires windows 0.62 and breaks 15+ API signatures. But 0.38 DOES have `ICoreWebView2_11`, `ContextMenuRequestedEventHandler`, and related types — they just use out-parameter patterns, not safe wrappers. Use `core.cast::<ICoreWebView2_11>()` which works in 0.38.++## Files++| File | What |+|---|---|+| `src-tauri/crates/hydrogen-app/src/lib.rs` | All 6 interception layers, dispatch_url_event, context menu |+| `src/lib/components/BrowserProfileDialog.svelte` | Browser Picker dialog (3 variants, auto-timer, keyboard nav) |+| `src/lib/components/editor/EditorNav.svelte` | Browser Picker Manager modal |+| `src/routes/+layout.svelte` | CustomEvent listeners, URL event parsing, deduplication |+| `src/lib/assets/claude-logo.svg` | Real Claude logo from claude.ai/favicon.svg |+| `src/lib/assets/adom-logo-teal.svg` | Adom brand logo |+| `src/lib/buildVersion.ts` | Build version tracking for verification |+| `src-tauri/capabilities/default.json` + `src-tauri/crates/hydrogen-app/capabilities/default.json` | Tauri permissions — MUST include `shell:default` AND `shell:allow-open` |++## The 5-second auto-countdown (why AI loves it)++The picker opens with a countdown on its best-default destination. If nothing interrupts, the URL auto-routes there. This is the key feature for AI automation: triggers like OAuth redirects, "open in browser" links, or wiki references work hands-free.++- **AI scripts can fire a URL open and the right thing happens within 5s** — no clicking, no blocking on a dialog.+- **Mouse movement cancels the timer** — if a human is at the keyboard, just wiggle the mouse and the picker waits.+- **Shift+click disables the timer entirely** — forces an explicit human choice.++Best-default heuristic:+- `*.claude.ai` / `*.claude.com` → "Claude auth" variant, defaults to work browser profile+- `*.adom.inc` + `/auth/intent` → "Adom auth" variant, defaults to work browser+- Right-click / iframe / unknown context → Pup (AI-drivable browser)+- General → user's last pick for that domain++## Localhost URLs route through the picker too++**Rule:** ALL http(s) URLs go through the picker, including `http://localhost:*` and `http://127.0.0.1:*`. PortMappingsDialog's container-service links must open in the user's chosen browser, NOT in a popup-blocked WebView2 child window.++The `on_new_window` handler in `lib.rs` (line ~1986) MUST dispatch `hydrogen-open-url` for both internal AND external URLs. If you see `is_internal` short-circuiting the dispatch with no `dispatch_url_event` call, that's the bug — internal URLs fall through to Tauri's default `shell.open()` which then fails with the permission error below.++## `shell:default` is non-negotiable++The picker's "open in native browser" path ultimately calls `@tauri-apps/plugin-shell.open()`. That requires BOTH `shell:allow-open` AND `shell:default` in BOTH capability files. Without `shell:default` you get this exact rejection in the Hydrogen log:++```+[JS UNHANDLED REJECTION] shell.open not allowed. Permissions associated with this command: shell:allow-open, shell:default+```++Both files must list both permissions:+- `src-tauri/capabilities/default.json`+- `src-tauri/crates/hydrogen-app/capabilities/default.json` (the hydrogen-app crate's own capabilities)++Tauri compiles capabilities INTO the binary at build time. Capability changes require a Rust rebuild (`hd_build_rust`), not just a frontend rebuild.++## AI automation recipe++```bash+# 1. (Optional) Set Pup as default for domains you want to drive+# Hydrogen UI → Settings → Browser Picker Manager → add domain → choose Pup+# OR let the 5s auto-countdown pick Pup for unknown URLs.++# 2. Trigger a URL open (read the live control URL — the port is dynamic, default 47084)+adom-bridge shell_execute '{"command":"CTRL=\"$(cat ~/.adom/hydrogen-control-url)\"; curl -s -X POST -d {\"url\":\"https://example.com\"} \"$CTRL/open-url\""}'++# 3. Within 5s, picker auto-routes to Pup (or saved pref)+# 4. Drive Pup with adom-bridge browser_* verbs+adom-bridge browser_eval '{"sessionId":"default","js":"document.title"}'++# Force a picker dialog (no auto-timer)+adom-bridge shell_execute '{"command":"CTRL=\"$(cat ~/.adom/hydrogen-control-url)\"; curl -s -X POST -d {\"url\":\"https://example.com\",\"force\":true} \"$CTRL/open-url\""}'++# Route through the picker with the fresh-window toggle PRE-CHECKED (force = skip the+# 5s timer). The user/AI then picks a native browser → opens fresh + auto-foregrounds.+adom-bridge hd_api '{"method":"POST","path":"/open-url","body":{"url":"https://claude.ai/","fresh":true,"force":true}}'+# BYPASS the picker and open directly in a specific browser+profile, fresh:+adom-bridge hd_api '{"method":"POST","path":"/open-in-profile","body":{"url":"https://claude.ai/","browser":"edge","profileDir":"Default","fresh":true}}'+# /open-url with direct:true also bypasses the picker (opens immediately, default browser):+adom-bridge hd_api '{"method":"POST","path":"/open-url","body":{"url":"https://claude.ai/","fresh":true,"direct":true}}'+```++`/open-url` ROUTES THROUGH THE PICKER by default (emits `browser-open-request`):+`fresh` pre-seeds the "Open in a fresh window" toggle, `force:true` skips the 5s+auto-timer, `direct:true` (or naming a concrete `browser`) bypasses the picker and+opens immediately. The response `routed` field is `"browser-picker"` or `"direct"`.+`/open-in-profile` always opens directly (no picker). Both direct paths echo+`method:"msedge.exe … fresh-window"` — read it before claiming success.++## Prefs are now an API — the picker is a FALLBACK, not a gate++The per-domain routing memory is CRUD over the control API, so the **AI governs routing** and+the picker only pops for a domain with no policy + no direct call:+`GET /browser-picker/prefs` (read `{domains:{<host>:{destination,label}}}`),+`PUT /browser-picker/prefs {domain,destination,label?}` (set/override),+`DELETE /browser-picker/prefs/<host>` (forget one), `DELETE /browser-picker/prefs` (clear all).+`destination` is an ID: `hydrogen-tab` (wv) | `webview-window` (**hbw** — Hydrogen's standalone Tauri browser window; also `POST /hbw/open`; close one with `POST /hbw/close {"label":"hbw-<ts>"}`, hbw labels only, from 1.0.142) | `pup-window` (pup), or a native id (**nb**): users address these as `nb <browser> [profile]` — "nb chrome work", "nb edge" — resolve via GET /browser-profiles; the id form is+`browser:<browser>:<dir>` (profile dir encoded in the ID). `fresh` is per-open, not stored.+Writes hit the Manager's live state (localStorage + `hydrogen-url-routing-prefs-changed`). Full+routing-policy guidance lives in **[hydrogen-open-url](../hydrogen-open-url/SKILL.md)**.++## How Hydrogen skills get deployed (workflow note)++Hydrogen public skills (this one, hydrogen-open-url, hydrogen-monitor, etc.) live in the Hydrogen repo under `skills/public-facing/{shared,wsl2}/<name>/SKILL.md`. They're BUNDLED into the Hydrogen bootstraps (adom/hydrogen-bootstrap ships `shared/`, adom/hydrogen-windows-bootstrap ships `wsl2/`) and deployed flat into `~/.claude/skills/<name>/` at install/bake time. See `hydrogen-skill-catalog` (public) and `hydrogen-skills-structure` (dev-internal) for the full layout.
skills/hydrogen-captions/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/hydrogen-captions/SKILL.mdadded+135@@ -0,0 +1,135 @@+---+name: hydrogen-captions+description: >+ Paint labeled caption text on the Hydrogen workspace for demos/walkthroughs —+ captions are CAPTURED BY RECORDINGS so they show up in the finished video. Drive the+ WORKSPACE caption with `adom-cli hydrogen caption` — THE default, identical on+ web-Hydrogen AND Hydrogen (text positional; `-d` duration in SECONDS not ms; 30s auto-expire+ by default, 5-min hard cap, newest-wins, `hide` to clear; a raw-HTTP+ `POST .../current/caption` fallback exists for scripts hitting Hydrogen's API directly).+ Distinct from toasts (notifications) and TTS (audio). NOTE: a full-DESKTOP overlay+ (over every window, on top of other apps) exists as a platform-specific companion —+ see [[hydrogen-captions-windows]] / [[hydrogen-captions-mac]]. Trigger words — caption, on-screen caption, workspace+ caption, hydrogen caption, screen overlay, screen label, callout, annotate the screen,+ step label, put text on screen, overlay text, caption command, adom-cli hydrogen+ caption, walkthrough label, demo callout, status overlay, label the screen, hide+ caption, clear captions, step 3 callout, caption duration, caption seconds, caption+ auto-expire, caption stuck on screen, caption units, caption position, caption size,+ persist caption.+---++# Hydrogen — On-Screen Captions++You can **paint labeled caption text on the Hydrogen workspace** for demos and+walkthroughs — and because captions are real pixels, **they're captured by recordings**,+so they end up in the finished video ("Step 3: run DRC →").++> Captions are **visual on-screen narration** — centered status / countdowns / "what+> I'm doing," great during demos and recordings. Distinct from:+> - **toasts** — corner pop-ups via `POST /ui/toast` (transient notifications, see+> `hydrogen-notifications`); a caption is centered narration, a toast is a corner alert.+> - **TTS** *audio* narration (`adom-tts`) — pair a caption with TTS for a narrated walkthrough.+> - the internal **auth-overlay caption** Hydrogen draws for login/auth flows — not something+> you trigger for demos. The workspace caption below is the one you drive.++## Two caption surfaces — pick by WHAT you're recording++Hydrogen has **two** caption surfaces, each captured by a *different* kind of recording:++| Surface | Command | Renders | Captured by | Use when |+|---|---|---|---|---|+| **Workspace caption** | `adom-cli hydrogen caption` | **inside the Hydrogen workspace** (webview overlay, web-Hydrogen style) | a recording/screenshot of the **Hydrogen window** (panel/workspace) | the demo is **inside Hydrogen** — captioning the workspace itself |+| **Desktop overlay** | platform-specific (see [[hydrogen-captions-windows]] / [[hydrogen-captions-mac]]) | **over the WHOLE desktop**, above every window | a **full-screen / desktop** recording | the demo shows **other apps** (KiCad, Fusion, a browser) or the whole screen |++**Match the caption to the recording.** A `screenshot panel` / workspace capture sees the+**webview** content, so it captures the **workspace** caption but NOT a desktop overlay (a+separate OS window). A full-**screen** recording captures the **desktop overlay** (it's+screen pixels). Pick wrong and your caption won't be in the video.++> Quick rule: "caption the Hydrogen workspace" → `adom-cli hydrogen caption`;+> "caption over the whole screen / while I record KiCad / a corner status label" → the+> desktop overlay in the platform companion (the only one with corners, ids, and x/y).++## Workspace caption — `adom-cli hydrogen caption` ⭐ THE DEFAULT++**Reach for this FIRST.** It's the *same* command on web Hydrogen and Hydrogen (adom-cli routes+through Hydrogen's workspace-events server → SSE → the frontend `captionStore`), so it stays+consistent across both — same principle as screenshots. Renders centered **inside the Hydrogen+workspace webview**. `show` takes the TEXT as a **positional arg** (NOT `--text`); the only+flags are `-d`/`--duration`, `-p`/`--position`, `-s`/`--size` (no `id`, `x`/`y`, or+`fontSize` — those are desktop-overlay-only). Subcommands: `show` / `hide`.++```bash+adom-cli hydrogen caption show "Step 1: open the board" -p center -s large # 30s auto-expire+adom-cli hydrogen caption show "Step 2: run DRC" -d 60 -p center -s large # hold 60 SECONDS+adom-cli hydrogen caption hide # clear now+```++### ⚠️⚠️ `-d` / `duration` is in **SECONDS — NOT milliseconds** ⚠️⚠️+THE gotcha. `-d 30` = 30 seconds. `-d 4000` = **66 MINUTES**, not 4 seconds. If you're+thinking in ms you're off by **1000×**. You almost never need a big number — the 30s+default already covers "leave it up while I do a thing." (Note: the *desktop overlay* in+the platform companion uses **milliseconds** — the two surfaces differ; don't cross the+units.)++> **Web-Hydrogen vs Hydrogen parity:** the **command + the SECONDS unit + `hide` are identical**+> on both (center/large rendering, `-d 20` held visibly). BUT the sane **lifecycle below+> (30s default / 300s cap / persist)** is verified on Hydrogen; web Hydrogen may still return the+> *older terse `_hint`* ("auto-hides after the specified duration") and may not enforce the+> 30s-default/300s-cap yet. **So on web Hydrogen, don't assume a fat `-d` self-heals — pass+> a sane duration.**++### Lifecycle (sane, never abandoned)+- **No `-d` (or `-d 0`) → auto-expires after 30s.** The right default — you do NOT need+ to `hide` it.+- **Positive `-d <seconds>` → holds that many seconds, HARD-CAPPED at 300s (5 min).** A+ larger value still clears at 5 min, so a fat-fingered `-d 4000` self-heals.+- **Newest wins:** showing another caption replaces the current one; `hide` clears immediately.+- **No "forever" via adom-cli** — its `-d` is an unsigned int (rejects negatives) by+ design. This is a **feature, not a gap**: adom-cli is the same binary on web-Hydrogen and+ Hydrogen, so "no persist via adom-cli" keeps both consistent and bounded (no abandoned+ captions). A persist-until-hidden escape exists ONLY on the raw HTTP endpoint+ (`{"duration":-1}`, below) — rarely needed.+- **No `caption status` verb (yet).** There's no way to query "is a caption up / how long+ left" from adom-cli — and with newest-wins + the 30s auto-expire you rarely need to.+- `-p` position: `top` / `center` / `bottom` (NO corners — use the desktop overlay for+ those). `-s` size: `small` / `medium` / `large`.+- Needs a live workspace/editor session (the server returns 409 if none).++### Direct HTTP fallback — only for scripts already hitting Hydrogen's API+adom-cli is the consistent default; use HTTP **only** when a script talks to Hydrogen's API+directly. It's the **same caption, same lifecycle** — adom-cli just wraps this path with+the workspace owner/repo. The caption lives on the **workspace-events server**+(`ADOM_HYDROGEN_URL`, the proxy) — **NOT the control port**:++```bash+# show — duration in SECONDS, identical body/lifecycle to adom-cli+curl -s -X POST "$ADOM_HYDROGEN_URL/api/workspaces/editor/<owner>/<repo>/current/caption" \+ -H 'Content-Type: application/json' \+ -d '{"text":"Step 1: open the board","duration":60,"position":"center","size":"large"}'+# hide+curl -s -X POST "$ADOM_HYDROGEN_URL/api/workspaces/editor/<owner>/<repo>/current/caption" \+ -d '{"hide":true}'+# persist until hidden (HTTP-ONLY escape — adom-cli can't send a negative):+# -d '{"text":"Recording…","duration":-1}'+```+Body: `{text, duration (SECONDS), position, size}`; clear with `{hide:true}`.++## Desktop overlay caption (over the whole screen)++For a caption that floats over **every window** (above other apps, the whole desktop) —+with ids, corners, and normalized x/y placement — Hydrogen provides a native always-on-top,+click-through desktop overlay. That surface and its arguments are **platform-specific**;+see **[[hydrogen-captions-windows]]** / **[[hydrogen-captions-mac]]**.++## Patterns++- **Walkthrough / demo callouts** — drop a `center` caption per step. Combine with a+ recording so the callouts end up in the finished video. Pair with TTS for narration.+- **Status corner / point-at-a-thing** — these need named corners or normalized x/y, which+ only the desktop overlay provides; see the platform companion.++## Related skills+- [hydrogen-adom-desktop](../hydrogen-adom-desktop/SKILL.md) — the relay that carries caption verbs to the host+- `adom-tts` — *audio* narration to pair with on-screen captions (captions are visual-only)+- [[hydrogen-captions-windows]] / [[hydrogen-captions-mac]] — the full-desktop always-on-top overlay caption surface
skills/hydrogen-container-stats/SKILL.md+29−49@@ -1,14 +1,13 @@ --- name: hydrogen-container-stats-description: Hydrogen's workspace stats indicator in the top-right of the title bar — two thin progress bars (CPU + RAM) labelled "Container" plus an info tooltip on hover that shows the workspace name, CPU vs total cores, RAM usage vs limit, disk usage, image/distro, ID, and creation date. Polls once per second when running, every 5 seconds when stopped. Status badges show stopped / starting / restarting / restart-needed states. Use this skill when the user asks about CPU/RAM usage, the resource bars in Hydrogen's title bar, workspace disk usage, the "restart needed" badge, why bars are red/yellow, or what's in the tooltip. Trigger words — container stats, workspace stats, container cpu, container ram, container disk, resource bars, cpu bar, ram bar, container tooltip, container indicator, top bar stats, restart needed badge, container stopped badge, container starting, workspace_stats, distro stats, wsl stats.+description: Hydrogen's workspace stats indicator in the top-right of the title bar — two thin progress bars (CPU + RAM) labelled "Container" plus an info tooltip on hover that shows the workspace name, CPU vs total cores, RAM usage vs limit, disk usage, image, ID, and creation date. Polls once per second when running, every 5 seconds when stopped. Status badges show stopped / starting / restarting / restart-needed states. Use this skill when the user asks about CPU/RAM usage, the resource bars in Hydrogen's title bar, workspace disk usage, the "restart needed" badge, why bars are red/yellow, or what's in the tooltip. Trigger words — container stats, workspace stats, container cpu, container ram, container disk, resource bars, cpu bar, ram bar, container tooltip, container indicator, top bar stats, restart needed badge, container stopped badge, container starting, workspace_stats, workspace stats. --- # Hydrogen Workspace Stats Indicator -> This is the WSL2-runtime version (default). The legacy Docker equivalent-> (`HD_RUNTIME=docker`) is in the docker/ bucket.+The top-right of Hydrogen's title bar (next to the audio/video icons and your profile avatar) shows a tiny `Container` block with two horizontal progress bars — CPU% and RAM% — that animate in real-time. Hovering brings up a tooltip with the full workspace details. The numbers reflect your **local Hydrogen workspace**; the header reads "Container" on all platforms. -The top-right of Hydrogen's title bar (next to the audio/video icons and your profile avatar) shows a tiny `Container` block with two horizontal progress bars — CPU% and RAM% — that animate in real-time. Hovering brings up a tooltip with the full workspace details. Under the WSL2 runtime these numbers now reflect the **`Adom-Workspace` distro**, not a Docker container — but the UI is unchanged, so the header still reads "Container".+> The platform bootstraps override this skill with the host-specific probe details — the `workspace_stats()` health check, the restart command, benign log signals, and disk semantics. ## How often it polls @@ -17,16 +16,16 @@ There's an active polling loop in Hydrogen's title bar that asks the runtime for - **Every 1 second** when the workspace is `running` — for live, smooth bar animation - **Every 5 seconds** when it's `stopped`, `starting`, `restarting`, or `restart needed` — saves cycles when nothing's changing -Each poll calls Tauri `invoke('get_container_stats')`. This command no longer reaches into Docker directly — it dispatches through the runtime trait: `WorkspaceRuntime::get_stats` → `WslDistroRuntime::get_stats` → `workspace_stats()`, a WSL health/stat probe of the `Adom-Workspace` distro. (Under `HD_RUNTIME=docker` the same command routes to the Docker impl instead.) The probe runs with short timeouts so a hung WSL subsystem can't freeze the UI.+Each poll calls Tauri `invoke('get_container_stats')`, which dispatches through the runtime trait (`WorkspaceRuntime::get_stats`) to the active host's probe. The probe runs with short timeouts so a hung host subsystem can't freeze the UI. If you ever want to lighten the load (e.g. on a battery-constrained laptop), the relevant code is in `EditorNav.svelte`'s `pollStats()` closure — adjust the `pollInterval` ternary. ## What the bars show -| Bar | What it measures | Source |-|-----|------------------|--------|-| CPU | % of the distro's available CPU being consumed (1.0 = 1 full vCPU). Capped at 100% display | `workspace_stats()` WSL probe |-| RAM | % of the distro's memory in use vs its limit | `workspace_stats()` WSL probe |+| Bar | What it measures |+|-----|------------------|+| CPU | % of the workspace's available CPU being consumed (1.0 = 1 full vCPU). Capped at 100% display |+| RAM | % of the workspace's memory in use vs its limit | Both bars share the same color-threshold rules: - **Teal / green** — ≤ 50% usage (normal)@@ -39,16 +38,16 @@ When you hover over the bars, a tooltip appears with: | Line | Example | |------|---------|-| Container | `Adom-Workspace` (the distro name) |+| Container | the workspace name | | CPU + RAM totals | `CPU: 1.7% of 16 cores | RAM: 0.7 GB / 16.0 GB` |-| Disk | `Disk: 1.3 GB` (distro disk usage) |-| Image | `Image: Ubuntu 24.04 (Adom-Workspace)` (the imported distro) |-| ID | `Adom-Workspace` (the registered distro identifier) |+| Disk | `Disk: 1.3 GB` (workspace disk usage) |+| Image | `Image: Ubuntu 24.04` |+| ID | the registered workspace identifier | | Created | `5/24/2026, 3:18:09 PM` (locale-formatted) | If the workspace is in a non-healthy state, the tooltip shows a single red line instead: - `Workspace is stopped — start from the menu` (when stopped)-- `Workspace runtime is broken — restart from the menu` (when the distro/code-server is unreachable, e.g. after sleep/hibernate)+- `Workspace runtime is broken — restart from the menu` (when the workspace / code-server is unreachable, e.g. after sleep/hibernate) ## Status badge in the header @@ -57,48 +56,32 @@ The `Container` text in the section header gets an inline status badge depending | Badge | Meaning | Color | |-------|---------|-------| | (no badge) | Running normally | normal |-| `stopped` | Distro registered but not running | red |-| `starting` | Hydrogen is starting the distro / code-server | yellow |-| `restarting` | Hydrogen is restarting the distro | yellow |-| `restart needed` | The distro or code-server is unreachable (usually a WSL2 hiccup after Windows wake) | red |+| `stopped` | Workspace registered but not running | red |+| `starting` | Hydrogen is starting the workspace / code-server | yellow |+| `restarting` | Hydrogen is restarting the workspace | yellow |+| `restart needed` | The workspace or code-server is unreachable (usually a hiccup after the host wakes from sleep) | red | -When `restart needed` shows, open the Adom menu → Restart Workspace (see the `hydrogen-ui` skill for the Adom menu). A "Restart" here is `wsl --terminate Adom-Workspace` + re-ensure code-server — NOT `docker restart`.+When `restart needed` shows, open the Adom menu → Restart Workspace (see the `[[hydrogen-ui]]` skill for the Adom menu). A "Restart" here terminates and re-ensures the workspace + code-server. ## How it works under the hood - **Polling frequency**: see "How often it polls" above — 1s when running, 5s when not.-- **Backend command**: Tauri `invoke('get_container_stats')` → `WorkspaceRuntime::get_stats`. In WSL mode that is `WslDistroRuntime::get_stats` → `workspace_stats()`. NO `docker exec`, NO cgroup file reads, NO bollard `stats()` — those were the Docker-runtime mechanism and do not run under WSL.-- **Two health levels** (`workspace_stats()` checks both): (1) is WSL2 itself available; (2) is `Adom-Workspace` registered + Running with code-server reachable on host port 7380. If either fails, the probe returns a needs-restart signal instead of stats and the UI shows the red "restart needed" badge.+- **Backend command**: Tauri `invoke('get_container_stats')` → `WorkspaceRuntime::get_stats`, routed to the active host runtime's probe.+- **Two health levels** the probe checks: (1) is the host runtime itself available; (2) is the workspace registered + running with code-server reachable on host port 7380. If either fails, the probe returns a needs-restart signal instead of stats and the UI shows the red "restart needed" badge. - **Rate-limited error logging**: the "needs restart" log line is printed at most once every 30 seconds, even though the frontend polls every 1s, so the log stays readable. -### The benign `[docker] unhealthy` log line--You will sometimes see this in Hydrogen's log even on the WSL2 path:--```-[docker] Container runtime unhealthy — exec failed, needs restart-```--**Under WSL this is BENIGN NOISE.** It's a leftover hydrogen-docker heartbeat with no container bound to it — there is no Docker container in WSL mode, so "exec failed" is expected and meaningless. It does NOT mean your workspace is broken.--The real WSL health signal to watch for is:--```-[wsl] Workspace unhealthy …-```--That line (not the `[docker]` one) reflects the actual `Adom-Workspace` distro state. And the authoritative "which runtime am I on" line is `[app] Active workspace runtime: WslDistro`.+The authoritative "which runtime am I on" log line is `[app] Active workspace runtime: …`. (Platform-specific log signals are covered in the platform-specific version of this skill.) ## API access If you want the same stats data your AI can hit directly via the control API: ```bash-# From inside the distro — query the control API on the host+# From inside the workspace — query the control API on the host curl -s "$(cat ~/.adom/hydrogen-control-url)/container-stats" ``` -(Reach the control API at `127.0.0.1` — WSL2 mirrored networking shares loopback with the Windows host. The port is **dynamic per launch**; read the live URL from `~/.adom/hydrogen-control-url` rather than hardcoding it.)+(Read the live URL from `~/.adom/hydrogen-control-url` rather than hardcoding it — the port is **dynamic per launch**.) Returns JSON like: ```json@@ -107,7 +90,7 @@ Returns JSON like: "container_name": "Adom-Workspace", "container_id": "Adom-Workspace", "container_created": "2026-05-24T20:18:09Z",- "image": "Ubuntu 24.04 (Adom-Workspace)",+ "image": "Ubuntu 24.04", "cpu_percent": 1.7, "cpu_cores": 16, "mem_usage_mb": 716,@@ -139,16 +122,13 @@ Or if the workspace is in a broken state: **"Why is CPU showing 200% / 1600%?"** It's not — the displayed % is capped at 100 for the bar fill, but the tooltip shows the real number. CPU of `100% of 16 cores` means 1 full vCPU pegged. -**"Why does RAM stay constant at ~4% even when I'm doing nothing?"** Code-server, the relay, gallia, the adom-vscode HTTP API, etc. all run in the distro all the time. ~700 MB baseline is normal.--**"The 'restart needed' badge appeared — what happened?"** Almost always a WSL2 hiccup after Windows wake/sleep, leaving the distro or code-server unreachable. Click Restart Workspace in the Adom menu (top-left). Hydrogen will `wsl --terminate Adom-Workspace`, re-ensure code-server, and the badge clears.+**"Why does RAM stay constant at ~4% even when I'm doing nothing?"** Code-server, the relay, gallia, the adom-vscode HTTP API, etc. all run in the workspace all the time. ~700 MB baseline is normal. -**"The log says `[docker] … unhealthy` — is my workspace broken?"** No. Under WSL that line is a benign leftover heartbeat with no container bound. Look at `[wsl] Workspace unhealthy …` for the real signal. See "The benign `[docker] unhealthy` log line" above.+**"The 'restart needed' badge appeared — what happened?"** Almost always a hiccup after the host woke from sleep/hibernate, leaving the workspace or code-server unreachable. Click Restart Workspace in the Adom menu (top-left); Hydrogen restarts the workspace, re-ensures code-server, and the badge clears. -**"Disk is 1.3 GB — does that include my project files?"** Yes — under WSL there is no separate Docker volume; `/home/adom/project` lives inside the distro, so the `Disk:` figure is the distro's on-disk size including your project.+**"Disk is 1.3 GB — does that include my project files?"** Yes — `/home/adom/project` lives inside the workspace, so the `Disk:` figure includes your project. ## Related skills -- [hydrogen-workspace-monitoring](../hydrogen-workspace-monitoring/SKILL.md) — the 15s state poll + floaty states behind these badges-- [hydrogen-workspace-lifecycle](../hydrogen-workspace-lifecycle/SKILL.md) — start/stop/restart/unregister of the distro-- [hydrogen-networking](../hydrogen-networking/SKILL.md) — port 7380 code-server, host reachability, control port 47084+- `[[hydrogen-workspace-monitoring]]` — the 15s state poll + floaty states behind these badges+- `[[hydrogen-container]]` — workspace context: relay, bridges, env vars
skills/hydrogen-container/SKILL.md+28−29@@ -1,47 +1,44 @@ --- name: hydrogen-container-description: Context for Claude Code running inside a Hydrogen WSL2 workspace distro. Documents the exact OS (Ubuntu 24.04, code-server 4.112.0, the Adom-Workspace distro), explains how setup differs from Adom cloud containers (setup-steps NOT bootstrap.sh), what bridges are available, and how to use the relay. Trigger on startup, adom-cli errors, bridge commands, screenshot requests, container-platform questions, code-server / VS Code extension issues, or when the AI needs concrete facts about its environment instead of guessing.+description: Context for Claude Code running inside a Hydrogen workspace. Documents the exact OS (Ubuntu 24.04, code-server, the local workspace), explains how setup differs from Adom cloud containers (Hydrogen setup-steps NOT bootstrap.sh), what bridges are available, and how to use the relay. Trigger on startup, adom-cli errors, bridge commands, screenshot requests, container-platform questions, code-server / VS Code extension issues, or when the AI needs concrete facts about its environment instead of guessing. --- -# Hydrogen Workspace (WSL2)+# Hydrogen Workspace -WSL2-runtime version (default). Legacy Docker container runtime (`HD_RUNTIME=docker`) is in the docker/ bucket.+You are running inside the **local workspace** managed by Hydrogen (ah). Hydrogen is the flagship Adom desktop app — it manages your workspace, bridges to desktop apps, and provides VS Code + Claude Code. Hydrogen runs on Windows, macOS, and Ubuntu; the platform-specific mechanics of how the workspace is hosted are abstracted away from inside the workspace — the facts below hold regardless of host OS. -You are running inside the **`Adom-Workspace` WSL2 distro** managed by Hydrogen (ah). Hydrogen is the flagship Adom desktop app — it manages your WSL2 workspace, bridges to desktop apps, and provides VS Code + Claude Code.+> The platform bootstraps override this skill with the host-specific spine — how the `Adom-Workspace` workspace is hosted and imported, code-server port forwarding, the runtime source, and the Cloud-vs-local comparison table. ## The workspace — exact facts (don't guess) | Field | Value | |-------|-------|-| Distro name | `Adom-Workspace` (fixed — there is no random container suffix) |-| Imported from | golden image `adom-golden.tar.gz` (full pre-baked image) via `wsl --import` |+| Provisioned from | a pre-baked golden image (full image, not bootstrapped from scratch) | | Base | Ubuntu 24.04 |-| Architecture | matches host (`dpkg --print-architecture`) — almost always `linux-x64`; `linux-arm64` on Windows-on-ARM hosts |-| code-server | 4.112.0 (pinned via setup step) |+| Architecture | matches host (`dpkg --print-architecture`) — `linux-x64` or `linux-arm64` |+| code-server | pinned via the golden image | | code-server binary | `/usr/lib/code-server/bin/code-server` | | Extensions dir | `~/.local/share/code-server/extensions/` | | Settings | `~/.local/share/code-server/User/settings.json` |-| Distro default user | `adom` (`wsl.conf [user] default=adom`) — in-distro exec via `wsl -d Adom-Workspace -u adom --`; root via `-u root` |-| Work user | `adom` (UID 1001, passwordless sudo, home `/home/adom`) |-| Workspace | `/home/adom/project` (lives directly in the distro's ext4 filesystem) |+| Work user | `adom` (passwordless sudo, home `/home/adom`) |+| Workspace | `/home/adom/project` | | Pre-installed | Node 18 + npm, Python 3.12, git, gh, curl, wget, jq, build-essential, cmake, pkg-config, libssl-dev |-| Code-server runs as | `code-server --bind-addr 0.0.0.0:7380 --auth none --disable-telemetry /home/adom/project` (WSL2 auto-forwards 7380 to Windows `localhost:7380`) |-| Runtime source | `hydrogen-desktop/src-tauri/crates/hydrogen-app/src/runtime/wsl.rs` (selector `runtime/mod.rs::select_runtime()`) |+| Code-server runs as | `code-server --bind-addr 0.0.0.0:7380 --auth none --disable-telemetry /home/adom/project` | If you ever need to confirm: `cat /etc/os-release`, `uname -m`, `/usr/lib/code-server/bin/code-server --version`. Don't speculate about alpine/arm64 — Ubuntu 24.04 matches the cloud container. ## Setup is via Hydrogen's setup-steps, NOT gallia bootstrap.sh -Hydrogen provisions this distro through its own **setup-steps** flow (Rust code in `hydrogen-desktop/src-tauri/crates/hydrogen-app/src/setup_steps_wsl.rs`, with the runtime mechanics in `hydrogen-desktop/src-tauri/crates/hydrogen-app/src/runtime/wsl.rs`), shown as the "Install Tools" panel in the Hydrogen UI. It is a **different code path** than cloud containers, which run `gallia/scripts/bootstrap.sh`.+Hydrogen provisions this workspace through its own **setup-steps** flow, shown as the "Install Tools" panel in the Hydrogen UI. It is a **different code path** than cloud containers, which run `gallia/scripts/bootstrap.sh`. -The WSL cascade is **28 steps** (state file `setup-steps-wsl.json`) vs the legacy Docker flow's 24. Most tooling is NOT installed by a step — it's BAKED into `adom-golden.tar.gz` at image-build time (gallia, the 8 Adom CLIs, the Claude Code extension, code-server, VS Code settings, and all `hd-*` skills). The one exception is the `claude` CLI, which is installed at RUNTIME by step 13 `install-claude-cli` (its self-setup needs a live session). The Docker `pull-image` / `create-container` / `start-container` steps are collapsed into a single step 1, `ensure-workspace`, which runs `wsl --import` of `adom-golden.tar.gz` if the distro isn't already registered. The relay is started by the `start-relay` step. See `hydrogen-golden-image`.+Most tooling is NOT installed by a step — it's BAKED into the golden image at image-build time (gallia, the Adom CLIs, the claude CLI + Code extension, code-server, VS Code settings, and all `hd-*` skills). The provisioning step imports the golden image if the workspace isn't already registered, and the relay is started by the `start-relay` step. See `[[hydrogen-setup-steps]]`. -When debugging install issues here, the source of truth is `setup_steps_wsl.rs` (and `runtime/wsl.rs`) — its step IDs (`ensure-workspace`, `install-adom-vscode`, `set-env-vars`, `inject-api-key`, `start-relay`, `claude-auth`, etc.) map 1:1 to what ran on this distro. There is NO `install-claude-ext` / `install-claude-cli` / `install-gallia` / `write-vscode-settings` step anymore — those are baked. Do NOT chase bugs into `bootstrap.sh` first — that script lives in the baked image but Hydrogen itself never invokes it. (For the legacy Docker `setup_steps.rs` flow see the docker/ bucket.)+When debugging install issues here, the source of truth is Hydrogen's setup-steps code — its step IDs (`ensure-workspace`, `install-adom-vscode`, `set-env-vars`, `inject-api-key`, `start-relay`, `claude-auth`, etc.) map 1:1 to what ran on this workspace. There is NO `install-claude-ext` / `install-claude-cli` / `install-gallia` / `write-vscode-settings` step — those are baked. Do NOT chase bugs into `bootstrap.sh` — that script lives in the baked image but Hydrogen itself never invokes it. ## VS Code extension caveats - The Claude Code extension and `code-server` settings (including `extensions.autoUpdate: true` / `extensions.autoCheckUpdates: true`) are BAKED into the golden image — there is no `write-vscode-settings` or `install-claude-ext` setup step. Auto-update **must** stay on so every installed extension auto-updates from the marketplace; the image bakes it on. Do not regress this.-- Bare `code-server --install-extension <id>` (no version pin) historically resolves to the **universal** target-platform build. For some extensions (notably `anthropic.claude-code` after 2.1.89), Anthropic stopped shipping universal builds — so bare install permanently sticks at 2.1.89 even when newer `linux-x64` builds exist. The golden-image bake installs the latest `linux-x64` build pinned, so the workspace ships current. It also heals orphan `extensions.json` entries (entries pointing to deleted dirs cause a permanent "Invalid extensions detected" banner).+- Bare `code-server --install-extension <id>` (no version pin) historically resolves to the **universal** target-platform build. For some extensions (notably `anthropic.claude-code` after 2.1.89), Anthropic stopped shipping universal builds — so bare install permanently sticks at 2.1.89 even when newer per-arch builds exist. The golden-image bake installs the latest per-arch build pinned, so the workspace ships current. It also heals orphan `extensions.json` entries (entries pointing to deleted dirs cause a permanent "Invalid extensions detected" banner). - Marketplace: code-server uses **Open VSX** (open-vsx.org), not Microsoft Marketplace. No `EXTENSIONS_GALLERY` env override is set; `product.json` has no gallery URL — it falls through to Open VSX defaults. ## adom-cli is authenticated@@ -54,12 +51,12 @@ If this returns user data, all adom-cli commands work (containers, repos, orgs, ## Relay server is running -The adom-bridge-cli relay runs on ports 8765 (WebSocket) / 8766 (HTTP) inside this distro. The `start-relay` setup step launches it.+The adom-bridge-cli relay runs on ports 8765 (WebSocket) / 8766 (HTTP) inside this workspace. The `start-relay` setup step launches it. Check health: `curl -sf http://127.0.0.1:8766/health` Check desktop connection: `adom-bridge-cli ping` -The Windows host control API is reachable from inside the distro at `127.0.0.1` — WSL2 mirrored networking shares loopback with the Windows host. The port is dynamic per launch, so read the live URL from `~/.adom/hydrogen-control-url` (`http://127.0.0.1:<dynamic>`): `BASE="$(cat ~/.adom/hydrogen-control-url)"; curl "$BASE/health"`.+The host control API is reachable from inside the workspace at `127.0.0.1`. The port is dynamic per launch, so read the live URL from `~/.adom/hydrogen-control-url` (`http://127.0.0.1:<dynamic>`): `BASE="$(cat ~/.adom/hydrogen-control-url)"; curl "$BASE/health"`. ## Available bridge commands (when desktop is connected) @@ -68,7 +65,7 @@ The Windows host control API is reachable from inside the distro at `127.0.0.1` adom-bridge-cli desktop_screenshot_screen # full screen capture adom-bridge-cli desktop_screenshot_screen '{"maxWidth":1500}' # resized for Claude vision adom-bridge-cli desktop_screenshot_window '{"hwnd": N}' # capture specific window-adom-bridge-cli desktop_list_windows # list all windows with HWNDs+adom-bridge-cli desktop_list_windows # list all windows with handles ``` Default to `maxWidth: 1500, format: "png"` for UI screenshots. Use `"jpeg"` for natural photos, `"webp"` for smaller files. @@ -82,11 +79,12 @@ adom-bridge-cli browser_navigate '{"url": "..."}' ### KiCad bridge ```bash-adom-bridge-cli kicad_open_board '{"path": "C:\\path\\to\\board.kicad_pcb"}'+adom-bridge-cli kicad_open_board '{"path": "/path/to/board.kicad_pcb"}' adom-bridge-cli kicad_screenshot_all adom-bridge-cli kicad_run_drc '{"path": "..."}' adom-bridge-cli kicad_window_info ```+(Paths to host files use the host's native path convention.) ### Fusion 360 bridge ```bash@@ -98,21 +96,21 @@ adom-bridge-cli fusion_export_step '{"path": "..."}' ### File transfer ```bash adom-bridge-cli send_files '{"files": [{"path": "/home/adom/file.txt"}]}'-adom-bridge-cli pull_file '{"remotePath": "C:\\Users\\john\\file.txt", "localPath": "/tmp/file.txt"}'+adom-bridge-cli pull_file '{"remotePath": "<host path>", "localPath": "/tmp/file.txt"}' ``` ### Desktop interaction ```bash adom-bridge-cli notify_user '{"message": "Build complete", "duration_ms": 3000}' adom-bridge-cli desktop_open_url '{"url": "https://..."}'-adom-bridge-cli desktop_open_folder '{"path": "C:\\Users\\john\\project"}'+adom-bridge-cli desktop_open_folder '{"path": "<host folder path>"}' ``` ## What's different from Adom cloud containers -| Feature | Adom Cloud | Hydrogen Local (WSL2) |-|---------|-----------|-----------------|-| Workspace provisioning | Adom platform API | `wsl --import` of `adom-golden.tar.gz` on the user's machine |+| Feature | Adom Cloud | Hydrogen Local |+|---------|-----------|----------|+| Workspace provisioning | Adom platform API | golden image imported on the user's machine | | Relay connection | wss:// through Cloudflare | ws://localhost:8765 (direct, no TLS) | | Bridge latency | ~50-100ms (internet round-trip) | <1ms (localhost) | | VSCODE_PROXY_URI | Coder proxy URL | http://localhost:7380/proxy/{{port}}/ |@@ -122,11 +120,12 @@ adom-bridge-cli desktop_open_folder '{"path": "C:\\Users\\john\\project"}' ## Environment variables - `ADOM_DESKTOP_MODE=local` — indicates Hydrogen local mode-- `HD_RUNTIME=wsl2` — indicates the WSL2 runtime (default); `docker` selects the legacy container runtime - `GALLIA_SERVICE=local` — tells gallia this is a local workspace - `VSCODE_PROXY_URI=http://localhost:7380/proxy/{{port}}/` — code-server's proxy +(Platform layers may set additional host-specific env vars — see your platform bootstrap's version of this skill.)+ ## Related skills -- `hydrogen-topology` — the three-tier topology; tier 3 is this WSL2 distro-- `hydrogen-volume` — where your files live, what persists, and how to back up / copy out the distro+- `[[hydrogen-container-stats]]` — the title-bar CPU/RAM indicator for this workspace+- `[[hydrogen-setup-steps]]` — what each setup step did to prepare your workspace
skills/hydrogen-desktop-sse/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/hydrogen-desktop-sse/SKILL.mdadded+241@@ -0,0 +1,241 @@+---+name: hydrogen-desktop-sse+description: How Hydrogen's local workspace API + SSE works (a.k.a. the workspace API) — replaces Carbon for workspace commands so the workspace's adom-cli controls Hydrogen, not cloud Hydrogen. The workspace is the WSL2 Adom-Workspace distro. Covers the full pipeline, routing, env vars, field format differences, and hard-won lessons. Trigger words — Hydrogen SSE, workspace API, local API, adom-cli workspace, add-tab, move-tab, webview tab, EventSource, workspace_updated, ADOM_CARBON_URL, ADOM_HYDROGEN_URL.+---++# Hydrogen — Local Workspace API + SSE++## Architecture++```+Workspace adom-cli (the WSL2 Adom-Workspace distro)+ → http://127.0.0.1:47083 (Hydrogen's proxy)+ → Hydrogen handles workspace commands locally + broadcasts SSE+ → Hydrogen frontend receives SSE, re-fetches workspace, renders++Cloud Docker's adom-cli (unchanged)+ → carbon.adom.inc → Hydrogen web+```++Hydrogen runs its own workspace API on port 47083 (reverse proxy, default — see PortConfig) so the workspace's adom-cli controls Hydrogen's UI, not cloud Hydrogen. No cross-talk between environments. (Under WSL2 the workspace reaches Hydrogen at `127.0.0.1:47083` — mirrored networking shares loopback with the Windows host.)++## Port 47083 routing (the hydrogen-proxy / reverse-proxy)++All requests hit port 47083 (reverse proxy, default — see PortConfig). The router in `handle_request()` decides:++| Path pattern | Handler | Where |+|---|---|---|+| `/api/workspaces/editor/{owner}/{repo}/current/*` | Local workspace API | In-memory |+| `/api/panels/webview/*`, `/api/panels/sandbox/*` | Local panel API | SSE broadcast |+| `/containers/*` | Local container discovery | Canned response |+| `/proxy/{port}/{path}` | Workspace proxy | TCP forward to the workspace (WSL2 distro) |+| Everything else (`/api/*`, etc.) | Carbon proxy | HTTPS to carbon.adom.inc |++The dev file server on port 1420 forwards `/api/*` directly to `handle_request()` (same function, not TCP — supports SSE streaming via BoxBody).++## SSE Connection++**Endpoint:** `GET /api/workspaces/editor/{owner}/{repo}/current/events`++Uses `tokio::sync::broadcast` channel. On connect sends `{"type":"connected","connectionId":"hydrogen-local"}`. Workspace mutations broadcast `{"type":"workspace_updated"}`.++**Frontend connects** in `PanelWorkspaceComponent.svelte` → `setupSSE()`. In Tauri mode, `$page.params` may be empty (adapter-static), so it falls back to `owner=local, repo=workspace`.++**Critical lesson:** The `onopen` handler PUTs the workspace state. This broadcasts `workspace_updated`, which the frontend re-fetches. Set `skipNextSSESync = true` before the PUT to prevent the feedback loop that kills the connection.++## Container Discovery++adom-cli calls `/containers/{slug}` to get owner/repo. The slug comes from parsing `VSCODE_PROXY_URI`'s hostname — split on `-`, take the LAST segment.++**Response format** (must match Carbon's exactly):+```json+{+ "repository": {+ "owner": { "name": "jlauer12" },+ "name": "workspace"+ },+ "slug": "hdlocal",+ "status": "running"+}+```++NOT `{"owner": "jlauer12", "repository": "workspace"}` — adom-cli reads `container["repository"]["owner"]["name"]`.++## Workspace Env Vars++Set by the WSL2 workspace-creation code when the distro is created:++| Var | Value | Why |+|---|---|---|+| `VSCODE_PROXY_URI` | `http://hydrogen-hdlocal/proxy/{{port}}/` | Slug = `hdlocal` (last segment after `-`) |+| `ADOM_CARBON_URL` | `http://127.0.0.1:47083` | Container discovery + non-workspace API |+| `ADOM_HYDROGEN_URL` | `http://127.0.0.1:47083` | Workspace mutations (adom-cli sends these HERE, not to Carbon) |+| `ADOM_DESKTOP_MODE` | `local` | Tells adom-cli it's a local environment |++Under WSL2, `127.0.0.1` reaches the Windows host directly — mirrored networking shares loopback with the host, so the same `127.0.0.1:<port>` works from inside the distro and on Windows. The canonical code-server port Hydrogen exposes on the host is 7380.++**Also set in adom-cli config** (persists across sessions):+```bash+adom-cli config set-url http://127.0.0.1:47083+adom-cli config set-token <session-token>+```+Plus manually write `hydrogen_url` to `~/.config/adom-cli/config.json` (no CLI command for it).++**API key** injected at `/var/run/adom/api-key` by bootstrap via `GET "$(cat ~/.adom/hydrogen-control-url)/auth-token"` (the control URL is `http://127.0.0.1:<dynamic>`).++## Workspace Tree Format++The frontend serializes splits with `first`/`second`, NOT `children`:+```json+{+ "type": "split",+ "id": "desktop-root-split",+ "direction": "horizontal",+ "ratio": 0.5,+ "first": { "type": "leaf", "id": "pane-left", ... },+ "second": { "type": "leaf", "id": "pane-right", ... }+}+```++All tree traversal functions (`find_leaf_path`, `find_split_path`, `resolve_path`) must handle BOTH `children` array and `first`/`second` keys.++## REST Endpoint Field Names (adom-cli vs Hydrogen)++adom-cli sends different field names than you might expect:++| Endpoint | adom-cli sends | Hydrogen must read |+|---|---|---|+| POST /tabs | `{panelId, panelType, displayName, initialState}` | Construct tab with UUID, merge initialState into panelState |+| POST /moves | `{sourcePanelId, tabId, targetPanelId}` | NOT `sourcePanel`/`targetPanel` |+| DELETE /tabs | `{panelId, tabId}` or `{name}` | Support both |++## Smoke Test (from inside the workspace)++```bash+# 1. Probe — confirms SSE alive+adom-cli hydrogen probe+# → browser_connected: true, sse_connections: 1++# 2. Get workspace+adom-cli hydrogen workspace get++# 3. Add tab+adom-cli hydrogen workspace add-tab \+ --panel-id desktop-pane-right \+ --panel-type adom/a1b2c3d4-0031-4000-a000-000000000031 \+ --display-name "Test Page" \+ --initial-state '{"url":"https://wiki.adom.inc/adom/adom-shotlog"}'++# 4. Move tab+adom-cli hydrogen workspace move-tab \+ --from-panel-id desktop-pane-right \+ --tab-id <tab-id-from-step-3> \+ --to-panel-id desktop-pane-left++# 5. Screenshot to verify+adom-bridge-cli hd_screenshot+```++## What Broke and Why++### SSE never connected on fresh launch+`setupSSE()` checks `$page.params.owner` and `$page.params.repo`. In Tauri mode with adapter-static, these are empty. **Fix:** fall back to `owner=local, repo=workspace` when in Tauri mode.++### SSE connected then immediately disconnected+The `onopen` handler PUTs workspace state → broadcasts `workspace_updated` → frontend re-fetches → gets the state it just PUT → re-applies → triggers autosave → another PUT → loop. **Fix:** set `skipNextSSESync = true` before the `onopen` PUT.++### adom-cli "Could not get owner from container info"+Three stacked issues:+1. `VSCODE_PROXY_URI` was `http://localhost:8080/...` → slug parsed as `8080` → called `/containers/8080` → Carbon 404+2. Container info response had flat `{"owner": "..."}` but adom-cli reads `container["repository"]["owner"]["name"]`+3. `ADOM_HYDROGEN_URL` wasn't set → workspace commands went to `hydrogen.adom.inc` instead of Hydrogen++### add-tab returned "Missing 'tab' in body"+Hydrogen expected `{"panelId", "tab": {id, panelType, panelState}}` but adom-cli sends `{"panelId", "panelType", "displayName", "initialState"}` — flat format. **Fix:** accept both, construct tab with UUID for the flat format.++### move-tab returned "Source/target panel not found"+Two issues:+1. Handler read `sourcePanel`/`targetPanel` but adom-cli sends `sourcePanelId`/`targetPanelId`+2. `find_leaf_path` only traversed `children` array, not `first`/`second` keys++### Webview tabs were blank + opened in native browser+`add_FrameNavigationStarting` was blocking ALL external URLs in child iframes, including legitimate webview panel content. **Fix:** stop blocking frame navigations entirely — webview panels need external URLs. URL interception for window.open still works via `on_new_window`.++### Google showed "blocked" icon+Not an Hydrogen bug. Google sends `X-Frame-Options: SAMEORIGIN` which prevents iframe embedding. Sites that allow embedding (adom.inc, wiki) load fine.++## Port Discovery — How Any Client Finds Hydrogen++All Hydrogen ports are dynamic (configurable via `ports.json`). The ONE constant is the **discovery port: 47080**. It never changes.++```+GET http://127.0.0.1:47080/discover+```++Returns:+```json+{+ "ok": true,+ "code_server": 7380,+ "proxy": 47083,+ "control": 47084,+ "cdp": 47085,+ "hostname": "hd.localhost"+}+```++> **Relay ports:** under the WSL2 runtime the relay listens **same-port 8765+> (WS) / 8766 (HTTP)** inside the distro and is reached same-port via mirrored+> loopback (or via `/proxy/`). There are **no host 47081/47082 listeners**.+> Clients discover the relay ports from `~/.adom/cli-relay-ports.json` (fallback+> 8765/8766), not from `/discover`.++### Who uses it++| Client | What it discovers | How |+|---|---|---|+| adom-bridge-cli relay | Control API port (was hardcoded 9001) | `curl :47080/discover` → use `control` field |+| adom-cli in the workspace (the WSL2 distro) | Proxy + Hydrogen URL | Already set via `ADOM_CARBON_URL` env var at workspace creation |+| External tools / diagnostics | Any port | Hit discovery, get the full map |+| Cloud Docker (this container) | All Hydrogen ports | `curl http://<desktop-ip>:47080/discover` via relay |++### Implementation++Tiny `std::net::TcpListener` on port 47080 in `hydrogen-app/src/lib.rs`. Runs on a plain OS thread (no tokio). Reads `get_runtime_ports()` on each request. Returns raw HTTP response with JSON body.++```rust+std::thread::spawn(move || {+ let listener = std::net::TcpListener::bind("127.0.0.1:47080").unwrap();+ for stream in listener.incoming() {+ let ports = hd_control::get_runtime_ports();+ let body = serde_json::json!({ "ok": true, ...ports });+ // write HTTP/1.1 200 OK + JSON+ }+});+```++### Port defaults (PortConfig)++| Service | Default Port | User-visible? | Workspace Port |+|---|---|---|---|+| Discovery | **47080** (FIXED) | No | n/a |+| Code-server | 7380 | Yes (in webview URLs) | 8080 |+| Proxy | 47083 | No | n/a |+| Control API | 47084 | No | n/a |+| CDP | 47085 | No | n/a |+| Relay WS | — (same-port under WSL2) | No | 8765 |+| Relay HTTP | — (same-port under WSL2) | No | 8766 |++Config stored at `%APPDATA%\hydrogen-desktop\ports.json`. On first launch, defaults are written without scanning. Workspace-side ports (8080, 8765, 8766) are always fixed — only host-side ports are dynamic. The relay (8765/8766) is reached **same-port** under WSL2 (mirrored loopback) — no host 47081/47082 mapping; discover it from `~/.adom/cli-relay-ports.json` (fallback 8765/8766).++## Files++| File | What |+|---|---|+| `src-tauri/crates/hydrogen-proxy/src/lib.rs` | Workspace API, SSE, Carbon proxy, tree mutations (~1500 lines) |+| `src-tauri/crates/hydrogen-control/src/lib.rs` | `/auth-token` endpoint, `/ports` endpoint, `/setup/*` routes |+| `src-tauri/crates/hydrogen-control/src/ports.rs` | PortConfig struct, load/save/resolve, DISCOVERY_PORT constant |+| `src-tauri/crates/hydrogen-app/src/lib.rs` | Discovery server on :47080, port resolution at startup |+| `src/lib/stores/portStore.ts` | Frontend Svelte store, `initPorts()`, `controlUrl()`, `codeServerUrl()` |+| `src/lib/components/editor/workspaces/PanelWorkspaceComponent.svelte` | Frontend SSE setup, autosave, workspace re-fetch |+| `scripts/hydrogen-bootstrap.sh` | Sets adom-cli config, injects API key, uses `HD_CONTROL_PORT` env var |
skills/hydrogen-file-transfer/SKILL.mdadded+92@@ -0,0 +1,92 @@+---+name: hydrogen-file-transfer+description: >+ Move files BOTH WAYS between the Hydrogen WSL2 Ubuntu workspace (the Adom-Workspace+ distro, where your code lives at /home/adom/project) and the user's Windows PC+ — including their Desktop, Downloads, and any folder. Three methods: (1) the+ auto-mounted C: drive at /mnt/c inside the distro, (2) the \\wsl$ network path+ from Windows Explorer, and (3) the adom-bridge-cli relay (send_files / pull_file)+ relay. Use when the user asks "how do I+ get this file onto my desktop", "copy this out of the workspace", "get a file+ from my PC into the workspace", "move a file in/out", "where do my downloads+ go", or "drag a file into the container". Trigger words: copy file out, copy+ file in, move file to desktop, get file from desktop, file transfer, /mnt/c,+ wsl$, \\wsl.localhost, send_files, pull_file, export file, import file, drag+ into workspace, file to my pc, download to desktop, upload to workspace.+---++# Move files between the workspace and the Windows PC++Hydrogen's default workspace is the **WSL2 `Adom-Workspace` distro**. Your work lives+at `/home/adom/project` inside it. There are three ways to move files to/from the+user's real Windows machine. Methods 1–2 are WSL2-native (fast, no relay);+method 3 goes through the Adom Bridge relay.++## Method 1 — `/mnt/c` (from INSIDE the distro) — simplest++Windows drives are auto-mounted in the distro: `C:\` is `/mnt/c`. So you can `cp`+straight to/from the user's Desktop or Downloads. (Find the Windows username from+`/mnt/c/Users/` if you don't know it.)++```bash+WINUSER=$(ls /mnt/c/Users | grep -viE 'public|default|all users' | head -1)++# Workspace → Desktop+cp /home/adom/project/report.pdf "/mnt/c/Users/$WINUSER/Desktop/"++# Desktop → workspace+cp "/mnt/c/Users/$WINUSER/Desktop/data.csv" /home/adom/project/++# Downloads folder works the same: /mnt/c/Users/$WINUSER/Downloads/+```++## Method 2 — `\\wsl$` (from Windows Explorer) — drag & drop++From Windows, the distro is a network share. In Explorer's address bar:++```+\\wsl$\Adom-Workspace\home\adom\project+ (newer Windows also: \\wsl.localhost\Adom-Workspace\home\adom\project)+```++The user can **drag files in/out** of that folder to/from their Desktop. Tell+them this path when they want to grab their code or drop a file in by hand.++## Method 3 — the adom-bridge-cli relay (works on ANY runtime)++Hydrogen embeds Adom Bridge, which bridges the workspace to the real OS. Use this when+you want the AI to push/pull files programmatically, or when you need a specific+Save-As target directory on the PC. See **hydrogen-adom-bridge-cli**.++```bash+# Workspace → PC (lands in the user's Downloads by default; large files OK)+adom-bridge-cli send_files '{"filePaths":["/home/adom/project/out.zip"],"saveTo":"C:\\Users\\john\\Desktop"}'++# PC → workspace+adom-bridge-cli pull_file '{"filePaths":["C:\\Users\\john\\Desktop\\photo.png"],"saveTo":"/home/adom/project"}'+```++Notes:+- Confirm the bridge first: `adom-bridge-cli ping` → `pong`. If not connected, the+ user must have Hydrogen running/foregrounded.+- Files whose basename starts with `_` are silently dropped by the relay — use+ plain names.++## Which method to use++| Situation | Use |+|---|---|+| You're scripting inside the workspace (WSL2) | **Method 1** (`/mnt/c`) — fastest |+| User wants to drag files by hand | **Method 2** (`\\wsl$` path) |+| Programmatic, or you need a Save-As target dir | **Method 3** (relay) |++## Where things persist+`/home/adom/project` survives a workspace restart; it is wiped by a **virgin+reset** (`wsl --unregister Adom-Workspace`). Copy anything important to the PC+(any method above) before a virgin reset. See **hydrogen-volume** for the full+persistent-vs-ephemeral map.++## Related+- **hydrogen-volume** — what persists, where files live, backing up before virgin reset+- **hydrogen-adom-bridge-cli** — the relay (send_files / pull_file) and OS reach+- **hydrogen-workspace-lifecycle** — distro import/export/unregister
skills/hydrogen-golden-image/SKILL.mdadded+86@@ -0,0 +1,86 @@+---+name: hydrogen-golden-image+description: >+ Your Hydrogen workspace starts from a THIN golden WSL2 image (v24-thin and+ later): the image carries ONLY the OS baseline — Ubuntu, systemd, code-server,+ the folder contract, and the adom-wiki CLI. EVERYTHING Adom (the adom/core+ toolchain, the 8 CLIs, all hydrogen-* skills, the editor extensions including+ Claude Code) is installed LIVE by the setup cascade's first+ `adom-wiki pkg install adom/hydrogen-windows-bootstrap`, and kept current by+ the registry updater. READ THIS when a user asks "where did these skills/CLIs+ come from", "why did first-run setup take a few minutes", "how do I update+ the workspace", or "what's actually in the image". Trigger words — golden+ image, thin image, pre-baked, baked image, adom-golden, adom-golden.tar.gz,+ wsl --import, tarball version, adom-tarball-version, migrate tarball,+ install-hydrogen-skills, where did the CLIs come from, how do I update the+ workspace, re-run setup, lightweight image, install on demand, command not+ found, missing tool, apt-get install, is ffmpeg installed.+---++# Hydrogen Workspace = a thin golden image + a live first install++Since image **v24-thin** (2026-08-13, John's call: "simple golden image, setup+steps configure everything") the golden image is deliberately MINIMAL. What the+tarball actually contains:++- Ubuntu base + systemd (PID 1), the adom user, the folder contract+ (`~/project` is the workspace root)+- **code-server** (the VS Code backend) + its systemd unit+- systemd units for the relay and shotlog, guarded by `ConditionPathExists` so+ they wait quietly until setup installs their binaries+- the **adom-wiki CLI seed** at `~/.local/bin/adom-wiki` — the installer that+ brings everything else+- a first-boot oneshot that mints the per-import `/etc/adom-distro-id`++**Everything else arrives at setup time.** The setup cascade's update-packages+step runs `adom-wiki pkg install adom/hydrogen-windows-bootstrap`, the leaf+package whose dependency tree pulls hydrogen-bootstrap → adom/core and the+whole ecosystem: ~150 skills, ~22 packages, the 8 Adom CLIs, and ALL editor+extensions (adom-theme, adom-vscode, anthropic.claude-code). Measured cost:+about 7 minutes on a fresh install — that is the tradeoff for an image that is+125MB smaller and for day-one installs that are ALWAYS current (no stale baked+package can ever exist again; the slug-migration class of bug is structurally+impossible).++## Why this changed (history)++Images v15–v23 were FAT: the whole toolchain was baked at image-build time,+which made first runs fast but froze whatever package versions (and package+NAMES) existed on bake day into every container for months — the root cause of+the retired-slug pinning bug (adom/[email protected] sitting next to+adom/hydrogen-bootstrap on live machines). If you are inside an older fat-image+workspace, everything below still applies to you; the content is the same,+only WHO installed it differs.++## How updates work (same in both eras)++- The registry updater keeps packages current: `adom/hook` runs+ `adom-wiki pkg update` on agent activity, and ah's setup gates on+ hydrogen-bootstrap matching the registry's latest.+- The IMAGE only changes when ah ships a new pinned tarball+ (`/etc/adom-golden-version` says which rootfs you're on;+ `/etc/adom-tarball-version` says what ah imported). An image bump requires a+ workspace re-import (Virgin Reset) — package updates never do.+- Force a full converge by hand:+ `adom-wiki pkg install adom/hydrogen-windows-bootstrap` (idempotent).++## "Command not found" / missing tools++The image intentionally ships no long-tail tooling. Get tools on demand:+`apt-get install <tool>` (via sudo where available), `apt python3-<lib>` for+Python libraries (PEP-668: prefer apt; `pip --break-system-packages` or a venv+otherwise), and `adom-wiki discover search -q <capability>` for Adom packages.+The Claude Code CLI is installed by the `install-claude-cli` setup step and+self-updates thereafter.++## Answering common user questions++- "Where did all these skills come from?" → the setup cascade's first package+ install (fat-era workspaces: baked into the image). They live flat in+ `~/.claude/skills/` and update via the registry.+- "Why did first-run setup take a few minutes?" → the live toolchain install;+ it is downloading the current versions of everything rather than using+ months-old baked copies.+- "How do I update the workspace?" → you almost never need to; the registry+ updater converges packages continuously. A tarball-version bump (rare, OS+ level) comes through ah and needs a re-import.
skills/hydrogen-notifications/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/hydrogen-notifications/SKILL.mdadded+125@@ -0,0 +1,125 @@+---+name: hydrogen-notifications+description: >+ How Hydrogen reaches the user OUTSIDE the window — native OS toast+ notifications + an OS-level attention request — so the AI can proactively tell the user a+ long job finished or that it needs them (great for hands-free / away-from-keyboard+ work). Covers Hydrogen's notify handler (levels info|warning|error|emergency, optional+ <actions> buttons; emergency = a persistent attention request until the user looks)+ and how to fire one with the `notify` desktop command / `adom-bridge-cli notify_user`.+ Also the in-app `/ui/toast` (a message INSIDE the Hydrogen window) and Pup's+ browser_alert_window attention nudge. The native OS-toast backend is+ platform-specific (see the platform companions [[hydrogen-notifications-windows]] /+ [[hydrogen-notifications-mac]]); cross-platform behavior is documented here. Trigger words — notify the user, send a+ notification, desktop notification, toast, os toast, native toast, taskbar flash, dock+ bounce, get the user's attention, alert the user, notify_user, emergency notification,+ urgent alert, notification actions, action buttons, ping the user, job done+ notification, tell me when it's done, request attention, browser_alert_window, in-app+ toast, ui toast, /ui/toast, show a toast in Hydrogen, toast inside the window.+---++# Hydrogen — Notifications (reaching the user outside the window)++When the user isn't looking at Hydrogen — a long build is running, you've finished a job, or+you're **blocked and need them** — you can reach them on the OS level: a native **toast**,+and for urgent cases a **persistent attention request** (the exact form is+platform-specific). This is what makes hands-free / away-from-keyboard work usable:+don't just print a message into a panel nobody's watching — fire a notification.++> **Platform note:** the native OS toast is delivered by a per-platform backend, and how+> richly the OS surfaces it varies by platform. The `notify_user` API itself is the same+> everywhere. The per-platform delivery mechanics live in the platform companions:+> [[hydrogen-notifications-windows]] and [[hydrogen-notifications-mac]].++## Firing a notification++Source: `src-tauri/crates/hydrogen-app/src/notifications.rs` (`handle_notify` → `show_toast`).+Dispatched via the `notify` message; from the workspace use the ab CLI:++```bash+adom-bridge-cli notify_user '{"title":"Build complete","body":"hd_build_rust finished — exit 0","level":"info"}'+```++Payload shape (`NotifyPayload`):++```json+{+ "title": "...",+ "body": "...",+ "level": "info | warning | error | emergency", // default: info+ "actions": ["Open log", "Dismiss"] // optional buttons+}+```++| Field | Notes |+|---|---|+| `title` | Toast heading. (On some platforms Hydrogen prefixes the app identity — see the platform companion.) |+| `body` | The message. |+| `level` | `info` / `warning` / `error` / `emergency`. `warning`/`error`/`emergency` render as **long-duration** toasts; `info` is short. |+| `actions` | Optional array of button labels rendered as buttons in the toast. Empty/omitted = no buttons. |++### Levels — and the `emergency` attention request++- `info` / `warning` / `error` — a standard toast (warnings and errors stay up longer).+- **`emergency`** — in addition to the toast, Hydrogen calls **`request_user_attention(Critical)`**+ on the main window and shows/unminimizes it. This produces a **persistent attention+ request** (the OS keeps signaling the app) that **keeps going until+ the user actually interacts with the window themselves** — it is *not* cleared by+ a programmatic focus. Reserve `emergency` for "I genuinely cannot proceed without you" —+ it's intentionally hard to ignore.++> Platform-specific toast identity/title behavior and delivery details live in the+> platform companions [[hydrogen-notifications-windows]] / [[hydrogen-notifications-mac]].++## In-app toast — `POST /ui/toast` (inside the Hydrogen window)++The notifications above reach the user **outside** the window (OS toast / taskbar). For a+lightweight message **inside** the Hydrogen window — the same toast UI the app uses for its own+success/error messages — fire a control-API toast:++```bash+# via the control API (or adom-cli hydrogen api, or the API Explorer "Try it")+POST /ui/toast { "message": "Build finished ✅", "type": "success", "duration": 5000 }+```++| Field | Notes |+|---|---|+| `message` | Required. The toast text. |+| `type` | `info` (default) / `success` / `warning` / `error` — sets icon + color. |+| `duration` | ms before auto-dismiss (default 5000). **0 = persistent** until the user dismisses. |+| `groupId` + `detail` | Optional: coalesce related toasts into one expandable group. |++It emits the `hydrogen-toast` Tauri event, which the main window renders via the global+`toastStore` (bottom-stacked toasts). **OS toast vs in-app toast:** use a `notify` OS toast+when the user may be **away from / not looking at** Hydrogen; use `/ui/toast` for an in-window+status cue while they're working in Hydrogen. (Hydrogen's native recording uses this for its auto-stop+notice — see [hydrogen-recording](../hydrogen-recording/SKILL.md).)++## Pup's attention nudge (`browser_alert_window`)++For AI-driven browser work, Pup has its own attention nudge:++```bash+adom-bridge-cli browser_alert_window '{"sessionId":"default"}'+```++It **triggers the OS attention nudge** for that browser window (and brings the page+to front within Chrome) **without stealing foreground focus** — a gentle "look here." (Use+`browser_focus_window` if you actually want to raise the window.)++## When you (the AI) should notify proactively++- **Long-job completion** — a build, a batch, a render, a deploy finished. Fire an `info`+ toast so the user can wander off and get pulled back.+- **You need the user** — an approval, a decision, a credential. Use `warning`, or+ `emergency` if you're fully blocked.+- **Hands-free / driving** — pair a notification with audio (TTS) so the user gets both a+ glance-able toast and the spoken answer.++Don't spam: one notification per meaningful event. Verify the bridge first+(`adom-bridge-cli ping`) before claiming you notified them.++## Related skills+- [hydrogen-adom-desktop](../hydrogen-adom-desktop/SKILL.md) — ab is what carries `notify_user` to the host; `ping`/`status` to verify the bridge and the `notify` capability+- [hydrogen-bridges](../hydrogen-bridges/SKILL.md) — the full ab capability list (`notify`, …) and Pup's `browser_alert_window`+- [[hydrogen-notifications-windows]] / [[hydrogen-notifications-mac]] — the per-platform toast backends and attention mechanics
skills/hydrogen-open-url/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/hydrogen-open-url/SKILL.mdadded+199@@ -0,0 +1,199 @@+---+name: hydrogen-open-url+description: >+ The decision guide for "open a URL / open a website" inside Hydrogen.+ When the user says "open ti.com" / "pull up the datasheet page" / "show me+ that site", Hydrogen has SIX distinct ways to do it — a Hydrogen webview tab, an Hydrogen+ 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 hydrogen-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.+---++# Hydrogen — Opening a URL (your options)++When a user says **"open the ti.com website"** (or any URL), Hydrogen 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* Hydrogen, next to their work, and to keep it around | a panel/tab in the Hydrogen workspace | partly (reload/route) |+| 2 | **Hydrogen Browser Window** | a quick popout window owned by Hydrogen, not a full browser | a standalone Hydrogen-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`)++**`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 Hydrogen workspace. The control API base URL is the live+value in `~/.adom/hydrogen-control-url` — `http://127.0.0.1:<dynamic>` (the workspace+shares loopback with the host; the port is dynamic per launch).+Read it with `BASE="$(cat ~/.adom/hydrogen-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 Hydrogen — the consistent surface.++**2 / 4 / 5. Through the Browser Picker** — the single unifying entry point. POST+the URL and Hydrogen routes it (auto-picking a sensible default after a 5-second+countdown, or showing the chooser):+```bash+BASE="$(cat ~/.adom/hydrogen-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"+```+> ⚠️ **`/open-url` often replies `"Hydrogen 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 `hydrogen-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`). A freshly-launched window **auto-foregrounds**;+a tab does not. So for **auth flows the user must complete** (Claude/Adom sign-in),+prefer `fresh:true` — the consent page comes to the front by itself, no manual focus.+The Browser Picker defaults the "Open in a 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 `hydrogen-browser-picker`.++**3. Pup** — open and then drive it with `adom-bridge-cli 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-bridge-cli 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/hydrogen-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 (Hydrogen prefs API)++The picker's per-domain memory is a first-class API (Hydrogen 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/hydrogen-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 `hydrogen-url-routing-prefs`+ localStorage + dispatches `hydrogen-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+- [hydrogen-browser-picker](../hydrogen-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)+- [hydrogen-runtime-mode](../hydrogen-runtime-mode/SKILL.md) — none of this changes across workspace runtimes
skills/hydrogen-permissions/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/hydrogen-permissions/SKILL.mdadded+69@@ -0,0 +1,69 @@+---+name: hydrogen-permissions+description: >+ Hydrogen AUTO-GRANTS every webview permission — microphone, camera,+ clipboard-read, geolocation, notifications, sensors — so inside Hydrogen a webview app NEVER+ shows a "this site wants to use your microphone/camera/clipboard/location" prompt. READ+ THIS before building or debugging a webview app that uses getUserMedia / clipboard /+ geolocation: do NOT tell the user to click "Allow" — there is no prompt to click; it's+ suppressed by design. Distinguish this browser-permission auto-grant from the SEPARATE+ in-app screen-share / mic consent the AI itself requests (that one IS user-approved — see+ hydrogen-capture-share). Hydrogen's webview engine is platform-specific; the per-engine+ implementation detail lives in the platform companion skills (see+ [[hydrogen-permissions-windows]] / [[hydrogen-permissions-mac]]). Trigger words — permission, permissions, webview permission,+ allow microphone, allow camera, allow clipboard, clipboard read, getUserMedia, mic+ permission, camera permission, geolocation permission, notifications permission, sensors+ permission, site wants to use, permission prompt, click allow, no permission prompt,+ auto-grant, auto granted, why no prompt, permission denied webview.+---++# Hydrogen auto-grants every webview permission++Hydrogen's webview registers a permission handler that returns **ALLOW for every+permission kind** — microphone, camera, clipboard-read, geolocation, notifications,+sensors, etc. So **inside Hydrogen the user never sees a "this site wants to use your microphone /+camera / clipboard / location" dialog.** Webview apps that call `getUserMedia`, read the+clipboard, or ask for geolocation simply *work*, with no prompt.++Every request is unconditionally allowed. In the Hydrogen log you'll see lines like `[perm]+auto-granted permission kind …` and, at startup, `[perm] Registered PermissionRequested+auto-grant handler`.++> **Platform note:** Hydrogen uses a different webview engine per platform. The auto-grant+> policy is the same everywhere — every permission is allowed up front — but the exact+> engine API is platform-specific. The per-platform implementations and their code+> evidence are in [[hydrogen-permissions-windows]] and [[hydrogen-permissions-mac]].++## Why Hydrogen does this++Hydrogen is the user's **trusted desktop app** — they've already chosen to run it. Intermediate+per-site Web permission prompts (especially the clipboard-paste dialog) are pure friction+with no added safety here, since there's no untrusted third-party browsing context: the+surfaces are Hydrogen's own panels and the user's own webview apps. So Hydrogen grants them all up+front.++## What this means for you when building webview apps++- **Don't instruct the user to "click Allow."** There is no prompt — telling them to look+ for one is wrong and confusing. `getUserMedia({ audio: true })`, clipboard reads,+ geolocation, notifications all resolve without a dialog.+- If a webview app *isn't* getting mic/camera data, the cause is **not** a denied+ permission prompt (Hydrogen never denies). Look elsewhere: no device present, the page served+ over a non-secure context, a code bug, or the OS-level privacy toggle for the camera/mic+ being off for the app entirely.+- This is behavior baked into Hydrogen's webview host — it's not something the web page or the+ user configures.++## NOT the same as in-app screen-share / mic consent++This auto-grant is **only** the low-level webview browser-permission layer. It is separate+from the **screen-share / microphone consent the AI itself requests** when it wants to+capture the user's screen or mic for a session — that flow **is** explicitly user-approved+(the user sees and accepts an in-app consent UI). Don't conflate them: webview site+permissions are silently allowed; the AI's capture/share request is a real, user-facing+approval. See `hydrogen-capture-share`.++## Related skills+- `hydrogen-capture-share` — the SEPARATE, user-approved screen-share / mic consent the AI requests+- [hydrogen-open-url](../hydrogen-open-url/SKILL.md) — opening URLs/webview apps inside Hydrogen (where these permissions apply)+- [[hydrogen-permissions-windows]] / [[hydrogen-permissions-mac]] — the per-platform webview implementations + code evidence
skills/hydrogen-ports/SKILL.md+42−6@@ -1,15 +1,12 @@ --- name: hydrogen-ports-description: How Hydrogen does port forwarding between your WSL2 workspace and your Windows machine — WSL2's automatic `0.0.0.0` localhost-forwarding (no Docker `-p` map), the code-server `/proxy/<port>/` URL pattern that exposes any internal port, the port-forward registry for `127.0.0.1`-only services that WSL2 won't auto-forward, and the PortMappingsDialog UI. Use when the user asks "why isn't my server reachable", "how do I expose port X", "what's localhost:7380", "register a port", "auto-forward a port", or "the dynamic port dialog". Trigger words — port forwarding, ports, hd ports, container port, proxy port, code-server proxy, /proxy/, port mappings dialog, port hints, auto forward, expose port, register port, host port, dynamic port, ports.json, PortConfig, port resolver, localhost port not working.+description: How Hydrogen does port forwarding between your WSL2 workspace and your Windows machine — WSL2's automatic `0.0.0.0` localhost-forwarding (no port map to declare), the code-server `/proxy/<port>/` URL pattern that exposes any internal port, the port-forward registry for `127.0.0.1`-only services that WSL2 won't auto-forward, and the PortMappingsDialog UI. Use when the user asks "why isn't my server reachable", "how do I expose port X", "what's localhost:7380", "register a port", "auto-forward a port", or "the dynamic port dialog". Trigger words — port forwarding, ports, ah ports, container port, proxy port, code-server proxy, /proxy/, port mappings dialog, port hints, auto forward, expose port, register port, host port, dynamic port, ports.json, PortConfig, port resolver, localhost port not working. --- # Hydrogen Ports — forwarding workspace services to your Windows host -> This is the WSL2-runtime version (default). For the legacy Docker container-> runtime (`HD_RUNTIME=docker`) see the docker/ bucket.--The Hydrogen workspace is a WSL2 distro (`Adom-Workspace`), not a Docker container.-There is **no Docker `-p` port mapping** — WSL2 auto-forwards listeners for you.+The Hydrogen workspace is the WSL2 distro `Adom-Workspace`.+There is **no port mapping to declare** — WSL2 auto-forwards listeners for you. Any service that binds `0.0.0.0:<port>` inside the distro is reachable at `localhost:<sameport>` on Windows automatically. code-server itself binds `0.0.0.0:7380`, so `localhost:7380` on Windows just works. For browser-facing@@ -155,6 +152,45 @@ control API (default `127.0.0.1:47084`) — it returns `PortConfig` (`hydrogen-app/src/lib.rs` `get_ports`). `ports.json` is the single source of truth every consumer reads. +## Discovering Hydrogen itself: the three-rung ladder++Because `control`/`proxy`/`cdp` can rebind to ephemeral ports, anything that+needs to FIND a running Hydrogen walks this ladder (static defaults are only+rung zero, never an assumption):++1. **Local files (authoritative when present).**+ - Windows side: `%APPDATA%\hydrogen-desktop\ports.json` (written every+ launch by `resolve_available()`).+ - Inside the distro: `~/.adom/hydrogen-control-url` (full live control URL)+ and `~/.adom/hydrogen-ports.json` (all four ports), both stamped by+ Hydrogen into the workspace. Adom Bridge's own direct API is at+ `~/.adom/direct-api-port` (stable 24680 since ab 2.0.27, legacy 47200) and the relay at+ `~/.adom/cli-relay-ports.json` (8765 WS / 8766 HTTP).+2. **Verify liveness AND identity.** A port answering `/health` is not proof+ it is THIS install: a zombie from an uninstalled copy can still be serving+ (a real observed failure). `GET /buildinfo` returns `instance_id`, `pid`,+ and `install_path`; check they match what you expect before trusting a+ discovered port.+3. **Cloud KV (works even when local files are unreachable).** On every+ launch, and every 4 hours after, Hydrogen publishes its live runtime+ identity to the Adom cloud KV (Carbon):++ - Key: `hydrogen.runtime.<machine>` (machine = lowercase+ `COMPUTERNAME`, sanitized to `[a-z0-9_]`).+ - Value: a JSON string:+ `{host, control, proxy, cdp, code_server, instance_id, pid, install_path, version, ts}`.+ - Read it: `GET https://carbon.adom.inc/user` (session token or API key+ auth) then `body.hydrogen_preferences.settings["hydrogen.runtime.<machine>"]`.+ - Freshness: compare `ts` (unix seconds) to now; the heartbeat refreshes it+ every 4 hours, so a `ts` older than ~5 hours means that instance is+ probably gone. Always confirm by hitting `/buildinfo` on the advertised+ port and matching `instance_id`.++ Adom Bridge publishes its container connection the same way+ (`bridge.container.<id>`, legacy `desktop.container.<id>` until ab's rename ships). This is what lets a cloud container, another+ machine, or a fresh install find the user's running desktop apps without+ port scanning.+ ## Inside vs outside — which URL to use | You're calling from | To reach a service on port N | Use |
skills/hydrogen-recording/SKILL.mdadded+408@@ -0,0 +1,408 @@+---+name: hydrogen-recording+description: >+ How to record screen + window video from Hydrogen. In Hydrogen the recorder is+ NATIVE: Windows.Graphics.Capture (per-window, "record kicad") + DXGI full-screen,+ hardware H.264 (default, universally playable) or H.265 (smaller, not web-playable)+ → mp4 in ~/project/recordings/, with NO picker, NO "you're sharing" banner, real+ 30-60fps, and NO display wake-lock. Driven by `adom-cli hydrogen recording+ start/stop` (which routes to the native control API) or the control endpoints+ directly (POST /recording/native/start|stop, GET /recording/native/status|sources).+ A server-side max-duration cap (default 600s) auto-stops + toasts. Read for codec+ choice, source selection, the cap+reason, and the "● Recording" indicator. Also+ covers AUDIO-ONLY / narration capture (`adom-cli hydrogen audio` → WebM/Opus in+ ~/project/audio/), ab host desktop recording (desktop_record_start, Windows-only),+ the tab-vs-desktop footgun, and what `--mic` does. TWO ENGINES: wgc (GPU, real+ displays) and gdigrab (ffmpeg GDI — the ONLY engine that records on VMs / virtual+ / indirect displays / VNC); engine:"auto" probes and falls forward; GET+ /recording/native/capabilities reports what THIS box supports. Multi-monitor:+ screen:1..N or screen:all. Trigger words — record,+ recording, screen recording, record my screen, record kicad, record a window,+ record whole screen, record the workspace, record a demo, record with mic, record+ no audio, mic on, mic off, voiceover, record audio, audio-only, record a narration,+ record my voice, narrate, narration track, record a voice track, h264, h265, hevc,+ mp4, webm, opus, codec, recordings folder, audio folder, ffmpeg, stitch clips,+ max duration, recording indicator, desktop_record_start, browser_record_start,+ start recording, stop and save, record on a vm, virtual display, indirect display,+ headless recording, vnc recording, 0 frames, zero frames, gdigrab, gdi capture,+ recording engine, capture engine, record all monitors, record second monitor,+ multi-monitor recording, recording capabilities, can this machine record.+---++# Hydrogen — Screen & Window Recording++In Hydrogen the primary recorder is **native** (Windows.Graphics.Capture + DXGI) — not+`getDisplayMedia`. It produces a real high-fps mp4, asks for no permission, shows no+"you're sharing" banner, and holds **no display wake-lock** (see+[hydrogen-screen-lock](../hydrogen-screen-lock/SKILL.md)). Pick the mechanism by *what* you're+recording — this is the single most important decision and a known footgun.++**DEFAULT — drive it with `adom-cli hydrogen recording start` / `stop`.** That IS the+Hydrogen native recorder (h264 mp4 → `~/project/recordings/`). Do **NOT** reach for+`adom-bridge-cli desktop_record_start` for an ordinary "record my screen / window / the+workspace" — that's the ab **host** fallback: it writes a `.webm` into ab's+bridges-cache (`…/Adom Bridge/bridges-cache/…`), **not** your `~/project/recordings/`,+and is only for when Hydrogen's native recorder is genuinely busy. "record my screen",+"record the workspace", "record a demo", "record kicad" all → `adom-cli hydrogen+recording`. (Same trap as screenshots: don't fall through to `adom-bridge-cli` for+anything inside Hydrogen.)++## Which recorder? — the priority ladder (read first)++Same shape as [hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md): **lead with `adom-cli`+(consistent with web Hydrogen), drop to the richer Hydrogen/ab surfaces only for what it can't+do.** Default to Hydrogen-native.++1. **Hydrogen content OR the whole screen (the 99% case): `adom-cli hydrogen recording start+ --share tab|screen` — THE DEFAULT.** The *same* command on web-Hydrogen and Hydrogen (web-H+ records via `getDisplayMedia`; Hydrogen records **natively** via WGC/DXGI — no picker, no "you're+ sharing" banner, no display wake-lock). `--share tab` = the Hydrogen workspace; `--share screen`+ = the entire display (KiCad/Fusion/desktop apps). Saves **h264 mp4 → `~/project/recordings/`**.+ Verified live 2026-06-14.+2. **A SPECIFIC WINDOW** (record just KiCad / Fusion, not the whole screen): **Hydrogen native+ control API `POST /recording/native/start {source:"window:<title-substring>"}`.** adom-cli's+ `--share` is **tab|screen only — it has no per-window flag**, so per-window recording is an+ **Hydrogen-native exclusive** (neither adom-cli nor ab can scope a recording to one window).+ Resolve the title via `GET /recording/native/sources`. Same endpoint does `{source:"screen"}`,+ `{source:"screen:2"}`, codec, custom cap.+3. **The whole DESKTOP when Hydrogen's native recorder is already busy:** `adom-bridge-cli+ desktop_record_start {confirmDesktopNotTabRecording:true}`. **Whole-desktop ONLY (no+ per-window), via Chrome `getDisplayMedia` → it DOES hold the display wake-lock** (unlike Hydrogen+ native) and writes a **`.webm` into ab's cache** (not `~/project/recordings/`). ab's own help+ says "prefer Hydrogen's `recording start --share screen`" — this is for the+ parallel-with-an-Hydrogen-capture case, not the default.+4. **A single Chrome tab** (AI-driven Pup browser — a clean per-tab clip): `adom-bridge-cli+ browser_record_start {sessionId}` / `browser_record_stop`. CDP screencast, ~50fps, no HUD,+ no wake-lock, coexists with everything.+5. **Just audio — a voice / narration track, no video** ("record a narration", "record my+ voice"): **`adom-cli hydrogen audio start/stop`** → `~/project/audio/<name>.webm` (Opus).+ See "Audio-only" below.++### What each recorder can actually scope (don't assume symmetry with screenshots)+Unlike screenshots — where ab grabs a window *or* the screen — **recording is NOT uniform**:++| Recorder | Window | Full desktop | Browser tab | Backend | Wake-lock | Output |+|---|---|---|---|---|---|---|+| `adom-cli hydrogen recording` (ah) | — (use control API) | ✅ `--share screen` | ✅ `--share tab` | native WGC/DXGI | **NO** | mp4 → `~/project/recordings/` |+| Hydrogen native control API | ✅ `source:"window:X"` | ✅ `source:"screen"` | — | native WGC/DXGI | **NO** | mp4 → `~/project/recordings/` |+| ab `desktop_record_start` | ❌ **none** | ✅ whole desktop | ❌ | Chrome `getDisplayMedia` | **YES** | `.webm` → ab cache |+| pup `browser_record_start` | — | ❌ | ✅ one tab | CDP screencast | **NO** | tab clip |++So: **per-window recording = Hydrogen-native only; whole-desktop = Hydrogen-native (no wake-lock) preferred+over ab (wake-lock); single tab = pup.** When someone says "record a window," that's #2 (Hydrogen+control API) — ab cannot do it.++---++## ⭐ Engines: wgc vs gdigrab — and why VMs are different++Hydrogen's native recorder has **two engines**, because the machine you're on decides what can+physically capture frames:++| | `wgc` (default on real displays) | `gdigrab` |+|---|---|---|+| Backend | Windows.Graphics.Capture + DXGI, Media Foundation MFT encoder | ffmpeg GDI capture (legacy, compositor-independent) |+| GPU accelerated | yes (~60fps) | no (~15-30fps, CPU) |+| Real display (laptop/desktop) | ✅ best choice | works, lower fps |+| **VM / virtual / indirect display / VNC / headless** | ❌ **captures 0 FRAMES, silently** | ✅ **the only engine that works** |+| Sources | screen, screen:N, window:X, self | screen, screen:N, **screen:all**, window:X |+| Containers | mp4 | mp4, mkv |+| Needs | nothing | ffmpeg (auto-fetched ~40MB on first use, cached) |++**The trap this exists for (ADOMGPU, 2026-07-19):** on a box whose desktop sits on an+indirect display driver (IddCx "Virtual Display Driver", Hyper-V Video, Parsec/Sunshine,+VNC-only), WGC *and* DXGI Desktop Duplication return success but deliver **zero frames**+— and `GraphicsCaptureSession.IsSupported()` still says True, so it can't warn you.+Legacy GDI keeps working there. Hydrogen now handles this for you:++- **`engine:"auto"` (the default)** probes the display adapters; on a virtual-display box+ it goes straight to gdigrab, otherwise wgc — and if wgc still yields 0 frames in ~2s it+ **falls forward to gdigrab automatically** (`fellForward:true` in the response).+- **Fail-fast:** `/start` verifies frames are actually flowing before returning success.+ A 0-frame take errors AT START with the fix in the message — never again a 90s take+ that turns out empty at stop.+- **`GET /recording/native/capabilities`** = the ground truth for THIS box: engines ++ ffmpeg readiness, every adapter (with `virtual:true` flags), every monitor, codecs,+ containers, and `recommended.engine` with the reason. **On any unfamiliar machine (a+ VM, a cloud box, anything over VNC), read this before recording.**++### Multi-monitor++`GET /recording/native/sources` (and `/capabilities`) list every monitor with its index,+geometry and `primary` flag. You can:+- record **one** monitor: `source:"screen:1"`, `"screen:2"`, … (each row's `select` value),+- record **all** monitors at once as a single spanning video: `source:"screen:all"`+ (gdigrab engine — WGC cannot span monitors),+- or just `source:"screen"` for the primary.++```bash+BASE="$(cat ~/.adom/hydrogen-control-url)"+# What can this box record? (do this first on a VM)+curl -s "$BASE/recording/native/capabilities"+# Record monitor 2 explicitly via gdigrab (e.g. on a VM):+curl -s -X POST -H 'Content-Type: application/json' \+ -d '{"source":"screen:2","engine":"gdigrab"}' "$BASE/recording/native/start"+# Record ALL monitors as one spanning video:+curl -s -X POST -H 'Content-Type: application/json' \+ -d '{"source":"screen:all"}' "$BASE/recording/native/start"+```++`status` and `stop` now report `engine`, `frames`, and **measured `fps`** — check them:+frames climbing + fps > 0 is a live take; the start already fail-fasts a dead one.+Note: `mic` audio is wgc-only (gdigrab records video-only for now).++---++## (a) Hydrogen native recording (WGC + DXGI) — the default++**Mechanism:** Rust `crates/hydrogen-control/src/recorder.rs` — Windows.Graphics.Capture+per-window + DXGI Desktop Duplication full-screen → Media Foundation hardware+encoder → mp4 in `~/project/recordings/` (born `adom`-owned). Real 30–60fps at full+source resolution. **No picker, no banner, no display wake-lock.**++### Drive it via adom-cli (the usual way)++```bash+adom-cli hydrogen recording start --share screen --mic false --countdown 0 --reason "demo"+# ... do the thing (wait / drive the app you're recording) ...+adom-cli hydrogen recording stop # → filePath: recordings/recording-<ts>.mp4 (relative to ~/project)+```++**All four `start` flags are REQUIRED** (`--share`, `--mic`, `--countdown`, `--reason`)+— the CLI rejects the call otherwise (it forces you to think through scope, audio,+pacing, intent). `--share screen` records the whole (primary) screen with no approval+dialog (native capture); re-running `start` with the same `--share`/`--mic` skips+straight to countdown. In Hydrogen the result JSON carries fields web-Hydrogen doesn't:+`native:true`, `codec` (h264), `maxSeconds`, `reason`, `source` (e.g. `monitor:primary`),+and `pendingFile` / `filePath` (relative `recordings/<name>.mp4`).++⚠️ **The `stop` `_hint` text currently LIES about the format** — it says *"WebM format+(VP9 + Opus)"*, but Hydrogen native recordings are **h264 mp4** (the file is `.mp4`, and the+`codec` field says `h264`). Trust the `.mp4` `filePath` + `codec` field, not the hint.+(Known adom-cli bug, on Colby's list.)++### Drive it via the control API (full control — window by name, codec, cap)++The AI can call the control endpoints directly for capabilities adom-cli flags don't+yet expose (a specific window, codec, a custom cap):++```bash+# Discover sources first (resolve a fuzzy name like "kicad")+GET /recording/native/sources # → { windows:[...titles...], monitors:[{index,width,height}] }++# Start — source is the key arg:+POST /recording/native/start { "source": "window:KiCad" } # a window by title substring+POST /recording/native/start { "source": "screen" } # primary monitor ("record whole screen")+POST /recording/native/start { "source": "screen:2" } # monitor by 1-based index++POST /recording/native/stop # → { file: "recordings/<name>.mp4", fileAbsolute, seconds, source }+GET /recording/native/status # → { recording, source, seconds, maxSeconds, reason, remainingSeconds }+```++So **"record kicad"** = `POST /recording/native/start {source:"window:kicad"}` (after+confirming the window title via `/sources`); **"record whole screen"** =+`{source:"screen"}`. No picker, no asking the user which window.++### Codec — H.264 (default) vs H.265++`POST /recording/native/start {"codec": "h264" | "h265"}` (default **h264**):++- **h264** — universally playable: browsers (Chrome/Edge `<video>`), the wiki, every+ player. **Use this for anything shared, embedded, or streamed.** (Default.)+- **h265 / hevc** — ~40% smaller at equal quality, BUT not playable in Chrome/Edge+ `<video>` (needs hardware/codec support). **Only for archival / local use** where+ you control the player.++When unsure, keep h264. The start response repeats this tradeoff in `_hints.codec`.+(adom-cli's `recording start` has no `--codec` flag yet — it always sends h264; use+the control API to choose h265. A `--codec` flag is on the adom-cli wishlist.)++### Audio (microphone)++`--mic true` (adom-cli) / `{"mic": true}` (control API) mixes the **default input+device** (the user's mic, via cpal/WASAPI) into the mp4's audio track (48 kHz AAC) —+for narration/voiceover. WGC is video-only, so Hydrogen opens the mic itself and feeds PCM+to the encoder. If no mic is available the recording proceeds **video-only** (graceful).+`--mic false` omits the voice. Notes:+- **`--mic false` is NOT "no audio track."** Either way the native mp4 carries an aac+ stream; with mic off it's a harmless EMPTY stream (0 frames, no audio, plays fine) —+ a windows-capture limitation, intentionally not stripped to avoid an ffmpeg+ dependency on clean VMs. There is **no recorder flag for a fully audio-free file**; if+ a user truly needs zero audio, strip it post-hoc (`ffmpeg -i in.mp4 -c copy -an out.mp4`).+- **~1s startup gap:** the audio stream begins once the encoder is created on the first+ video frame, so the mic track can be ~1s shorter than the video and slightly lead.+ Fine for casual narration; flag if you need frame-accurate sync.+- **adom-cli `_hint` bugs (Colby) — don't be misled:** the `start` hint always says+ *"No mic in this recording — pass --mic true"* even when you DID pass `--mic true`;+ and the `stop` hint says *"WebM (VP9 + Opus)"* though the file is h264 mp4. Trust the+ `.mp4` `filePath` + the `codec` field, not the hints.++### Max-duration cap + "● Recording" indicator (safety nets)++- A **`maxSeconds`** cap (default **600s / 10 min**) is enforced server-side: a+ watchdog auto-stops + finalizes the mp4 and fires a UI toast. To override, pass+ `maxSeconds` AND a **required `reason`** (e.g. `{"maxSeconds":1800,"reason":"30-min+ walkthrough the user asked for"}`) — the reason is shown in the toast when the cap+ hits. Overriding without a reason is rejected (400).+- While recording, Hydrogen shows a persistent **"● Recording · <source>"** badge so it's+ never silently on.+- Hydrogen does **not** stop on blur/minimize — recording while Hydrogen is backgrounded (to+ capture KiCad/Fusion) is the normal case. See [hydrogen-screen-lock](../hydrogen-screen-lock/SKILL.md).++### Taking screenshots WHILE recording (multi-AI-thread capture contention)++Adom users run several AI threads against one Hydrogen at once, so a recording started by one+thread and a screenshot requested by another routinely overlap. **Almost every capture+method coexists with a live recording — only the `adom-cli` SSE screenshot path does not.**++- ✅ **Coexist with a recording** (use these while recording): CDP `POST /screenshot`+ (Hydrogen-native, the default), `adom-bridge-cli desktop_screenshot_window` (ab per-window),+ `adom-bridge-cli desktop_screenshot_screen` (ab host GDI), pup `browser_screenshot`.+- ⚠️ **The ONE exception** — `adom-cli hydrogen screenshot panel|workspace|screen` (the SSE+ path, `PATCH /current/screenshot`). The moment a native recording is active this returns+ **`{ok:false, error:"capture_busy", retry_after_ms, alternatives:[...], _hint}`** instead of+ silently failing. **Don't blind-retry it — read `alternatives` and switch to a coexisting+ method** (CDP `POST /screenshot` is the drop-in).+- **Pre-flight when unsure:** `GET /capture/availability` reports `recording:{active,source}`,+ per-method reliability + `coexists_with_recording`, and a `recommended` method. Check it+ before a screenshot if you might be mid-recording.++Full screenshot routing + the coexistence matrix live in+[hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md) ("During a recording"). The takeaway for+*this* skill: **starting a recording does NOT lock out screenshots** — it only redirects the+SSE path to `capture_busy`; every other method keeps working.++## Audio-only — record a narration / voice track (`adom-cli hydrogen audio`)++When the user wants **just their voice, no video** — "record a narration track for+the video", "record my voice", "let me record some audio to narrate this" — use the+dedicated **`audio`** verb, NOT the video recorder:++```bash+adom-cli hydrogen audio enable # activate mic (silent if cached)+adom-cli hydrogen audio start --countdown 0 --reason "narration for the demo video"+# ... user speaks ...+adom-cli hydrogen audio stop # → filePath: audio/<name>.webm (relative to ~/project)+```++- Output is a **WebM / Opus** audio file in **`~/project/audio/`** (the `audio` verb's+ WebM hint is **correct**, unlike the video recorder's). Reason is required.+- Other subcommands: `audio status` (mic active? device label), `audio level`+ (rms/peak/clipping/gain — warn if clipping), `audio gain <0.0-4.0>` (persists),+ `audio disable`.+- Audio-only files land in `audio/`; screen videos land in `recordings/`.++### Stitching / post-processing (ffmpeg) — install it on demand++A common follow-on is "stitch these clips together" or "lay this narration over the+video." **ffmpeg/ffprobe are NOT pre-installed** — the Hydrogen workspace image is+intentionally **lightweight**, and the AI adds tools as the user needs them. That's+not a blocker: when you need to concatenate clips, mux a narration `.webm` onto an+`.mp4`, transcode, or probe a file, **just install ffmpeg and proceed** (passwordless+sudo + apt are available; ffmpeg ~6.1 is in the repos):++```bash+sudo apt-get update -qq && sudo apt-get install -y ffmpeg # pulls ffprobe too+```++Don't tell the user "ffmpeg isn't available here" — install it (a few seconds) and do+the job. Same pattern for any other missing tool (imagemagick, pandoc, …): install on+demand rather than declaring it impossible.++---++## (b) Hydrogen's in-app combined recorder (web-Hydrogen / legacy getDisplayMedia)++> **In Hydrogen this path is superseded by (a) native recording.** The+> getDisplayMedia-based combined recorder below is the **web-Hydrogen** mechanism+> (and the mic/level-meter detail still applies there). In Hydrogen, `adom-cli hydrogen+> recording` and the Recording dropdown route to the native recorder in (a).++Source: `src/lib/components/editor/EditorNav.svelte` (~L2418–2519).+Stores: `src/lib/stores/recordingStore.ts` + `src/lib/stores/audioCaptureStore.ts`.++The **Recording dropdown** in the editor toolbar records the workspace (and/or mic)+into one file.++**Sources** — you choose **Screen** and/or **Mic** (a recording needs at least one+enabled; the **Start recording** button is disabled with "Enable screen sharing or+microphone to start recording" until one is active):+- **Screen** row — shows the current share state; click to set up sharing (this+ routes through the Screen Capture share flow — see+ [hydrogen-capture-share](../hydrogen-capture-share/SKILL.md)).+- **Microphone** row — shows the selected device label or "Not enabled."++**While recording** the dropdown shows a **live MM:SS timer** (`recordingSeconds`),+a "Recording" status dot, and tags for the active sources (Screen / Mic), with:+- **Stop & save** — finalizes the recording.+- **Discard** — throws it away (`discardCombinedRecording()`).++**Output:** a **single `.webm`** (VP9+Opus preferred, then VP8+Opus / WebM, MP4 as+last resort). `startCombinedRecording()` muxes the screen + audio streams; **Stop &+save** finalizes and **uploads to the container's `recordings/` folder.** If the+upload fails, Hydrogen **falls back to a local browser download** named+`recording-<timestamp>.webm`.++**AI-requested auto-open:** `promptRecording(message?)` sets a prompt and the+Recording dropdown can **auto-open** so the user sees what you're asking to record+(default message: "Enable screen sharing and microphone to allow AI recording").+Store state: `{ isRecording, recordingSeconds, hasVideo, hasAudio, promptMessage }`.+API-side finalizers exist too: `stopCombinedRecordingForAPI()` (base64 data URL)+and `stopCombinedRecordingAsBlob()` (raw blob) for programmatic capture.++### Mic device picker + level/gain meter++`audioCaptureStore.ts` drives the microphone:+- **Device picker:** `enumerateAudioDevices()` lists `audioinput` devices;+ `startAudioCapture(deviceId?)` selects one; the last device persists as+ `adom:lastMicDeviceId`.+- **Level + gain meter:** the store exposes `audioLevel` (0–1 RMS), `rms`, `peak`,+ and `clipping` (peak ≥ 0.99) from an analyser node, updated ~every 50 ms — this+ is the live mic meter. **Gain** is adjustable via `setAudioGain(v)` (clamped+ **0–4**, persisted as `adom:micGain`). Watch `clipping` to warn the user their+ mic is too hot.++---++## (c) ab host desktop recording (Windows-only)++Served by the embedded **Adom Bridge** process (see+[hydrogen-adom-bridge-cli](../hydrogen-adom-bridge-cli/SKILL.md)), not by the Hydrogen frontend. These+record the **real OS desktop**, not Hydrogen's webview:++```bash+adom-bridge-cli desktop_record_start '{"reason":"Demo recording","confirmDesktopNotTabRecording":true}'+adom-bridge-cli desktop_record_stop+adom-bridge-cli desktop_record_status+adom-bridge-cli desktop_recorder_open # open the recorder UI on the host+```++**Verify the capability first** — desktop recording is **Windows-only** and only+present when ab advertises it:++```bash+adom-bridge-cli status # → .capabilities must include "record"+```++If `record` isn't listed, the host/runtime doesn't support it — fall back to the+in-app recorder (for the workspace) or `browser_record_start` (for a tab).++---++## Quick reference++| Intent | Command / surface | Output |+|---|---|---|+| **Record whole screen (ah)** | `adom-cli hydrogen recording start --share screen …` or `POST /recording/native/start {source:"screen"}` | **h264 mp4** → `~/project/recordings/` |+| **Record a window (ah), e.g. "record kicad"** | `POST /recording/native/start {source:"window:KiCad"}` | **h264 mp4** → `~/project/recordings/` |+| **Smaller archival recording (ah)** | `POST /recording/native/start {source:…, codec:"h265"}` | **h265 mp4** (not web-playable) |+| Record one AI browser tab | `adom-bridge-cli browser_record_start` (pup) | tab screencast |+| Record host desktop via ab (Hydrogen busy) | `adom-bridge-cli desktop_record_start '{"confirmDesktopNotTabRecording":true}'` | host recording |+| Record the workspace (web-Hydrogen) | In-app Recording dropdown (or `promptRecording()`) | `.webm` → container `recordings/` |++## Related skills+- [hydrogen-screen-lock](../hydrogen-screen-lock/SKILL.md) — native capture holds NO display wake-lock; the max-duration cap + "● Recording" indicator+- [hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md) — native CDP screenshots (the 90% "ralph test the webview" case); the capture-contention matrix — which screenshot methods coexist with a recording and which (`adom-cli` SSE) return `capture_busy`+- [hydrogen-capture-share](../hydrogen-capture-share/SKILL.md) — the (web-Hydrogen) screen-share + AI-consent flow incl. the cancelable CountdownToast+- [hydrogen-bridges](../hydrogen-bridges/SKILL.md) — `browser_record_start` (Pup tab recording) and the ab capability list (`record`, `screenshot`, …)+- `pup` — driving the Pup browser windows you'd record per-tab
skills/hydrogen-screen-lock/SKILL.mdadded+89@@ -0,0 +1,89 @@+---+name: hydrogen-screen-lock+description: >+ Hydrogen screen capture (screenshots + recording) and the display wake-lock. A browser+ getDisplayMedia capture asserts ES_DISPLAY_REQUIRED, which blocks the user's+ screensaver and stops the display turning off → burn-in. Hydrogen is NATIVE: its+ screenshots use CDP and its recording uses Windows.Graphics.Capture (WGC) /+ DXGI — neither holds the display wake-lock — so Hydrogen does not have the web leak.+ Hydrogen still must start→capture→release per op, cap recordings, and show a "●+ recording" indicator so a forgotten recording is never silent. Read before+ adding or using Hydrogen screenshot / screen-recording features. Trigger words: ah+ screenshot, ah recording, screen capture, wake lock, screensaver blocked,+ display won't sleep, ES_DISPLAY_REQUIRED, release capture, burn-in, powercfg+ requests, getDisplayMedia, recording auto-stop, max duration.+---++# Hydrogen screen capture & the display wake-lock++A browser/`getDisplayMedia` screen capture asserts **`ES_DISPLAY_REQUIRED`** →+Windows won't run the screensaver or turn the display off while it's held →+**burn-in**. Web Hydrogen is forced to keep a capture stream alive all session+(the browser re-prompts otherwise), so it **leaks this lock constantly** — that+has cost real nights of burn-in. `powercfg /requests` shows the holder, e.g.+`DISPLAY: msedge.exe — screen capture`.++## Hydrogen is native — it does NOT have this leak++Hydrogen never uses `getDisplayMedia`:++- **Screenshots** use **CDP** (`Page.captureScreenshot` via `/capture/viewport` and+ `/shot`) — a one-shot grab, no `MediaStream`, no wake-lock. See+ [hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md).+- **Recording** uses **Windows.Graphics.Capture (WGC)** per-window + **DXGI Desktop+ Duplication** full-screen (Rust, `crates/hydrogen-control/src/recorder.rs`). **WGC/DXGI+ do NOT call `SetThreadExecutionState` / hold `ES_DISPLAY_REQUIRED`** — so a native+ recording does **not** block the screensaver or keep the display awake. See+ [hydrogen-recording](../hydrogen-recording/SKILL.md).++So the burn-in failure mode that bites web Hydrogen **cannot happen** from Hydrogen's own+capture. The remaining risk is only a *forgotten* recording wasting CPU/disk — which+Hydrogen guards with the rules below.++## Rules Hydrogen must follow (and already does)++- **Screenshots:** grab the frame → done. Never hold a capture between shots.+ (CDP capture is inherently one-shot; there is nothing to release.)+- **Recording:** has a server-side **max-duration auto-stop** (`maxSeconds`, default+ **600s / 10 min**; a watchdog finalizes the mp4 and emits `recording-auto-stopped`+ → a UI toast). Overriding the cap requires a `reason` so the toast can explain it.+- **Loud indicator:** while a native recording runs, Hydrogen shows a persistent **"●+ Recording · <source>"** badge (bottom-right) so it's never silently on.+- **No blur-stop for screen recording:** Hydrogen being backgrounded is the *normal* case+ for native screen recording (you're recording KiCad / Fusion / the desktop), so Hydrogen+ does **not** stop on blur/minimize — that would break the core use. The+ max-duration cap + indicator are the right nets here. (Blur-stop only makes sense+ for a `getDisplayMedia` *tab* capture, which Hydrogen doesn't use.)+- **Treat a held wake-lock as a bug.** Hydrogen shouldn't ever hold one; if you find one,+ diagnose with `powercfg /requests` (elevated) — the holder is listed under+ `DISPLAY:`.++## If you DO find the display pinned on++1. `powercfg /requests` (elevated) — read the `DISPLAY:` section for the holder.+2. If it's a **browser** (`msedge.exe`/`chrome.exe — screen capture`), that's a+ `getDisplayMedia` leak from web Hydrogen or a pup tab capture — stop that capture+ (close the tab / `browser_record_stop`), not an Hydrogen problem.+3. If it's **Hydrogen**, that's unexpected (native capture holds no lock) — capture the+ `powercfg` output and treat it as a bug to file against the recorder.++A screensaver can't clear another process's power request, so "release at the+source" is the fix. Hydrogen's native capture is release-by-construction.++## Separate concern: capture CONTENTION while recording (multi-thread)++Distinct from the wake-lock: when one AI thread is recording, another thread's+**screenshot** must not silently fail. Hydrogen handles this without holding any lock —+CDP `POST /screenshot`, ab `desktop_screenshot_window`/`_screen`, and pup+`browser_screenshot` all **coexist** with a live recording; only the `adom-cli` SSE+screenshot path returns **`capture_busy`** (with `alternatives`) so the caller switches+methods instead of getting an empty frame. Pre-flight with `GET /capture/availability`.+This is a *contention* guard, not a power-state one — full detail in+[hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md) ("During a recording") and+[hydrogen-recording](../hydrogen-recording/SKILL.md).++## Related skills+- [hydrogen-recording](../hydrogen-recording/SKILL.md) — native WGC/DXGI recording, the max-duration cap + reason, the "● Recording" indicator+- [hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md) — native CDP screenshots (no wake-lock)+- [hydrogen-notifications](../hydrogen-notifications/SKILL.md) — OS toasts + the in-app `/ui/toast`+- the `screen-capture-wake-lock` skill (formerly a gallia skill; skills now ship via adom-wiki packages) — the **web-Hydrogen** side of this issue (where the leak actually happens)
skills/hydrogen-self-screenshot/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/hydrogen-self-screenshot/SKILL.mdadded+236@@ -0,0 +1,236 @@+---+name: hydrogen-self-screenshot+description: >+ Screenshot Hydrogen from inside the workspace — a panel/webview (the welcome page, a+ tab), the whole workspace, OR the whole screen/desktop — using the BUILT-IN+ `adom-cli hydrogen screenshot`. It saves a PNG into the container's+ `screenshots/` folder and prints the path to Read. THE answer for "take a+ screenshot of the welcome page", "screenshot the webview", "screenshot this+ panel", "screenshot the editor", "screenshot my whole screen / the desktop",+ "show me what Hydrogen looks like", or verifying a UI change (ralph loop). Use this+ even for "my whole screen / my desktop / my monitor" — that's the `screen`+ scope. You do NOT need any image resizer. For a SPECIFIC element/region (a CPU+ meter, a menu, a dialog, a toolbar) or an inline-b64 PNG, use the canonical+ `POST /screenshot` control endpoint with a CSS `selector` (it ALWAYS region-clips+ to that element) — and combine it with the UI command bus (`hydrogen-ui`: `ui/invoke`+ to open a menu/dialog, then `/screenshot` its selector). Identical across web+ Hydrogen and Hydrogen. Trigger words — screenshot Hydrogen, screenshot the welcome page,+ screenshot the webview, screenshot a panel, screenshot a tab, screenshot the+ workspace, screenshot the editor, screenshot my screen, screenshot the whole+ screen, screenshot the desktop, capture my monitor, full screen capture,+ screenshot everything on screen, what does Hydrogen look like, adom-cli hydrogen+ screenshot, capture panel, capture workspace, capture screen, screenshot a region,+ clip to selector, screenshot an element, screenshot a menu, screenshot a dialog,+ region screenshot, b64 screenshot, POST /screenshot, /shot, screenshot without flash,+ no-flash screenshot, which screenshot tool, verify visually, ralph loop, shotlog,+ image resizer, resize screenshot.+---++# Hydrogen — Screenshot a panel / webview / the workspace / the screen++The built-in way to screenshot anything inside Hydrogen is the **`adom-cli hydrogen+screenshot`** command — the *same* `adom-cli` you already use. It captures the+real rendered surface, drops a PNG into the container's `screenshots/` folder,+and prints the path. Just `Read` that path. **No image resizer needed.**++## Which screenshot tool? — the priority ladder++What you're capturing (and whether you want the teal flash) picks the tool. Default to+Hydrogen-native:++1. **Hydrogen content — a panel, the workspace, the editor, a webview, the screen+ (the 99% case): `adom-cli hydrogen screenshot panel|workspace|screen`. THIS IS THE+ DEFAULT — reach for it first.** It's the *same* command in web Hydrogen and Hydrogen (Hydrogen is+ meant to behave like web Hydrogen, and `adom-cli` is the one surface that's identical on+ both), saves a PNG to your container's `screenshots/`, and you just `Read` it. Don't lead+ with an Hydrogen-only endpoint.+ - **Hydrogen-only enhancement / power path — `POST /screenshot {target, selector, b64, silent}`+ (CDP).** Use ONLY when you need something `adom-cli` doesn't give you: a precise CSS+ `selector` region-clip, an inline-base64 PNG (`b64`), or a `silent` (no-flash) grab — see+ below. It does NOT exist in web Hydrogen, so it's a deliberate step OFF the consistent path.+ - **The one time you switch to CDP for an ordinary shot:** during another thread's+ recording the `adom-cli` SSE path returns `capture_busy` — then fall to `POST /screenshot`+ (CDP), which coexists (see "During a recording" below). That's a fallback, not the default.++## During a recording (multi-thread) — pick a coexisting method, never blind-retry++Adom users run **multiple AI threads on one Hydrogen**, so another thread may be recording the+screen while you capture. **Only the `adom-cli` SSE path (`screenshot panel/workspace/screen`)+is affected** — the CDP path coexists. Hydrogen makes this deterministic:++- **Pre-flight `GET /capture/availability`** → reports the recording state plus, per method,+ reliability + whether it coexists with a recording, a `recommended` method, and a `_hint`.+ Check it **only when a recording might be running.**+ - **`recommended` is STATE-DEPENDENT and agrees with the default:** when+ `recording.active === false` it returns **`"sse"`** (≡ `adom-cli hydrogen screenshot`, the+ web-Hydrogen-consistent default); when `recording.active === true` it returns **`"cdp"`**+ (the SSE path would `capture_busy`, so CDP is correct). So you can follow `recommended`+ directly — it will never push you off `adom-cli` while idle. Your everyday default remains+ `adom-cli`; CDP is the during-a-recording fallback.+- **`capture_busy`:** the SSE path (`PATCH /current/screenshot`, i.e. `adom-cli screenshot+ panel/workspace`) returns, the moment a recording is active, `{ok:false, error:"capture_busy",+ retry_after_ms, alternatives:[{method:"cdp"}], _hint}` — never an empty body. **On+ `capture_busy`: read `alternatives` and SWITCH — do NOT blindly retry the SSE path.**+- Authoritative recording state: `GET /recording/native/status`. See `hydrogen-recording`.++## Pick the scope — panel / workspace / screen++| You want… | Command | Sub-command |+|---|---|---|+| ONE panel or webview tab (the welcome page, a viewer, the editor) | `adom-cli hydrogen screenshot panel …` | `panel` |+| The whole Hydrogen **workspace** (every panel side by side) | `adom-cli hydrogen screenshot workspace` | `workspace` |+| The whole **screen** — desktop, OTHER apps | `adom-cli hydrogen screenshot screen` | `screen` |++**"My whole screen / my desktop / my monitor / everything"** → `adom-cli hydrogen+screenshot screen`. The Hydrogen `screen` scope captures the real desktop and saves the PNG into+your container, same as the others.++## Do this — it saves a PNG and prints the path; then Read it++```bash+# Whole workspace (simplest — no tab name needed):+adom-cli hydrogen screenshot workspace+# → prints screenshots/<ts>.png (relative to ~/project) → Read ~/project/screenshots/<ts>.png++# Whole screen / desktop (other apps too):+adom-cli hydrogen screenshot screen --reason "show the desktop"++# A specific panel / webview tab. Tab names are whatever the USER has open — DON'T+# guess (a guess like --name welcome 404s if the tab is actually named "Wiki").+# If you're not certain the tab exists by that name, LIST tabs first:+adom-cli hydrogen workspace get # shows every panel/tab: its id AND name+# then capture by --tab-id (most reliable) or the EXACT name you saw:+adom-cli hydrogen screenshot panel --tab-id <tabId>+adom-cli hydrogen screenshot panel --name "3D Viewer"+```++The welcome page is *usually* a tab named "Welcome", but on an active workspace it+may be replaced (e.g. by a "Wiki" tab) — so if `--name welcome` 404s with+`No tab matching 'welcome'`, run `adom-cli hydrogen workspace get` and screenshot+the webview tab you find by its `--tab-id`.++### What it returns — a saved path; just Read it+The command saves the PNG into the container's `screenshots/` folder and prints the+path. **Read it — done.** No decoding, no resizer, no second tool. The printed path+is **relative to `~/project`** (matches the cloud contract); the `_hint` gives the full+path:++```+screenshots/2026-06-13T20-04-43.png+_hint: Screenshot saved to ~/project/screenshots/<ts>.png. Read the file to view it.+```+→ `Read ~/project/screenshots/<ts>.png` (prepend `~/project/` to the line-1 path, or+just use the full path from the `_hint`).++### If a capture errors, retry once+A COLD capture can occasionally fail with a proxy URL containing a literal `{{port}}`+(e.g. `…/proxy/{{port}}/: error sending request`) — a known **adom-cli** cold-start+race (the proxy port isn't substituted yet). Just **run the same command again**; the+warm retry succeeds.++## Sharing must be active (it usually is; here's the gate)++Capturing a panel/workspace needs an active tab/screen share. Check and, if+needed, the `--reason` you pass pops a one-time approval the user clicks:++```bash+adom-cli hydrogen screenshot status # → {"sharing":{"active":true},"availableScopes":["panel","workspace"]}+# If a panel/workspace shot 409s or says sharing isn't active, just include+# --reason "<why>" (it triggers the approval dialog), or explicitly request it:+adom-cli hydrogen sharing request --share tab # panel/workspace scopes+adom-cli hydrogen sharing request --share screen # adds the 'screen' scope+```+The `screen` scope is for the whole desktop; `panel`/`workspace` come from tab+sharing. See `hydrogen-capture-share` for the sharing/approval UX.++**Release SCREEN sharing when you're done.** A `screen` share (getDisplayMedia) holds the+display **awake** — leaving it active blocks the user's screensaver. After a one-off+`screen` capture, run `adom-cli hydrogen sharing stop` (and close any Pup windows you+opened). Tab `panel`/`workspace` sharing doesn't hold the display, so this only matters+once you've requested the `screen` scope.++## Why this works the same in Hydrogen as it does for web Hydrogen++`adom-cli` is **one binary** used by both web Hydrogen and Hydrogen — you can test these+commands anywhere. The difference is purely *where the work runs*: Hydrogen intercepts+what adom-cli talks to. `adom-cli hydrogen <cmd>` hits `ADOM_HYDROGEN_URL`, which+in Hydrogen points at **Hydrogen's local proxy**. Hydrogen's proxy pulls out the **local hydrogen/SSE+commands** (screenshot, webview, caption, notify, recording…) and runs them against+the Hydrogen frontend over SSE, while **carbon/cloud calls pass straight through** to the+cloud. For a screenshot: `PATCH /current/screenshot` → Hydrogen broadcasts an SSE+`screenshot_request` → the frontend Element-Captures the panel → uploads it into+**this container's** `screenshots/` folder → the CLI returns the saved path. So the+command, and the returned path, behave the same — the bytes just land in your local+container.++## What NOT to do (these are the traps)++- **You do NOT need an image resizer.** `adom-cli hydrogen screenshot` produces a+ normal PNG and the `Read` tool reads PNGs directly. (Don't reach for ImageMagick+ `convert` or Pillow — neither is guaranteed in the golden image, and you don't+ need them.)++## Capture ANY region, element, or surface — `POST /screenshot`++`adom-cli hydrogen screenshot` above is the easy "save a panel/workspace/screen PNG"+path. For **precise control** — a specific VS Code or webview surface, **any CSS+region** (a meter, a menu, a dialog, a toolbar), or an inline PNG with no flash — use+Hydrogen's **canonical capture endpoint `POST /screenshot`** (aliases: `/shot`,+`/capture/viewport`). Read the control URL from the FILE — it's the dynamic port, and+your non-login Bash shell won't have the env var:++```bash+CTRL="$(cat ~/.adom/hydrogen-control-url)" # http://127.0.0.1:<dynamic>+curl -s -X POST "$CTRL/screenshot" -H 'Content-Type: application/json' \+ -d '{"target":"full","b64":true}'+```++| Field | Meaning |+|---|---|+| `target` | the surface — `full`/`window` (whole Hydrogen window) · `vscode`/`editor`/`workbench`/`code` (the VS Code panel) · `shell`/`hd` (the Svelte shell: title bar, menus, panels) · a **webview url-substring** (e.g. `wiki`) · **or a TAB DISPLAY NAME** (e.g. `"ScreenA"`, `"VS Code"`) — see by-name note below |+| `selector` | **optional CSS selector — it ALWAYS WINS and region-clips to that element.** e.g. `.resource-bars` (CPU/RAM meter), `iframe[src*='wiki']`, `.setup-panel`, `.dialog`, `.dropdown-content.open` |+| `b64` | `true` → returns the PNG inline as a `dataUrl` (decode + Read); else saved to a file in `screenshots/` |+| `silent` | `true` → suppress the capture flash |+| `name` | optional label for the saved file |++A `selector` clips to the **first matching element** in that surface — grab JUST a+meter, a menu, a dialog, a toolbar instead of the whole window (verified live:+`{selector:".resource-bars"}` → 192×61, vs `{target:"full"}` → the whole window).++**Screenshot a tab by its display NAME — incl. a BACKGROUND tab.**+`POST /screenshot {target:"<tab name>"}` resolves the name → the workspace panel, **activates+that tab** (so a *background* tab actually paints — a webview is an OOPIF and can't be CDP-shot+while hidden), captures the shell cropped to that panel, then **restores the previously-active+tab**. So in a pane stacked `VS Code | ScreenA | ScreenB` (ScreenB showing), `{target:"ScreenA"}`+returns ScreenA's pixels and leaves ScreenB active. The `adom-cli` SSE path+(`screenshot panel --name ScreenA`) also activates + restores. This is the right call for+"shot tab #2 while #4 is showing."++**The teal zap:** every Hydrogen-driven capture flashes a teal (`#00b8b0`) highlight on the+captured **region** (just the panel/menu/region; full-window flash only for+`target:full`). Teal = Hydrogen-driven. Pass `silent:true` to suppress it.++### Screenshot a menu / dialog / panel — open it, then clip to it+The killer combo with the UI command bus (see **`hydrogen-ui`**): **open a surface →+screenshot just that surface → close it** — no screen-clicking, no guessing where it is.+```bash+CTRL="$(cat ~/.adom/hydrogen-control-url)"+curl -s -X POST "$CTRL/ui/invoke" -H 'Content-Type: application/json' -d '{"id":"adom-menu.open"}'+curl -s -X POST "$CTRL/screenshot" -H 'Content-Type: application/json' -d '{"target":"shell","selector":".dropdown-content.open","b64":true}'+curl -s -X POST "$CTRL/ui/invoke" -H 'Content-Type: application/json' -d '{"id":"adom-menu.close"}'+```+Resolved selectors (verified live): **Adom menu & profile menu** → `.dropdown-content.open`+(only one open at a time); **Setup panel** → `.setup-panel`; **API Explorer** → `.dialog`.+`GET $CTRL/ui/actions` for the live set of menus/dialogs you can open (see `hydrogen-ui`).++## Ralph loop + shotlog (optional)++Verify a UI change: make the change → (rebuild if needed) →+`adom-cli hydrogen screenshot panel --name <panel>` → `Read` it → wrong? fix and+repeat. Log shots for the user with `shotlog inject -c hydrogen-ui -d "<desc>" <path>`+(shotlog resizes internally — another reason you don't need `convert`).++## Related skills+- [hydrogen-capture-share](../hydrogen-capture-share/SKILL.md) — the sharing/approval UX behind `screenshot`+- [hydrogen-recording](../hydrogen-recording/SKILL.md) · [hydrogen-captions](../hydrogen-captions/SKILL.md) — sibling `adom-cli hydrogen` AV verbs+- `shotlog` — the screenshot log viewer/injector
skills/hydrogen-setup-steps/SKILL.md+92−63@@ -1,88 +1,111 @@ --- name: hydrogen-setup-steps-description: What Hydrogen's setup steps did to prepare your workspace — the 28-step cascade that imports the Adom-Workspace golden image, injects your Adom session, wires up the relay, walks the Claude auth gate, and then opens 9 named AI-thread conversations. Use this skill when the user asks "what did setup do", "why is X installed", "re-run a setup step", "what's a virgin reset", or "why did step N fail". Trigger words — setup steps, install steps, setup panel, virgin reset, re-run step, Run All, what did setup do, why is X installed, setup failed, install-tools, hd setup, claude code extension install, gallia install, hd workspace ready.+description: What Hydrogen's setup steps did to prepare your workspace — the install sequence that provisions the golden workspace image, injects your Adom session, wires up the relay, and walks the Claude auth gate. Use this skill when the user asks "what did setup do", "why is X installed", "re-run a setup step", "what's a virgin reset", or "why did step N fail". Trigger words — setup steps, install steps, setup panel, virgin reset, re-run step, Run All, what did setup do, why is X installed, setup failed, install-tools, ah setup, claude code extension install, gallia install, ah workspace ready. --- # Hydrogen Setup Steps — what was done to prepare your workspace -WSL2-runtime version (default). The legacy 24-step Docker cascade-(`HD_RUNTIME=docker`) is in the docker/ bucket.--When you first launched Hydrogen (or after a virgin reset), Hydrogen ran a 28-step cascade to turn a blank machine into a fully-tooled Adom workspace — the `Adom-Workspace` WSL2 distro. The heavy tooling (code-server, gallia, the Adom CLIs, the Claude Code VS Code extension, VS Code settings, and all `hd-*` skills) is already BAKED into the golden image; setup imports that image, injects your Adom session, wires up the relay, installs the `claude` CLI at runtime (the one thing NOT baked — its self-setup needs a live session), walks the Claude auth gate, then opens the first conversation + 9 named AI-thread conversations. This skill explains what each step did so you can help the user understand their environment, re-run failed steps, or wipe and rebuild. See `hydrogen-golden-image` for the baked-image model.--The cascade is executed by `setup_steps_wsl.rs` (`step_defs_wsl()` / `run_all_wsl()`). `run_all_wsl` is halt-on-failure, resume-not-restart, with per-step 3× auto-retry. In-distro work runs via `wsl -d Adom-Workspace -u adom -- <cmd>` (the distro's DEFAULT user is `adom` per `wsl.conf default=adom`, home `/home/adom`; root via `-u root`). There is NO `docker exec` and NO Docker image anywhere in this flow.--## The 28 steps (in order)--| # | Step | Purpose |-|---|------|---------|-| 1 | ensure-workspace | Install WSL2 if needed (one auto-reboot), `wsl --import Adom-Workspace` from the golden image tarball (full pre-baked image from the `adom-inc/hd-wsl2-image` GitHub release), create the `adom` user, start code-server, verify host loopback on 7380. SUBSUMES the old Docker pull-image / create-container / start-container steps. |-| 2 | wait-codeserver | Host TCP probe of the code-server port (default 7380), 120s |-| 3 | update-packages | GATE: `adom-wiki pkg update` converges the container to the LATEST published packages (skills included) — the golden image is baked once but installed for months. Gates on the ARTIFACT (hydrogen-bootstrap == registry latest + skills deployed), not the raw exit code; re-runs bootstrap postinstalls when an update lands. |-| 4 | install-adom-vscode | "Activate editor extensions" — installs NOTHING (binary + extension are baked); waits for Adom sign-in, reloads the editor iframe, and proves the :8821 editor-control API answers |-| 5 | set-env-vars | Set `ADOM_CARBON_URL`, `ADOM_HYDROGEN_URL`, `ADOM_HD_CONTROL_URL`, `VSCODE_PROXY_URI`, `ADOM_DESKTOP_MODE` |-| 6 | inject-api-key | Write the Adom session token into the distro at `/var/run/adom/api-key` (mode 644) so adom-cli works |-| 7 | configure-vscode | settings.json / trusted-domains / activity-bar are BAKED; re-asserts theme + workbench backstops idempotently, and applies the per-session layout via :8821 |-| 8 | ensure-adom-bridge-cli | Verify the Adom Bridge companion app is running on Windows (the relay's desktop bridge) |-| 9 | start-relay | Start the adom-bridge-cli relay inside the workspace (ports 8765 WS / 8766 HTTP) |-| 10 | test-direct-connect | Prove the fast workspace→desktop command path |-| 11 | test-relay | Register the relay with Adom Bridge for file streaming (file transfer + shell exec) |-| 12 | test-adom-cli | GATE: carbon path + hydrogen-proxy reachability via adom-cli (6 retries) |-| 13 | install-claude-cli | Install the Claude Code CLI (`claude`) into the distro via the official installer. NOT baked (its self-setup needs a live session) — Hydrogen installs it here at runtime, before sign-in, so the editor's Claude Code + the Welcome/thread conversations have a working `claude`. Idempotent (skips if present). |-| 14 | claude-auth | The single human gate — restore Claude creds or drive the in-editor Claude.ai sign-in via the native browser Browser Picker; runs just before the payoff steps |-| 15 | ensure-sse | GATE: confirm the editor browser SSE session is connected so Welcome's webview-open doesn't 409 |-| 16 | verify-workspace | GATE: battery confirming the proxy holds a real layout |-| 17 | welcome | Open Claude Code, authenticate if needed, and human-type the first prompt ("who am i and what can i do here") |-| 18 | thread-parts-search | Fresh Claude conversation: Adom parts-search app in a webview, find a popular STM32 for a BLDC motor |-| 19 | thread-kicad | Fresh conversation: is KiCad installed? open the symbol library, else offer to install + show a sample board |-| 20 | thread-fusion360 | Fresh conversation: is Fusion 360 installed? open a sample project (sch/brd/3d), else offer to install |-| 21 | thread-altium | Fresh conversation: is Altium installed? open it, else report whether it can be installed |-| 22 | thread-pup | Fresh conversation: intro pup, show the Adom wiki (adom-lbr page) in a pup window |-| 23 | thread-native-browser | Fresh conversation: what the AI can do to drive the user's real Chrome/Edge (logged-in sites) + any extension needed |-| 24 | thread-google | Fresh conversation: install adom-google (its own guided auth), then latest email subject, next cal entry, a gchat DM, a sample PCB BOM sheet |-| 25 | thread-chip-fetcher | Fresh conversation: install chip-fetcher, show it in a webview, search VL53L8CH |-| 26 | thread-tsci | Fresh conversation: adom-tsci builds an RP2040 + USB-C dev board in one prompt, shown in pup |-| 27 | verify-setup | GATE (catch-all): independently re-verifies the whole workspace end-to-end; HALTS naming any artifact that isn't actually there |-| 28 | open-welcome | GATE (final): open welcome.html in Hydrogen's right pane; hard-fails if the Welcome tab doesn't appear |--Steps 18–26 (the AI threads) each open a FRESH Claude conversation and human-type a prompt, strictly one at a time. They replaced the old single `eda-discovery` + `instapcb-quote` steps.+This is the platform-generic, user-facing Q&A about what Hydrogen's setup did. The+exact per-platform step list, the workspace-provisioning commands, and the+state-file name live in your platform bootstrap's version of this skill.++When you first launched Hydrogen (or after a virgin reset), Hydrogen ran an+install sequence to turn a blank machine into a fully-tooled Adom workspace.+The heavy tooling (code-server, gallia, the Adom CLIs, the claude CLI + Code+extension, VS Code settings, and all `hd-*` skills) is already BAKED into the+golden image; setup just provisions that image, injects your Adom session,+wires up the relay, and walks the Claude auth gate. This skill explains what+each phase did so you can help the user understand their environment, re-run+failed steps, or wipe and rebuild. See `hydrogen-golden-image` for the baked-image+model.++The cascade is halt-on-failure, resume-not-restart, with per-step 3x+auto-retry. The exact steps and ordering are platform-specific — see your+platform bootstrap's version of this skill.++## The setup state machine — what each phase does++| Phase | Purpose |+|---|---|+| Provision the workspace | Ensure the runtime is available (install it if needed), provision the workspace from the golden image, create the workspace user, start code-server, verify host loopback. SUBSUMES the old pull-image / create-instance / start-instance steps. |+| wait-codeserver | Host TCP probe of the code-server port until it answers |+| install-adom-vscode | "Activate editor extensions" — installs NOTHING (binary + extension are baked); waits for Adom sign-in, reloads the editor iframe, and proves the editor-control API answers |+| set-env-vars | Set `ADOM_CARBON_URL`, `ADOM_HYDROGEN_URL`, `ADOM_HD_CONTROL_URL`, `VSCODE_PROXY_URI`, `ADOM_DESKTOP_MODE` |+| inject-api-key | Write the Adom session token into the workspace so adom-cli works |+| configure-vscode | settings.json / trusted-domains / activity-bar are BAKED; re-asserts theme + workbench backstops idempotently, and applies the per-session layout |+| ensure-adom-bridge-cli | Verify the Adom Bridge companion app is running (the relay's desktop bridge) |+| start-relay | Start the adom-bridge-cli relay inside the workspace |+| test-direct-connect | Prove the fast workspace→desktop command path |+| test-relay | Register the relay with Adom Bridge for file streaming (file transfer + shell exec) |+| test-adom-cli | GATE: carbon path + hydrogen-proxy reachability via adom-cli |+| claude-auth | The single human gate — restore Claude creds or drive the in-editor Claude.ai sign-in via the native browser Browser Picker; runs LAST before the payoff steps |+| ensure-sse | GATE: confirm the editor browser SSE session is connected so Welcome's webview-open doesn't 409 |+| verify-workspace | GATE: battery confirming the proxy holds a real layout |+| welcome | Open Claude Code, authenticate if needed, send the first prompt |+| open-welcome | GATE (final): open welcome.html in Hydrogen's right pane; hard-fails if the Welcome tab doesn't appear | ### Steps that NO LONGER EXIST (baked into the golden image) -These are NOT setup steps anymore — they are baked at image-build time: `install-gallia`, `install-hd-skills`, `verify-adom-bridge-cli`, `install-claude-ext`, `write-vscode-settings`, `set-trusted-domains`, `clean-layout`. If a user asks "what installed gallia / the Claude extension / my skills / VS Code settings", the answer is: **baked into the golden image at build time, not a setup step.** (Note: `install-claude-cli` is NOT in this list — the `claude` CLI IS a live setup step, since its self-setup needs a live session.) See `hydrogen-golden-image`.+These are NOT setup steps anymore — they are baked at image-build time:+`install-gallia`, `install-hd-skills`, `verify-adom-bridge-cli`,+`install-claude-cli`, `install-claude-ext`, `write-vscode-settings`,+`set-trusted-domains`, `clean-layout`. If a user asks "what installed gallia /+the Claude extension / my skills / VS Code settings", the answer is: **baked+into the golden image at build time, not a setup step.** See `hydrogen-golden-image`. ## Common user questions -**"Why is X installed in my workspace?"** → If X is gallia, an Adom CLI, the Claude Code extension, VS Code settings, or an `hd-*` skill, it was **baked into the golden image** — not run as a setup step. (The `claude` CLI is the exception: it's installed at RUNTIME by step 13 `install-claude-cli`.) The setup steps only import the image, inject your session, wire the relay, and walk Claude auth. The setup panel (Hydrogen UI → bottom) lists every step with its current status.+**"Why is X installed in my workspace?"** → If X is gallia, an Adom CLI, the+claude CLI, the Claude Code extension, VS Code settings, or an `hd-*` skill, it+was **baked into the golden image** — not run as a setup step. The setup steps+only provision the image, inject your session, wire the relay, and walk Claude+auth. The setup panel (Hydrogen UI → bottom) lists every step with its current+status. -**"Setup failed at step N"** → The setup panel shows the error. Most common failures: step 1 (WSL2 unavailable / Virtual Machine Platform feature off, or the `adom-golden.tar.gz` download), step 12 (test-adom-cli — carbon/hydrogen-proxy not reachable yet), step 13/14 (claude CLI install / Claude credentials expired). Each step has a retry button (and the cascade already retried it 3×).+**"Setup failed at step N"** → The setup panel shows the error. Most common+failures: the provision step (runtime unavailable, or the golden-image+download), test-adom-cli (carbon/hydrogen-proxy not reachable yet), and+claude-auth (Claude credentials expired). Each step has a retry button (and the+cascade already retried it 3x). Platform-specific failure causes are in your+platform bootstrap's version of this skill. -**"Re-run a single step"** → In the setup panel, click the step row and use the Re-run action. Or via API: `POST http://127.0.0.1:47084/setup/step/<id>` (optional body `{"continue_after":true}` to keep going after it).+**"Re-run a single step"** → In the setup panel, click the step row and use the+Re-run action. Or via API: `POST http://127.0.0.1:47084/setup/step/<id>`+(optional body `{"continue_after":true}` to keep going after it). -**"Re-run all steps"** → Click "Run All" in the setup panel; it resumes from the first not-done step. (The old headless `POST /setup/run-all` is now REFUSED.)+**"Re-run all steps"** → Click "Run All" in the setup panel; it resumes from the+first not-done step. (The old headless `POST /setup/run-all` is now REFUSED.) ## Virgin reset -Virgin reset wipes parts of your workspace so setup can rebuild them cleanly. The ONLY programmatic trigger is `POST /setup/panel/run-virgin-reset`. Toggle which artifacts to delete (option keys in parentheses):+Virgin reset wipes parts of your workspace so setup can rebuild them cleanly.+The ONLY programmatic trigger is `POST /setup/panel/run-virgin-reset`. Toggle+which artifacts to delete (option keys in parentheses): | Toggle (key) | What gets wiped | When to use |-|--------|----------------|-------------|-| Install step state (`install_state`) | Marks all steps as pending (`setup-steps-wsl.json`) so they re-run | Always (default on) |-| WSL2 distro (`container`) | PRISTINE `wsl --unregister Adom-Workspace` + reimport — deletes the distro AND everything in it, including `/home/adom/project` | Always (default on); your work in `/home/adom/project` is GONE |-| Distro tarball (`tarball`, legacy `image`) | Deletes the cached `adom-golden.tar.gz` (full-image re-download) | Rarely — only if the tarball is corrupted |+|---|---|---|+| Install step state (`install_state`) | Marks all steps as pending so they re-run | Always (default on) |+| Workspace (`container`) | PRISTINE wipe + re-provision — deletes the workspace AND everything in it, including your project files | Always (default on); your work is GONE |+| Image tarball (`tarball`, legacy `image`) | Deletes the cached golden image (full re-download) | Rarely — only if the image is corrupted | | Webview storage (`webview_storage`) | Queued wipe, flushed at next Hydrogen launch (restart_required) | Rarely | | VS Code state (`vscode_state`) | Queued wipe, flushed at next Hydrogen launch (restart_required) | Rarely | | Adom session token (`adom_token`) | Deletes `hydrogen-session.txt`; forces re-login | Rarely | | Claude credentials (`claude_token`) | Deletes the host-side Claude creds backup; forces Claude re-auth | When Claude auth is stuck | -`confirmed_destructive` must be set. After toggling, hit "Wipe Selected", then "Run All" to rebuild. The distro reset backs up Claude creds first UNLESS `claude_token` is checked; the `webview_storage` / `vscode_state` wipes are queued and flushed at the next Hydrogen launch (restart_required), not immediate.+`confirmed_destructive` must be set. After toggling, hit "Wipe Selected", then+"Run All" to rebuild. The workspace reset backs up Claude creds first UNLESS+`claude_token` is checked; the `webview_storage` / `vscode_state` wipes are+queued and flushed at the next Hydrogen launch (restart_required), not immediate. -CRITICAL: the WSL reset ONLY ever runs `wsl --unregister Adom-Workspace` (+ reimport). It never runs global `wsl --shutdown` or `wsl --uninstall` (those would kill the user's other distros, including any Docker Desktop WSL integration), and it performs NO Docker actions and NO reboot for the wipe.+CRITICAL: the workspace reset ONLY ever wipes Hydrogen's own workspace. It never+touches other workspaces or runtimes the user may have on the+machine, and it performs NO reboot for the wipe. The platform-specific wipe+commands are in your platform bootstrap's version of this skill. ## Setup panel APIs (useful for AI automation) | Endpoint | What it does |-|----------|--------------|+|---|---| | `GET /setup/state` | Current state — `never-run`, `in-progress`, `complete`, or `error` | | `POST /setup/step/<id>` | Re-run a single named step (optional `{"continue_after":true}`) | | `POST /setup/panel/run-virgin-reset` | Trigger virgin reset with the supplied toggle options |@@ -90,17 +113,23 @@ CRITICAL: the WSL reset ONLY ever runs `wsl --unregister Adom-Workspace` (+ reim All on Hydrogen's control API at `http://127.0.0.1:47084`. -**DEPRECATED — now REFUSED, do not use:** `POST /wsl/unregister`, `POST /setup/virgin-reset`, `POST /setup/run-all`, `POST /setup/run-step`. Use `POST /setup/panel/run-virgin-reset` and `POST /setup/step/<id>` instead.+**DEPRECATED — now REFUSED, do not use:** `POST /setup/virgin-reset`,+`POST /setup/run-all`, `POST /setup/run-step` (plus deprecated runtime-specific+endpoints listed in your platform bootstrap). Use+`POST /setup/panel/run-virgin-reset` and `POST /setup/step/<id>` instead. ## Setup state file -Setup writes its state to `%APPDATA%\hydrogen-desktop\setup-steps-wsl.json` (one entry per step with `status: pending|running|done|failed`, output text, percent complete). Hydrogen reads this on launch to decide whether the setup panel auto-opens. (The legacy Docker runtime uses `setup-steps.json`.)+Setup writes its state to a per-step state file (one entry per step with+`status: pending|running|done|failed`, output text, percent complete). Hydrogen reads+this on launch to decide whether the setup panel auto-opens. The exact file+name and location are platform-specific — see your platform bootstrap's+version of this skill. ## Related skills -- `hydrogen-golden-image` — the baked-image model: what's pre-installed in `adom-golden.tar.gz` vs. what setup does-- `hydrogen-container` — the WSL2 distro and runtime that setup configures-- `hydrogen-networking` — how the distro reaches the host (`127.0.0.1` via WSL2 mirrored networking; live control URL in `~/.adom/hydrogen-control-url`) and how ports forward-- `hydrogen-browser-picker` — used by step 14 (claude-auth) for the Claude auth OAuth flow-- `hydrogen-adom-auth` — how the Adom session token (step 5) gets injected-- `hydrogen-volume` — where your files live in the distro and what survives a virgin reset (or doesn't)+- your platform bootstrap's `hydrogen-setup-steps` — the concrete per-platform cascade, provisioning/wipe commands, state file, and the golden tarball+- `hydrogen-golden-image` — the baked-image model: what's pre-installed vs. what setup does+- `hydrogen-setup` — the setup-panel UX and virgin-reset toggles+- `hydrogen-browser-picker` — used by claude-auth for the Claude auth OAuth flow+- `hydrogen-adom-auth` — how the Adom session token (inject-api-key) gets injected
skills/hydrogen-setup/SKILL.md+98−102@@ -1,71 +1,70 @@ --- name: hydrogen-setup description: >- Hydrogen setup panel: 27 install steps, Run All, Rollback All, Virgin Reset with- toggles, and automated testing patterns. MUST READ before running setup,+ Hydrogen setup panel: the install-step cascade, Run All, Rollback All, Virgin Reset+ with toggles, and automated testing patterns. MUST READ before running setup, testing steps, or doing virgin resets. Covers the step list, the virgin reset toggle panel, how to keep auth during resets, and how Run All handles failures. Trigger words — setup panel, install steps, run all, virgin reset,- rollback, step failed, 28 steps, wipe, reset workspace, keep auth, test setup.+ rollback, step failed, wipe, reset workspace, keep auth, test setup. --- -# Hydrogen — Setup Panel & Virgin Reset+# Hydrogen -- Setup Panel & Virgin Reset -WSL2-runtime version (default). The legacy 24-step Docker cascade-(`HD_RUNTIME=docker`) is in the docker/ bucket.+This is the platform-generic reference for the Hydrogen setup panel and virgin+reset. The host-platform specifics (the exact install cascade, workspace+import/unregister mechanics, the per-platform step list and state file)+live in your platform bootstrap's version of this skill. ## Zero-Click Goal The Hydrogen setup process targets ZERO manual clicks for the user. The heavy-lifting is already done at image-build time: the `Adom-Workspace` golden-image ships with code-server, gallia, the 8 Adom CLIs, the claude CLI + Code-extension, VS Code settings, and all `hd-*` skills BAKED IN. Setup just-imports that image, injects the user's session, wires up the relay, and walks-the single human auth gate. If something is missing, Hydrogen downloads it — never-tell the user to install something manually. See the `hydrogen-golden-image` skill-for the baked-image model.--## The 27 Install Steps--The WSL cascade is executed by `setup_steps_wsl.rs` (`step_defs_wsl()` /-`run_all_wsl()`). `run_all_wsl` is halt-on-failure, resume-not-restart, with-per-step 3× auto-retry. State is tracked in `setup-steps-wsl.json`-(the Docker flow uses `setup-steps.json`). The Docker-only `pull-image` /-`create-container` / `start-container` steps are SUBSUMED into step 1,-`ensure-workspace`, which imports the golden distro via `wsl --import-Adom-Workspace`.--| # | ID | Label | Rollback? |-|---|---|---|---|-| 1 | ensure-workspace | Install WSL2 if needed (one auto-reboot), import golden distro, create `adom` user, start code-server, verify host loopback (7380) | ✓ |-| 2 | wait-codeserver | Host TCP probe of code-server port (default 7380), 120s | ✗ |-| 3 | install-adom-vscode | "Activate editor extensions" — installs NOTHING (binary+extension baked); waits for Adom sign-in, reloads editor iframe, proves the :8821 editor-control API answers | ✓ |-| 4 | set-env-vars | ADOM_CARBON_URL / ADOM_HYDROGEN_URL / ADOM_HD_CONTROL_URL / VSCODE_PROXY_URI / ADOM_DESKTOP_MODE | ✓ |-| 5 | inject-api-key | Write the Adom session token to `/var/run/adom/api-key` (mode 644) | ✓ |-| 6 | configure-vscode | settings.json / trusted-domains / activity-bar are BAKED; re-assert theme + workbench backstops idempotently; apply per-session layout via :8821 | ✓ |-| 7 | ensure-adom-bridge-cli | Verify the Adom Bridge companion app is running on Windows | ✗ |-| 8 | start-relay | Start the adom-bridge-cli relay inside the workspace (8765/8766) | ✓ |-| 9 | test-direct-connect | Prove the fast container→desktop command path | ✗ |-| 10 | test-relay | Register the relay with Adom Bridge for file streaming | ✗ |-| 11 | test-adom-cli | GATE: carbon path + hydrogen-proxy reachability via adom-cli (6 retries) | ✗ |-| 12 | install-claude-cli | Install the `claude` CLI into the distro at RUNTIME (NOT baked — its self-setup needs a live session). Idempotent | ✗ |-| 13 | claude-auth | The single human gate; restore creds or drive in-editor Claude.ai sign-in; runs just before payoff | ✓ |-| 14 | ensure-sse | GATE: editor browser SSE session connected so Welcome's webview-open doesn't 409 | ✗ |-| 15 | verify-workspace | GATE: battery confirming the proxy holds a real layout | ✗ |-| 16 | welcome | Opens Claude Code, authenticates if needed, human-types the first prompt | ✗ |-| 17-25 | thread-* (9) | Nine FRESH Claude conversations, one at a time: parts-search, kicad, fusion360, altium, pup, native-browser, google, chip-fetcher, adom-tsci. Replaced the old eda-discovery + instapcb-quote steps | ✗ |-| 26 | verify-setup | GATE (catch-all): independently re-verifies the whole workspace end-to-end; HALTS naming any missing artifact | ✗ |-| 27 | open-welcome | GATE (final): opens welcome.html in Hydrogen's right pane; hard-fails if the Welcome tab doesn't appear | ✗ |+lifting is already done at image-build time: the golden workspace image ships+with code-server, gallia, the Adom CLIs, the claude CLI + Code extension, VS+Code settings, and all `hd-*` skills BAKED IN. Setup just provisions that+image, injects the user's session, wires up the relay, and walks the single+human auth gate. If something is missing, Hydrogen downloads it — never tell the+user to install something manually. See the `hydrogen-golden-image` skill for the+baked-image model.++## The Install Steps++The setup cascade is a halt-on-failure, resume-not-restart sequence with+per-step 3x auto-retry. State is tracked per-step (status pending / running /+done / failed, output text, percent complete) and Hydrogen reads it on launch to+decide whether the setup panel auto-opens. The exact step list, ordering, and+state-file name are platform-specific — see your platform bootstrap's version+of this skill.++Conceptually the steps fall into these phases:++1. **Provision the workspace** — ensure the runtime is available, import the+ golden image, create the workspace user, start code-server, verify host+ loopback reachability. This single step subsumes any older+ pull-image / create-instance / start-instance trio.+2. **Activate the editor** — wait for code-server, activate editor extensions+ (baked, installs nothing), reload the editor iframe, prove the+ editor-control API answers.+3. **Wire identity + environment** — set the Adom env URLs, inject the Adom+ session token, re-assert VS Code config backstops idempotently, apply the+ per-session layout.+4. **Wire the relay** — verify the Adom Bridge companion app, start the+ relay inside the workspace, prove direct-connect and register the relay for+ file streaming.+5. **Gates + payoff** — gate on adom-cli reachability (carbon ++ hydrogen-proxy), walk the single Claude auth gate, confirm the editor SSE+ session, verify the proxy holds a real layout, open Claude Code and send the+ first prompt, and finally open welcome.html in Hydrogen's right pane (hard-fails+ if the Welcome tab doesn't appear). ### Steps that NO LONGER EXIST (baked into the golden image) These are NOT setup steps anymore — they are baked at image-build time and must not be listed or "run": `install-gallia`, `install-hd-skills`,-`verify-adom-bridge-cli`, `install-claude-ext`,-`write-vscode-settings`, `set-trusted-domains`, `clean-layout`. gallia, the 8-Adom CLIs, the Claude Code VS Code extension (the claude CLI is runtime step 12, NOT baked), code-server, settings, and all-`hd-*` skills are baked into the golden image. See-`hydrogen-golden-image`.+`verify-adom-bridge-cli`, `install-claude-cli`, `install-claude-ext`,+`write-vscode-settings`, `set-trusted-domains`, `clean-layout`. gallia, the+Adom CLIs, the claude CLI + Code extension, code-server, settings, and all+`hd-*` skills are baked into the golden image. See `hydrogen-golden-image`. ## Panel Buttons @@ -77,10 +76,10 @@ Adom CLIs, the Claude Code VS Code extension (the claude CLI is runtime step 12, ## CRITICAL: Run All Stops on Failure -If any step fails, Run All ABORTS (halt-on-failure in `run_all_wsl()`, after-the per-step 3× auto-retry is exhausted). Remaining steps are NOT executed.-`run_all_wsl` resumes — it does NOT restart from step 1 — so to recover after-a failure: fix the issue, then either:+If any step fails, Run All ABORTS (halt-on-failure, after the per-step 3x+auto-retry is exhausted). Remaining steps are NOT executed. The cascade+resumes — it does NOT restart from the first step — so to recover after a+failure: fix the issue, then either: - Click "Run All" again (it resumes from the first not-done step) - Click "Run" on the failed step to retry just that one @@ -92,9 +91,9 @@ live status badge showing the current state. The ONLY programmatic trigger is | Toggle (option key) | What it wipes | Status badges | |---|---|---|-| Install step state (`install_state`) | Resets all 28 steps to pending (`setup-steps-wsl.json`) | CLEAN / EXISTS |-| WSL2 distro (`container`) | PRISTINE `wsl --unregister Adom-Workspace` + reimport (ONLY that distro) | GONE / RUNNING / EXISTS |-| Distro tarball (`tarball`, legacy `image`) | Deletes cached `adom-golden.tar.gz` (full image re-download) | CACHED / GONE |+| Install step state (`install_state`) | Resets all steps to pending | CLEAN / EXISTS |+| Workspace (`container`) | PRISTINE wipe + re-provision of the workspace (ONLY Hydrogen's own workspace) | GONE / RUNNING / EXISTS |+| Image tarball (`tarball`, legacy `image`) | Deletes the cached golden image (full re-download) | CACHED / GONE | | Webview storage (`webview_storage`) | Queued wipe, flushed at next Hydrogen launch (restart_required) | CLEAN / EXISTS | | VS Code state (`vscode_state`) | Queued wipe, flushed at next Hydrogen launch (restart_required) | CLEAN / EXISTS | | Adom session token (`adom_token`) | Deletes `hydrogen-session.txt` | CLEAN / EXISTS |@@ -103,15 +102,15 @@ live status badge showing the current state. The ONLY programmatic trigger is `confirmed_destructive` must be set for the reset to proceed. **"Wipe Selected"** at the bottom executes the reset for all checked toggles. -The distro reset is a PRISTINE `wsl --unregister` + reimport — it backs up-Claude creds first UNLESS `claude_token` is checked. The `webview_storage` /+The workspace reset is a PRISTINE wipe + re-provision — it backs up Claude+creds first UNLESS `claude_token` is checked. The `webview_storage` / `vscode_state` wipes are queued and flushed at the next Hydrogen launch (restart_required), NOT immediate; there is no reboot for the wipe. -CRITICAL — the WSL virgin reset only ever touches `Adom-Workspace`. It runs-`wsl --unregister Adom-Workspace` — NEVER global `wsl --shutdown` or-`wsl --uninstall`, which would knock out the user's other distros (e.g. a-pre-existing Docker Desktop integration). There is NO Docker actions.+CRITICAL — the virgin reset only ever touches Hydrogen's OWN workspace. It NEVER+touches other workspaces or runtimes the user may have on the+machine. The platform-specific commands (and the never-touch-global rule)+are in your platform bootstrap's version of this skill. ## Automated Testing Pattern @@ -123,8 +122,8 @@ Unchecked: tarball (cached, saves the full-image re-download), adom_token, claude_token ``` -This gives a freshly-imported distro while keeping auth working — step 5-(inject-api-key) and step 12 (claude-auth) won't fail.+This gives a freshly-provisioned workspace while keeping auth working — the+inject-api-key step and the claude-auth step won't fail. ### Programmatic triggers (the ONLY supported headless paths) @@ -142,43 +141,40 @@ curl -X POST http://127.0.0.1:47084/setup/step/test-direct-connect \ ``` DEPRECATED — do NOT use these; they are now REFUSED (deprecated headless-triggers): `POST /wsl/unregister`, `POST /setup/virgin-reset`,-`POST /setup/run-all`, `POST /setup/run-step`.+triggers): `POST /setup/virgin-reset`, `POST /setup/run-all`,+`POST /setup/run-step` (plus deprecated runtime-specific endpoints listed in+your platform bootstrap). ## Key Steps Explained -### Step 1 — ensure-workspace-The big one. Installs WSL2 if needed (one auto-reboot), then `wsl --import-Adom-Workspace` from the full pre-baked golden image `adom-golden.tar.gz`-(pulled from the `adom-inc/hd-wsl2-image` GitHub release — NOT a thin ~30 MB-tarball, NOT a 390 MB Docker image), creates the `adom` user, starts-code-server, and verifies host loopback on port 7380. This single step-replaces the old Docker pull-image / create-container / start-container trio.-The distro's DEFAULT user is `adom` (`wsl.conf default=adom`), NOT root;-in-distro exec = `wsl -d Adom-Workspace -u adom --` (root via `-u root`) —-NEVER `docker exec`.--### Step 5 — inject-api-key-Writes the Adom session token into the distro at `/var/run/adom/api-key`-(mode 644) so adom-cli works. Requires the user to be signed in. Keep the-`adom_token` toggle UNCHECKED during automated test resets.--### Step 9 — test-direct-connect-Proves the fast container→desktop command path. The CLI auto-probes the host-control API (wired to the WSL gateway IP by `/etc/init-host-internal.sh`) and-routes through Adom Bridge's direct connect API.--### Step 10 — test-relay (register relay)-Registers the relay (started by step 8, `start-relay`) with Adom Bridge for+### Provision the workspace (step 1)+The big one. Ensures the runtime is available (installing it if needed), then+provisions the workspace from the full pre-baked golden image, creates the+workspace user, starts code-server, and verifies host loopback reachability.+This single step replaces the old pull-image / create-instance /+start-instance trio. Platform-specific provisioning commands and any+runtime-install reboot are in your platform bootstrap's version of this skill.++### inject-api-key+Writes the Adom session token into the workspace so adom-cli works. Requires+the user to be signed in. Keep the `adom_token` toggle UNCHECKED during+automated test resets.++### test-direct-connect+Proves the fast workspace→desktop command path. The CLI auto-probes the host+control API and routes through Adom Bridge's direct connect API.++### test-relay (register relay)+Registers the relay (started by the start-relay step) with Adom Bridge for file streaming via the code-server proxy path. This enables `pull_file`, `send_files`, and `shell_execute`. -### Step 12 — claude-auth-The single human gate. Restores / validates Claude creds in the distro+### claude-auth+The single human gate. Restores / validates Claude creds in the workspace (backed up host-side) and, when needed, drives the in-editor Claude.ai sign-in via the native browser Browser Picker. Runs LAST before the payoff steps. -### Steps 13-16 — the final gates+### The final gates `ensure-sse` confirms the editor browser SSE session is connected (so the Welcome webview-open doesn't 409); `verify-workspace` confirms the proxy holds a real layout; `welcome` opens Claude Code and sends the first prompt; the@@ -186,14 +182,14 @@ final `open-welcome` gate opens welcome.html in Hydrogen's right pane and HARD-F if the Welcome tab doesn't appear. > Note: gallia, the Adom CLIs, the claude CLI + Code extension, VS Code-> settings, and `hd-*` skills are NOT setup steps — they are baked into-> `adom-golden.tar.gz`. See `hydrogen-golden-image`.+> settings, and `hd-*` skills are NOT setup steps — they are baked into the+> golden image. See `hydrogen-golden-image`. -## Distro Recreation+## Workspace Recreation -When the distro is unregistered and re-imported (step 1), the new distro is a-fully pre-baked Ubuntu from `adom-golden.tar.gz`. WSL2 auto-forwards-code-server's port 7380 to Windows `localhost:7380` — there is no Docker-port-mapping layer, so the port-conflict / stale-mapping class of bug that-affected the Docker runtime does not apply here. Other services tunnel through-the code-server `/proxy/{port}/` path.+When the workspace is wiped and re-provisioned (step 1), the new workspace is+a fully pre-baked image. Host port-forwarding for code-server is handled by the+runtime, so the port-conflict / stale-mapping class of bug does not apply.+Other services tunnel through the code-server `/proxy/{port}/` path. The+platform-specific port-forwarding behavior is in your platform bootstrap's+version of this skill.
skills/hydrogen-ui/._SKILL.mdadded⋯ 1 unchanged line ⋯
skills/hydrogen-ui/SKILL.mdadded+231@@ -0,0 +1,231 @@+---+name: hydrogen-ui+description: >+ Hydrogen's user interface: menus, dialogs, panels, login/logout, profile menu,+ workspace layout, setup panel — and how to DRIVE them from the workspace via+ the UI command bus (GET /ui/actions → POST /ui/invoke), the first-class way to+ open/close/toggle any Hydrogen menu or dialog (no CDP clicking). READ when the user+ asks you to open/close a dialog or menu, operate Hydrogen's UI, or you need to know+ where a UI element is. Trigger words — Hydrogen menu, Hydrogen dialog, Hydrogen panel, open+ dialog, open menu, close dialog, drive UI, ui/actions, ui/invoke, command bus,+ ports.open, settings.open, about dialog, profile menu, login, logout, settings,+ setup panel, Adom menu, Hydrogen UI.+---++# Hydrogen — UI Reference++## Window Layout++Hydrogen's window has these regions (top to bottom, left to right):++### Title Bar (frameless, 0.1.251+)+Hydrogen is a FRAMELESS window: the nav bar IS the title bar (no separate OS chrome).+- **Left**: Adom logo (the Adom menu button), then the lightbulb (sample-prompts menu, moved here 0.1.265) — drag anywhere else in the bar to move the window+- **Right**: CPU/RAM badges, profile avatar menu, then the window controls+ (minimize/maximize/close) merged into the bar's right end+- All title-bar controls share one interaction family: soft white hover wash, teal+ press, steady teal wash while their menu is open++### Tab Bar+- **VS Code** tab — the main code editor (iframe to code-server)+- **Webview** tab — wiki/app content panel+- Additional tabs for bridges, sensors, etc.++### Main Content Area+- **Left pane**: Usually VS Code iframe+- **Right pane**: Usually webview (wiki, shotlog, etc.)+- Panes are split and resizable++### Moving tabs between panes (drag-to-split, 1.0.140+)+Drag any tab and hover another pane: teal drop zones light up (Split Left / Right /+Top / Bottom, Swap, or the tab bar to add). This works over EVERY pane type,+webviews included (a glass layer covers iframes during the drag so they cannot+swallow the pointer), and exactly ONE zone pill shows at a time. Releasing on a+zone performs the move; **Escape abandons the drag** and everything returns to+exactly how it was. Right-clicking a tab offers the same splits as menu items+(a one-tab pane asks what to put in the new half via Add Panel).++### Setup Panel (bottom)+- Collapsible panel showing the setup-cascade steps (27 steps under the WSL2 runtime)+- See `hydrogen-setup` skill for details++## Menus++### Adom Menu (top-left logo dropdown)+The main menu, opened by clicking the Adom logo at the top-left. Items are grouped into labelled sections (in order, top to bottom):++**(top, unlabelled)** — "Go to Dashboard" was REMOVED (0.1.252, John 2026-07-24); don't offer it+- **Settings** — opens the Settings dialog+- **Ports...** — opens the port configuration dialog (see hydrogen-ports)+- **API Explorer...** — opens the control-API endpoint explorer dialog++**DESKTOP** (Tauri-only — won't appear in browser-served Hydrogen; in order)+- **Adom Bridge** — brings the embedded Adom Bridge window to the foreground (taskbar); bus id `adom-bridge-cli.open`+- **Zoom** — webview zoom controls (− % +) with persistence across launches+- **Fullscreen** — toggle fullscreen+- **Console** — show/hide the Hydrogen debug console window+- **Developer Tools** — open WebView2 DevTools. (The "Dev Toolbar" item was retired in 0.1.256 — use the API Explorer instead)++> The Bridges section / "Bridge Manager" item was removed (it was a no-op with no backing panel). For the bridge panel itself, see `hydrogen-bridges`.++**Workspace / runtime section** (section label shows live status badge: `RUNNING`, `STOPPED`, etc.)+- **Restart** — restarts the workspace: terminates the `Adom-Workspace` distro and re-ensures code-server.+- **Stop** — stops the workspace: terminates the `Adom-Workspace` distro.++**ADMIN**+- **Setup Steps** — opens the setup panel (the setup cascade); see hydrogen-setup-steps+- **Virgin Reset** — opens the virgin reset section of the setup panel; see hydrogen-volume for what each toggle deletes+- **Browser Picker Manager** — configure per-domain default browser/profile prefs; see hydrogen-browser-picker++### Profile Menu (top-right avatar)+Click the user avatar (circular photo) in the top-right corner.+- **Your profile** — view Adom profile+- **Your repositories** — list repos+- **Your molecules** — list molecules+- **Log out** — signs out of Adom, removes session token++**CRITICAL**: Logout is ONLY in the profile menu (top-right avatar),+NOT in the Adom menu (top-left). Future AI threads: don't look for+logout in the wrong menu.++## Dialogs++### Port Settings Dialog+Access: Adom menu → Ports...+Shows all configurable ports with live conflict detection.+Fields: Code Editor, Workspace Proxy, Control API, DevTools (CDP), Hostname.+See `hydrogen-networking` skill for port details.++### Settings Dialog+Access: Adom menu → Settings+Application-level preferences.++### Browser Picker Dialog+Appears when clicking external links. Shows detected browser profiles+(Edge, Chrome, etc.) with per-domain "remember" option.++## Driving the UI — the command bus (PREFERRED, first-class)++⭐ **Every menu / dialog / panel Hydrogen has wired is a first-class action you drive over the control+API — no CDP, no `.click()`, no selector guessing.** This is how you operate Hydrogen's UI when the+user says "open the Ports dialog", "open About", "show settings", etc.++**Get the control URL from the discovery file** `~/.adom/hydrogen-control-url` (Hydrogen rewrites it with+the live, dynamic control port every launch; it's the host loopback, shared with the distro by+WSL2 mirrored networking). Read it from a file, not an env var — your Bash tool runs+non-interactive shells that don't source `.bashrc`/`profile.d`:++```bash+CTRL="$(cat ~/.adom/hydrogen-control-url)"+```++**Always do this in order:**++1. **Discover** what's drivable (ids + descriptions):+ ```bash+ curl -s "$CTRL/ui/actions"+ # → {ok, count, actions:[{id,label,description,group}], _hints}+ ```+2. **Drive** one — returns ONLY after the frontend actually ran it:+ ```bash+ curl -s -X POST "$CTRL/ui/invoke" \+ -H 'Content-Type: application/json' -d '{"id":"ports.open"}'+ # → {ok:true, id:"ports.open", value:…}+ ```+3. **Not in the list?** That absence is the signal: the element isn't wired to the bus yet. It+ needs a 2-line `registerUiAction(...)` in its Svelte component (a dev change in the+ hydrogen-desktop repo — see that repo's `hydrogen-ui-actions` skill). Don't fall back to clicking;+ wire it. The response's `_hints.if_missing` says the same.++Common ids (always `GET /ui/actions` for the live set): `adom-menu.open`/`.close`,+**`profile-menu.open`/`.close`**, `ports.open`/`.close`, `settings.open`/`.close`,+`api-explorer.open`/`.close`, `browser-picker.open`/`.close`, `about.open`/`.close`,+`adom-bridge-cli.open`, `setup-panel.show`, `virgin-reset.open`,+**`samples-menu.open`/`.close`** (the lightbulb sample-prompts menu),+`fullscreen.toggle`, `console.toggle`, `devtools.open`,+`container.start`/`.stop`/`.restart`. (`dashboard.go` and `dev-toolbar.toggle` were+REMOVED — 0.1.252/0.1.256.)++**Menu ITEMS, not just open/close:** the profile menu also exposes per-item actions —+**`profile.view`**, **`profile.repositories`**, **`profile.molecules`**, **`profile.logout`** —+so you can trigger a specific item directly (it navigates as if the human clicked it),+WITHOUT opening the menu + screen-clicking. (`profile.logout` is the canonical logout — see+the logout note above.) `GET /ui/actions` for the live set; the Adom menu will get the same+item-level treatment over time.++> Note: there is **no** `port-mappings.open`/`.close` id — that action was never+> registered. Don't try to invoke it.+> **Gap:** `setup-panel` has `.show` but **no `.hide`** — to close it, eval+> `window.__hdSetSetupPanel(false)` (via `/eval-in`) or open another surface. A+> `setup-panel.hide` action is on the wishlist.++### Open a surface, then screenshot it (the killer combo)+Combine the bus with Hydrogen's canonical capture endpoint to grab any menu/dialog/panel with+**no screen-clicking**: `POST /ui/invoke {id:"<x>.open"}` → `POST /screenshot+{target:"shell", selector:"<open-state selector>"}` → `POST /ui/invoke {id:"<x>.close"}`.+The `/screenshot` region-flash lands on that selector. Resolved selectors (verified live):+**Adom & profile menus** → `.dropdown-content.open` (one open at a time); **Setup panel** →+`.setup-panel`; **API Explorer** → `.dialog`. Full `/screenshot` reference (target/selector/+b64/silent) is in **[hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md)**.++> **This is Hydrogen-desktop-only and SEPARATE from `adom-cli`.** `adom-cli` drives *web-hydrogen's*+> UI via the SSE bridge (`ADOM_HYDROGEN_URL`) and is unchanged. For Hydrogen desktop's far richer UI+> control, use the command bus above — never `adom-cli` for this.++## Legacy: raw CDP clicking (deprecated fallback — the command bus above is canonical)++⚠ The command bus (`/ui/actions` → `/ui/invoke`) is the canonical way to drive Hydrogen's UI. Before+it existed, UI was driven by CDP `.click()` on CSS selectors via hydrogen-cdp.js. This is brittle+(selectors drift; multiple `localhost:1420` CDP targets) and the selectors below may be stale.+Reach for CDP ONLY for an element not yet on the bus — and the better fix is to register it.++### Find and click menu items+```bash+# Open Adom menu+echo document.querySelector('.logo-button')?.click() | node C:\Users\john\hydrogen-cdp.js++# Find a menu item by text+echo var items=document.querySelectorAll('.dropdown-link,.menu-item');+var target=Array.from(items).find(function(e){return e.textContent.includes('Ports')});+if(target)target.click() | node C:\Users\john\hydrogen-cdp.js+```++### Click setup panel buttons+```bash+# Run All+echo document.querySelector('.run-all').click() | node C:\Users\john\hydrogen-cdp.js++# Virgin Reset dropdown+echo document.querySelector('.virgin-reset-btn')?.click() | node C:\Users\john\hydrogen-cdp.js++# Wipe Selected (execute virgin reset)+echo document.querySelector('.virgin-go')?.click() | node C:\Users\john\hydrogen-cdp.js++# Close setup panel+echo document.querySelector('.close-btn,.setup-close')?.click() | node C:\Users\john\hydrogen-cdp.js+```++### Query step states+```bash+echo JSON.stringify(Array.from(document.querySelectorAll('.step-row')).map(+function(e,i){+ var icon=e.querySelector('.step-icon');+ return{n:i+1, s:icon?.textContent?.trim()}+})) | node C:\Users\john\hydrogen-cdp.js+```++## CSS Selectors for Key Elements++| Element | Selector |+|---------|----------|+| VS Code iframe | `iframe.vis-studio-iframe` or `iframe[title="code"]` |+| Setup panel | `.setup-panel` |+| Run All button | `.run-all` |+| Virgin Reset button | `.virgin-reset-btn` |+| Wipe Selected button | `.virgin-go` |+| Step rows | `.step-row` |+| Step icon (✅/❌/○) | `.step-icon` |+| Step label | `.step-label` |+| Adom menu button | `.logo-button` |+| Profile menu button | `.profile-dropdown-button` |+| Tab bar | tab container at top |
skills/hydrogen-wake-repair/SKILL.mdadded+144@@ -0,0 +1,144 @@+---+name: hydrogen-wake-repair+description: >+ How Hydrogen automatically repairs the workspace connection after the+ laptop wakes from sleep: the wake-heal cascade, the header wake indicator with+ its live info tooltip, the Settings > Desktop > Wake section (heal toggle,+ indicator toggle, linger slider), and the /wake/* control API endpoints. Read+ this when the user reports Hydrogen did not reconnect after sleep, sees a "cannot+ reconnect" dialog, asks what the sunrise/sync icon in the header is, or wants+ to test or configure wake behavior.+ Trigger words: wake from sleep, laptop sleep, lid close, sleep resume, resume+ from sleep, reconnect after sleep, wake repair, wake heal, wake indicator,+ sunrise icon, cannot reconnect, workspace stuck after wake, wake status,+ wake simulate, wake settings, run wake check, wake check menu, backend+ banner, workspace stopped responding, reconnect button.+---++# Hydrogen Wake Repair (reconnect after laptop sleep)++> The heal cascade below manipulates the `Adom-Workspace` WSL distro.++When the laptop sleeps, WSL2 networking and code-server often come back wedged:+the user reopens the lid and sees a dead editor pane or VS Code's "Cannot+reconnect" dialog. Hydrogen detects the wake natively (Win32 power notifications,+both manual and automatic resume, deduplicated) and runs an automatic repair so+the user should normally have to do NOTHING.++## What the user sees++- **Wake indicator** in the header, to the right of the lightbulb (only if+ enabled in settings, default off): a spinning sync icon while healing, a+ sunrise icon when the heal finished, an alert icon if it failed.+- Hovering the small **info (i) icon** next to it shows a live tooltip with the+ current phase and the last log lines, updating as the heal runs.+- The indicator lingers for a configurable time after the wake (default 60s),+ then hides itself.++## Running it manually: Adom menu > "Run Wake Check"++The whole cascade can be triggered on demand from the Adom menu ("Run Wake+Check", right after API Explorer). The tooltip behavior is deliberate:++- **Manual trigger** (menu item): the wake icon appears AND the info tooltip+ pins itself open so the user can watch every phase live, then unpins shortly+ after the heal reaches a terminal phase (with a 45s safety timeout).+- **Automatic wake** (real sleep resume, or `POST /wake/simulate`): icon only,+ no tooltip. Hovering the (i) icon shows it. An unrequested tooltip popping+ over the editor would be noise.++## The backend watchdog banner (catches what wake events miss)++Wake detection covers sleep resume, but a workspace can die without any wake+event (WSL wedge, code-server crash). The VS Code panel therefore polls+`/workspace/health` every 8 seconds for its whole life. Two consecutive misses+of `editor_will_load` show an amber banner over the editor: "Your workspace+stopped responding - this can happen after your laptop sleeps", with a+**Reconnect** button that runs the same heal cascade (`POST /wake/simulate`).+`editor_will_load` is honest: it is a real HTTP GET of code-server's+`/healthz` (success or redirect), not a bare TCP connect, because a wedged+code-server can still accept TCP while serving nothing.++For a full-stack diagnosis beyond wake issues, use the Doctor+(Adom menu > "Doctor...", or `GET/POST /doctor`, see `hydrogen-doctor`); its+"Fix problems" button runs this same cascade plus before/after evidence.++## Settings (Settings window > Desktop > Wake)++| Setting | Key | Default | Meaning |+|---|---|---|---|+| Repair connection after wake | `desktop.wake_heal` | on | Run the heal cascade on resume. Turning it off means wakes are only logged. |+| Show wake indicator | `desktop.wake_indicator` | off | Show the header indicator + info tooltip after a wake. |+| Indicator linger | `desktop.wake_indicator_linger` | 60s | Slider (0-600s, 15s steps): how long the indicator stays visible after a wake. |++If the user complains about reconnects, suggest turning the indicator ON so+they (and you) can watch what the heal is doing.++## Control API endpoints++On the Hydrogen control API (port from `~/.adom/hydrogen-control-url`, see `hydrogen-api`):++- `GET /wake/status` — full state: `last_sleep_at`, `last_wake_at`, `wake_kind`,+ current `phase`, `healing`, `heal_enabled`, and the recent log (capped 40+ lines). **Always read this first** when debugging a wake complaint.+- `POST /wake/simulate` — fires a fake wake event and runs the real heal+ cascade end-to-end. Safe to use for testing; takes ~20-60s on a healthy+ system.+- `POST /wake/config` `{"heal_enabled": true|false}` — toggle healing at+ runtime (the settings toggle calls this).++## What the heal actually does (so you can explain or debug it)++Three phases, visible in the `/wake/status` log:++1. **Backend** — polls workspace health up to 8 rounds x 12s. Restarts+ code-server at round 2 if still down. If a WSL wedge is suspected, runs+ `wsl --terminate Adom-Workspace` at round 4 and lets the distro boot fresh.+ A full `wsl --shutdown` is NEVER automatic (it would kill other distros);+ that remains a manual last resort.+2. **Token re-stamp** — `/var/run/adom/api-key` lives on tmpfs inside the+ distro, so a distro restart wipes it. Hydrogen re-writes it from the saved+ session so agents inside the workspace keep working.+3. **Pane** — checks the VS Code pane up to 3 x 15s for a stuck "Cannot+ reconnect" dialog and, only if stuck, reloads the editor iframe (pane-only+ reload, not the whole app).++## From inside the workspace (your perspective)++A laptop sleep looks to you like a frozen clock and a brief network blackout.+After a wake: your API key file may have just been re-stamped, and code-server+may have restarted underneath you. If the user says "it didn't come back":++1. `GET /wake/status` and read the log — did the heal run, and where did it+ stop?+2. If `heal_enabled` is false, ask the user if they want it on+ (`POST /wake/config`).+3. `POST /wake/simulate` reproduces the whole flow on demand.+4. If the heal reports failure repeatedly, the workspace may need a manual+ restart: Adom menu > Restart Container (which shows a live log of what it+ is doing).++## The reveal invariant (2026-08-06, the overnight grey-pane disaster)++The pane phase above got a hard rule after an overnight sleep left a dead grey+editor pane revealed all morning:++- **The editor pane may only be revealed after the iframe has (re)loaded+ against a backend CONFIRMED serving** (`/workspace/health` reports+ `editor_will_load`) **and painted.** A paint timeout alone proves nothing: a+ stale error page passes both a timeout and a healthy-backend check.+- **":8821 up" means the backend is alive, NOT that the iframe reloaded onto+ it.** If the iframe last loaded while the backend was dead, it is a stale+ corpse: force one reload onto the live backend before any reveal+ (the `reloadedAgainstHealthy` gate in PanelVisualStudioCode.svelte).+- While the backend is unreachable: keep the orbital splash up and hard-reload+ the iframe (about:blank then the URL) on a bounded cadence. Never reveal.+- The post-wake stuck-dialog reload waits up to ~60s for+ `editor_will_load` before reloading the pane (wake_heal.rs) so it cannot+ reload INTO a dead backend.++Testing rule (how round 1 shipped broken): **screenshot the RECOVERY frame,+not just the outage.** Kill code-server inside the distro, verify the splash+(outage frame), bring it back, and verify the editor self-reveals with real+content (recovery frame). Round 1 passed the outage and revealed a stale error+page on recovery; only the recovery screenshot caught it.
skills/hydrogen-workspace-monitoring/SKILL.md+87−79@@ -1,107 +1,111 @@ --- name: hydrogen-workspace-monitoring description: >- How Hydrogen monitors and manages the local WSL2 workspace distro- (`Adom-Workspace`) and code-server. Covers the 15s workspace state poll, the- two health levels (WSL2 available, distro Running + code-server reachable), the- floaty states (WSL not available, distro stopped), auto-start via setup_and_start,- auto-reload of the VS Code iframe, and the lifecycle dialog system. Read BEFORE- touching PanelVisualStudioCode.svelte, ContainerLifecycleDialog.svelte, or any- workspace state handling code. Trigger words: workspace stopped, wsl not available,- workspace monitoring, distro poll, workspace floaty, start workspace, code-server- reachable, lifecycle dialog, workspace state, Adom-Workspace running.+ How Hydrogen monitors and manages the local workspace and code-server.+ Covers the 15s workspace state poll, the two health levels (runtime available,+ workspace Running + code-server reachable), the floaty states, auto-start via+ setup_and_start, auto-reload of the VS Code iframe, and the lifecycle dialog+ system. Read BEFORE touching PanelVisualStudioCode.svelte,+ ContainerLifecycleDialog.svelte, or any workspace state handling code. Trigger+ words: workspace stopped, workspace monitoring, workspace poll, workspace floaty,+ start workspace, code-server reachable, lifecycle dialog, workspace state,+ workspace running. --- # Hydrogen Workspace Monitoring -> This is the WSL2-runtime version (default). The legacy Docker equivalent-> (`HD_RUNTIME=docker`) is in the docker/ bucket.+This is the platform-generic reference for how Hydrogen monitors workspace health.+The host-platform specifics (the runtime-unavailable floaty wording, the stop+command, the exact code-server reachability port, and the no-daemon framing)+live in your platform bootstrap's version of this skill. ## Architecture -Under the WSL2 runtime the workspace is the **`Adom-Workspace` distro** (imported via-`wsl --import`), not a Docker container. Hydrogen monitors workspace health at two levels:+The workspace is Hydrogen's local code-server-backed dev environment. Hydrogen monitors+workspace health at two levels: -1. **WSL2 available** — is WSL2 itself installed and responding on this machine?-2. **Workspace healthy** — is `Adom-Workspace` registered AND Running AND is- code-server reachable on host port **7380**?+1. **Runtime available** — is the workspace runtime itself installed and+ responding on this machine?+2. **Workspace healthy** — is the workspace present AND Running AND is+ code-server reachable on its host port? -The second level is a real reachability check, not just "is the distro registered."-A resume reality-check (after sleep, or after an Hydrogen rebuild/relaunch) requires-code-server to actually answer before Hydrogen trusts the workspace as healthy — a distro-can be `Running` while code-server is dead.+The second level is a real reachability check, not just "is the workspace+present." A resume reality-check (after sleep, or after an Hydrogen rebuild/relaunch)+requires code-server to actually answer before Hydrogen trusts the workspace as+healthy — a workspace can be Running while code-server is dead. -The VS Code panel (`PanelVisualStudioCode.svelte`) polls workspace state every 15s-and shows appropriate UI for each state. These frontend components survive unchanged-from the Docker era; they now front the WSL runtime via the runtime trait.+The VS Code panel (`PanelVisualStudioCode.svelte`) polls workspace state every+15s and shows appropriate UI for each state. These frontend components are+runtime-agnostic; they front the active runtime via the runtime trait. ## Workspace State Poll -`PanelVisualStudioCode.svelte` calls `get_container_status` (Tauri command) every 15s.-The command dispatches through the runtime trait to the WSL impl. Returns a status-struct shaped like:+`PanelVisualStudioCode.svelte` calls `get_container_status` (Tauri command)+every 15s. The command dispatches through the runtime trait to the active+runtime impl. Returns a status struct shaped like: ```rust pub struct ContainerStatus {- pub docker_available: bool, // under WSL: is WSL2 available?- pub image_pulled: bool, // is the Adom-Workspace tarball/distro present?- pub container_exists: bool, // is the distro registered (Running or Stopped)?- pub container_running: bool, // is it Running AND code-server reachable on :7380?+ pub docker_available: bool, // is the workspace runtime available?+ pub image_pulled: bool, // is the workspace image/tarball present?+ pub container_exists: bool, // is the workspace present (Running or Stopped)?+ pub container_running: bool, // is it Running AND code-server reachable? pub container_id: Option<String>, pub error: Option<String>, } ```-The field names are inherited from the Docker era; under WSL they map onto the two-health levels above (WSL availability, then distro-registered → distro-Running +-code-server-reachable).+The field names are inherited from the Docker era; under any runtime they map+onto the two health levels above (runtime availability, then+workspace-present → workspace-Running + code-server-reachable). ## Floaty States All floaties appear INSIDE the VS Code panel only — not as full-app modals.-They are draggable (grab header to move). The rest of the app (wiki tab, menu, etc.)-remains fully interactive. The old Docker floaties ("Docker Desktop not installed/-running") become WSL/distro states under this runtime.--### 1. WSL2 Not Available (red dot)-- **When**: WSL2 itself is not installed / not responding (level-1 health fails)-- **Message**: "WSL2 is required but not available."-- **Action**: "Run Setup Steps" button → dispatches `hydrogen-show-setup` event (which walks- the user through enabling WSL2)+They are draggable (grab header to move). The rest of the app (wiki tab, menu,+etc.) remains fully interactive.++### 1. Runtime Not Available (red dot)+- **When**: the workspace runtime itself is not installed / not responding+ (level-1 health fails)+- **Action**: "Run Setup Steps" button → dispatches `hydrogen-show-setup` event+ (which walks the user through enabling the runtime) - **Color**: Red dot in header +The exact runtime name and floaty message are platform-specific — see your+platform bootstrap's version of this skill.+ ### 2. Workspace Stopped / Not Ready (red dot)-- **When**: WSL2 is available but `Adom-Workspace` isn't Running, or it's Running but- code-server isn't reachable on :7380 (`container_running == false`)+- **When**: the runtime is available but the workspace isn't Running, or it's+ Running but code-server isn't reachable on its host port+ (`container_running == false`) - **Message**: "The Adom workspace is not running." - **Action**: "Start Workspace" button → calls `setup_and_start` Tauri command-- **Lifecycle dialog**: Shows the draggable lifecycle dialog with live setup output-- **Auto-reload**: After the distro + code-server come up, waits for code-server- health, then reloads the VS Code iframe--There is no separate "daemon not running" middle state like Docker had — WSL has no-always-on daemon to launch. Either WSL2 is available or it isn't (state 1), and the-workspace is either healthy or it isn't (state 2).+- **Lifecycle dialog**: Shows the draggable lifecycle dialog with live setup+ output+- **Auto-reload**: After the workspace + code-server come up, waits for+ code-server health, then reloads the VS Code iframe ## Auto-Start via `setup_and_start` -The "Start Workspace" path calls Tauri `setup_and_start`, which (in WSL mode)-delegates to `WslDistroRuntime::setup_and_start`:-- If the distro isn't present yet, `fresh_import` = download the golden- `adom-golden.tar.gz` (from the `adom-inc/hd-wsl2-image` release) + `wsl --import- Adom-Workspace` + start code-server.-- If the distro is present but code-server is down, it just (re-)starts code-server.+The "Start Workspace" path calls Tauri `setup_and_start`, which delegates to+the active runtime's `setup_and_start`:+- If the workspace isn't present yet, `fresh_import` = download the golden+ image + provision the workspace + start code-server.+- If the workspace is present but code-server is down, it just (re-)starts+ code-server. Importantly, code-server runs as an **Hydrogen-owned child process** (not a detached-`nohup`) — it dies with Hydrogen. So Hydrogen also **re-ensures code-server on every launch**-(look for `[wsl-startup] re-ensured code-server on launch` in the log). Without that,-a freshly relaunched Hydrogen would show the distro Running but the editor stuck on-"Starting AI Environment" forever, because the previous Hydrogen's code-server child was-orphaned. See [hydrogen-workspace-lifecycle](../hydrogen-workspace-lifecycle/SKILL.md) for the-full lifecycle detail.+`nohup`) — it dies with Hydrogen. So Hydrogen also **re-ensures code-server on every+launch** (look for `re-ensured code-server on launch` in the log). Without+that, a freshly relaunched Hydrogen would show the workspace Running but the editor+stuck on "Starting AI Environment" forever, because the previous Hydrogen's+code-server child was orphaned. See `hydrogen-workspace-lifecycle` for the full+lifecycle detail. ## Lifecycle Dialog -`ContainerLifecycleDialog.svelte` — a draggable floating card (no background blur)-that shows live command output for start/stop/restart operations on the distro.+`ContainerLifecycleDialog.svelte` — a draggable floating card (no background+blur) that shows live command output for start/stop/restart operations on the+workspace. ### Events Listens for `hydrogen-container-lifecycle` CustomEvents on `window`:@@ -119,34 +123,38 @@ Dynamic based on `actionType`: - On error: "Restart/Stop/Start Failed" ### VS Code Auto-Reload-After the `done` event, the dialog polls code-server health (`/proxy/8080/`) every 5s-for up to 2 minutes. When healthy, it reloads all iframes whose src points at-`localhost:7380` (the host-side code-server port), then shows "VS Code iframe reloaded"+After the `done` event, the dialog polls code-server health (`/proxy/8080/`)+every 5s for up to 2 minutes. When healthy, it reloads all iframes whose src+points at the host-side code-server port, then shows "VS Code iframe reloaded" and stops. ## Tauri Commands -| Command | What it does (WSL mode) |+| Command | What it does | |---|---|-| `get_container_status` | Returns workspace status — WSL available, distro present/registered, Running + code-server reachable |-| `setup_and_start` | `WslDistroRuntime::setup_and_start` — `fresh_import` (import distro) + start code-server, up to 10 min timeout |-| `stop_container` | Stops the workspace (`wsl --terminate Adom-Workspace`) |+| `get_container_status` | Returns workspace status — runtime available, workspace present, Running + code-server reachable |+| `setup_and_start` | `fresh_import` (provision workspace) + start code-server, up to 10 min timeout |+| `stop_container` | Stops the workspace | | `toggle_console` | Show/hide the debug console window | | `is_console_visible` | Returns true if console window exists | -There is no `launch_docker_desktop` equivalent under WSL — there's no daemon to start.+The exact stop command is platform-specific — see your platform bootstrap's+version of this skill. ## Menu Items (Adom Menu → Workspace Section)-- **Restart Workspace** — `wsl --terminate Adom-Workspace` + re-ensure code-server; emits lifecycle events, shows dialog-- **Stop Workspace** — terminates the distro; emits lifecycle events, shows dialog, triggers stopped floaty+- **Restart Workspace** — stop the workspace + re-ensure code-server; emits+ lifecycle events, shows dialog+- **Stop Workspace** — stops the workspace; emits lifecycle events, shows+ dialog, triggers stopped floaty - **Start Workspace** — available when stopped (from floaty button or menu) ## Settings (Desktop section in Settings dialog) - `desktop.show_console_on_startup` — boolean, default false-- `desktop.launch_on_boot` — boolean, default true (auto-start registry key)+- `desktop.launch_on_boot` — boolean, default true (auto-start on boot) ## Related skills -- [hydrogen-workspace-lifecycle](../hydrogen-workspace-lifecycle/SKILL.md) — import/export/unregister/terminate, the never-touch-global-WSL rule, control endpoints-- [hydrogen-container-stats](../hydrogen-container-stats/SKILL.md) — the title-bar CPU/RAM bars and `workspace_stats()` probe-- [hydrogen-networking](../hydrogen-networking/SKILL.md) — port 7380 code-server, control port 47084+- your platform bootstrap's `hydrogen-workspace-monitoring` — the runtime-specific floaty wording, stop command, reachability check, and no-daemon framing+- `hydrogen-workspace-lifecycle` — provision/export/remove/stop, the never-touch-global rule, control endpoints+- `hydrogen-container-stats` — the title-bar CPU/RAM bars and `workspace_stats()` probe+- `hydrogen-networking` — code-server port, control port