← Commit history
README.md+1−1
@@ -24,7 +24,7 @@ core stays generic across web + desktop Hydrogen.  ## What this layer adds -- **Dependencies:** `adom/hd-bootstrap` (generic HD) + `adom/adom-workspace-updater`+- **Dependencies:** `adom/hd-bootstrap` (generic HD) + `~~adom/adom-workspace-updater~~ (RETIRED 2026-07-16, dep removed in 0.2.3)`   (the in-distro systemd updater daemon — WSL2/systemd-specific). - **Bundled skills:** the WSL2-runtime `hd-*` skills (container, networking,   ports, port-watcher, setup, setup-steps, topology, volume, workspace-lifecycle,
docs/hero.htmladded+37
@@ -0,0 +1,37 @@+<!doctype html><html><head><meta charset="utf-8"><style>+  *{margin:0;box-sizing:border-box}+  html,body{width:1520px;height:950px;background:#0b0f14;font-family:ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif}+  .wrap{padding:46px 54px;height:100%;display:flex;flex-direction:column;gap:26px}+  .top{display:flex;align-items:baseline;gap:18px}+  .slug{font-family:ui-monospace,Menlo,monospace;font-size:38px;font-weight:700;color:#a78bfa;letter-spacing:-.5px}+  .badge{font-size:18px;color:#0b0f14;background:#a78bfa;border-radius:30px;padding:4px 16px;font-weight:700}+  .vp{font-size:25px;line-height:1.45;color:#e6edf3;max-width:1360px}+  .vp b{color:#c9b8ff}+  .term{flex:1;background:#0e141c;border:1px solid #1f2b38;border-radius:16px;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.5)}+  .bar{background:#121922;padding:13px 18px;display:flex;gap:9px;align-items:center;border-bottom:1px solid #1f2b38}+  .dot{width:13px;height:13px;border-radius:50%}.r{background:#f85149}.y{background:#f0b429}.g{background:#3fb950}+  .bar .t{margin-left:14px;color:#8b9bab;font-family:ui-monospace,monospace;font-size:16px}+  pre{margin:0;padding:24px 28px;font-family:ui-monospace,Menlo,monospace;font-size:19px;line-height:1.6;color:#cdd9e5}+  .p{color:#a78bfa}.c{color:#e6edf3;font-weight:600}.ok{color:#3fb950}.dim{color:#6b7d8f}.path{color:#f0b429}.s{color:#7fe9dd}.bin{color:#f0a847}+</style></head><body><div class="wrap">+  <div class="top"><span class="slug">adom/hd-windows-bootstrap</span><span class="badge">bootstrap · WSL2</span></div>+  <div class="vp"><b>The Windows layer for Hydrogen Desktop.</b> On top of <b>adom/hd-bootstrap</b> it adds the WSL2-runtime skills and the in-distro updater daemon — exactly what the golden WSL2 image installs so a fresh Windows workspace boots ready to run.</div>+  <div class="term">+    <div class="bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><span class="t">adom@Adom-Workspace: ~  (WSL2)</span></div>+<pre><span class="p">$</span> <span class="c">ADOMPKG_ALLOW_SUDO=1 adompkg install adom/hd-windows-bootstrap</span>+Resolving via wiki.adom.inc...+  <span class="s">adom/hd-bootstrap</span>            0.2.0    <span class="ok">✓</span>  <span class="dim">generic HD layer → pulls adom/core</span>+  <span class="s">adom/adom-workspace-updater</span>  0.1.11   <span class="ok">✓</span>  <span class="dim">systemd timer (needs sudo)</span>+  <span class="dim">+ 28 Windows skills (15 WSL2 + 13 -windows companions)   running postinstall…</span> <span class="ok">✓</span>++<span class="p">$</span> <span class="c">ls ~/.claude/skills/</span>            <span class="dim"># WSL2 skills + -windows companions</span>+<span class="path">hd-container/</span>   <span class="path">hd-networking/</span>  <span class="path">hd-ports/</span>       <span class="path">hd-topology/</span>+<span class="path">hd-container-stats/</span> <span class="path">hd-port-watcher/</span> <span class="path">hd-setup/</span>  <span class="path">hd-volume/</span>+<span class="path">hd-workspace-lifecycle/</span>  <span class="path">hd-workspace-monitoring/</span>  <span class="path">hd-setup-steps/</span>++<span class="p">$</span> <span class="c">ls /usr/local/bin /etc/systemd/system</span>   <span class="dim"># the daemon</span>+<span class="path">/usr/local/bin/</span>adom-workspace-updater+<span class="path">/etc/systemd/system/</span>adom-workspace-updater.<span class="bin">service</span>  …<span class="bin">.timer</span> <span class="ok">(enabled)</span>+</pre>+  </div>+</div></body></html>
package.jsonadded+25
@@ -0,0 +1,25 @@+{+  "schema_version": 1,+  "slug": "hd-windows-bootstrap",+  "type": "bootstrap",+  "title": "Hydrogen Desktop (Windows / WSL2)",+  "brief": "The Windows (WSL2) platform layer for Hydrogen Desktop.",+  "version": "0.2.3",+  "description": "The Windows platform layer for Hydrogen Desktop, where the workspace runs in a WSL2 Ubuntu distro. Depends on adom/hd-bootstrap (the platform-generic HD layer) and adds everything specific to the WSL2 runtime: the WSL2/networking/lifecycle hd-* skills, and the WSL2 code-server workbench seed (the in-distro workspace-updater daemon is RETIRED 2026-07-16 \u2014 updates are registry-native via adom-wiki pkg update / adom/hook). Sibling layers adom/hd-mac-bootstrap and adom/hd-ubuntu-bootstrap depend on the same adom/hd-bootstrap. This is the package the golden WSL2 image installs.",+  "org": "adom",+  "platform": "linux",+  "tags": [+    "bootstrap",+    "hydrogen-desktop",+    "hd",+    "windows",+    "wsl2"+  ],+  "needs_sudo": false,+  "scripts": {+    "postinstall": "./postinstall.sh"+  },+  "dependencies": {+    "adom/hd-bootstrap": "^0.2.0"+  }+}
page.json+19−6
@@ -3,18 +3,31 @@   "type": "bootstrap",   "slug": "hd-windows-bootstrap",   "title": "Hydrogen Desktop (Windows / WSL2)",-  "brief": "The Windows platform layer for Hydrogen Desktop, where the workspace runs in a WSL2 Ubuntu distro. Depends on adom/hd-bootstrap (the platform-generic HD layer) and adds everything specific to the WSL2",-  "version": "0.1.0",+  "brief": "The Windows (WSL2) platform layer for Hydrogen Desktop.",+  "version": "0.2.3",   "author": {     "name": "John Lauer",     "email": "[email protected]"   },-  "visibility": {-    "public": true-  },+  "visibility": "public",   "hero": {     "type": "image",     "path": "screenshots/hero.png"   },-  "org": "adom"+  "org": "adom",+  "description": "The Windows platform layer for Hydrogen Desktop, where the workspace runs in a WSL2 Ubuntu distro. Depends on adom/hd-bootstrap (the platform-generic HD layer) and adds everything specific to the WSL2 runtime: the WSL2/networking/lifecycle hd-* skills, and the WSL2 code-server workbench seed (the in-distro workspace-updater daemon is RETIRED 2026-07-16 — updates are registry-native via adom-wiki pkg update / adom/hook). Sibling layers adom/hd-mac-bootstrap and adom/hd-ubuntu-bootstrap depend on the same adom/hd-bootstrap. This is the package the golden WSL2 image installs.",+  "tags": [+    "bootstrap",+    "hydrogen-desktop",+    "hd",+    "windows",+    "wsl2"+  ],+  "needs_sudo": false,+  "scripts": {+    "postinstall": "./postinstall.sh"+  },+  "dependencies": {+    "adom/hd-bootstrap": "^0.2.0"+  } }\ No newline at end of file
postinstall.shadded+84
@@ -0,0 +1,84 @@+#!/usr/bin/env bash+# postinstall.sh — adom/hd-windows-bootstrap+#+# The Windows (WSL2) platform layer. Runs after adom/hd-bootstrap (generic) and+# all deps install. Applies ONLY what is specific to the WSL2 runtime:+#   - deploy the WSL2/networking/lifecycle hd-* skills (this package's skills/)+#   - seed the WSL2 code-server workbench.html (trusted domains + activity bar)+# The generic editor config (Claude CLI, extensions, settings.json) is in+# adom/hd-bootstrap. (The workspace-updater daemon dep was RETIRED 2026-07-16 —+# updates are registry-native: adom-wiki pkg update via adom/hook.) Mac/Ubuntu siblings provide their own platform layer instead.+#+# Idempotent + live-installable; the golden WSL2 image runs the same install.+set -euo pipefail+log() { echo "[hd-windows-bootstrap] $*"; }+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"+  install -d -m 0755 "$HOME/.claude/skills"+  count=0+  for d in "$HERE"/skills/hd-*/; do+    [ -f "${d}SKILL.md" ] || continue+    name="$(basename "$d")"+    install -d -m 0755 "$HOME/.claude/skills/${name}"+    install -m 0644 "${d}SKILL.md" "$HOME/.claude/skills/${name}/SKILL.md"+    count=$((count + 1))+  done+  log "  deployed ${count} WSL2 hd-* skills"+fi++# 1. workbench.html state seed — patches the WSL2 code-server install. Seeds+#    VS Code's per-origin IndexedDB/layout on every load: (1) trusted domains "*"+#    (suppress the 'open external website?' dialog), (2) unpin Search/SCM/Run-+#    and-Debug from the activity bar (once per profile, marker+#    adom.activityBarSeeded), (3) collapse the primary sidebar once per profile+#    (marker adom.sidebarSeeded) so the golden image opens to a CLEAN editor —+#    no Explorer, no tabs, no panel, no welcome (startupEditor:none in settings).+#    System-owned file → sudo (adom has NOPASSWD sudo).+WB=/usr/lib/code-server/lib/vscode/out/vs/code/browser/workbench/workbench.html+if [ -f "$WB" ] && ! grep -q __hdAbSeed "$WB"; then+  log "seeding WSL2 code-server workbench.html (trusted domains + activity bar + clean sidebar)"+  sudo python3 - "$WB" <<'PY'+import sys+wb = sys.argv[1]+html = open(wb).read()+SCRIPT = ('<script>(function(){try{var r=indexedDB.open("vscode-web-state-db-global",1);'+ 'r.onsuccess=function(e){var d=e.target.result;'+ 'try{var t=d.transaction("ItemTable","readwrite");t.objectStore("ItemTable").put(JSON.stringify(["*"]),"http.linkProtectionTrustedDomains")}catch(_){}'+ 'try{var t1=d.transaction("ItemTable","readonly");var os1=t1.objectStore("ItemTable");'+ 'var sg=os1.get("adom.activityBarSeeded");sg.onsuccess=function(){if(sg.result)return;'+ 'var pg=os1.get("workbench.activity.pinnedViewlets2");pg.onsuccess=function(){'+ 'var arr=[];try{if(pg.result)arr=JSON.parse(pg.result)}catch(_){}'+ 'var ids=["workbench.view.search","workbench.view.scm","workbench.view.debug"];'+ 'ids.forEach(function(id){var f=null;for(var i=0;i<arr.length;i++){if(arr[i].id===id)f=arr[i]}'+ 'if(f){f.pinned=false}else{arr.push({id:id,pinned:false,visible:false})}});'+ 'try{var t2=d.transaction("ItemTable","readwrite");var o2=t2.objectStore("ItemTable");'+ 'o2.put(JSON.stringify(arr),"workbench.activity.pinnedViewlets2");o2.put("1","adom.activityBarSeeded")}catch(_){}}}}catch(_){}};'+ 'window.__hdTrustedDomains=1;window.__hdAbSeed=1}catch(_){}})();</script>'+ # Collapse the primary sidebar ONCE per profile so the golden image opens clean+ # (no Explorer panel). Guarded by a localStorage marker so it won't fight the+ # user if they later open it. Sidebar visibility is workspace-scoped (not a+ # global IndexedDB key), so we drive VS Code's own toggle (Ctrl+B / keyCode 66)+ # after the workbench mounts rather than hand-crafting the grid serialization.+ '<script>(function(){try{'+ 'var fk="adom.sidebarSeeded:"+((new URLSearchParams(location.search)).get("folder")||"_");'+ 'if(localStorage.getItem(fk))return;'+ 'var n=0;var iv=setInterval(function(){n++;try{'+ 'if(document.querySelector(".monaco-workbench .part.editor")){'+ 'var sb=document.querySelector(".part.sidebar");'+ 'if(sb&&sb.getBoundingClientRect().width>3){'+ 'var e=new KeyboardEvent("keydown",{key:"b",code:"KeyB",ctrlKey:true,bubbles:true,cancelable:true});'+ 'Object.defineProperty(e,"keyCode",{get:function(){return 66}});'+ 'Object.defineProperty(e,"which",{get:function(){return 66}});'+ 'var t=document.querySelector(".monaco-workbench")||document.body;'+ '[window,document,t].forEach(function(x){try{x.dispatchEvent(e)}catch(_){}});}'+ 'localStorage.setItem(fk,"1");clearInterval(iv);}'+ 'else if(n>120){clearInterval(iv);}}catch(_){clearInterval(iv);}},500);}catch(_){}})();</script>')+if '__hdAbSeed' not in html:+    html = html.replace('</head>', SCRIPT + '</head>')+    open(wb, 'w').write(html)+PY+fi+log "done"
skills/hd-adom-auth-windows/SKILL.mdadded+46
@@ -0,0 +1,46 @@+---+name: hd-adom-auth-windows+description: >+  Windows/WSL2-specific details for Adom auth in HD — the exact %APPDATA%+  app-data path table for the session/replay/Claude credential files, and how+  to read the injected Adom API key from outside the WSL2 Adom-Workspace distro.+  READ alongside the generic [[hd-adom-auth]] skill when you need the concrete+  Windows file locations or the WSL2 cat command. Trigger words —+  %APPDATA%, hydrogen-session.txt, replay-session.txt,+  replay-claude-credentials.json, wsl Adom-Workspace, api-key path, windows+  auth files, where is the session token on windows.+---++# Hydrogen Desktop -- Auth (Windows / WSL2 specifics)++This is the Windows companion to the generic [[hd-adom-auth]] skill. Read that+first for the auth-intent flow, profile menu, login UI, and control-API auth+endpoints. This page only carries the Windows-specific file paths and the+WSL2-specific way to read the injected API key.++## Host app-data file table++On Windows, HD's app-data directory is `%APPDATA%\hydrogen-desktop\`. The+auth-related files the generic skill refers to live there:++| File | Path | Purpose |+|------|------|---------|+| Adom session token | `%APPDATA%\hydrogen-desktop\hydrogen-session.txt` | Plain-text Adom session cookie value. Read by the auth proxy; deleted on logout / 401. |+| Replay session copy | `%APPDATA%\hydrogen-desktop\replay-session.txt` | Replay copy of the session token; presence drives the "REPLAY" badge + countdown on the login page. |+| Claude credentials | `%APPDATA%\hydrogen-desktop\replay-claude-credentials.json` | Claude Code OAuth credentials replayed into the workspace during bootstrap. |++These are the same files the virgin-reset toggles operate on (see the generic+skill's "Virgin reset options for auth" table).++## Reading the injected API key from outside the WSL2 workspace++The `inject-api-key` setup step writes the Adom session token into the workspace+at `/var/run/adom/api-key` (mode 644). From inside the workspace you just+`cat /var/run/adom/api-key`. To read it from the Windows host (outside the+distro), go through `wsl`:++```bash+wsl -d Adom-Workspace -u adom -- cat /var/run/adom/api-key+```++(The default workspace distro is `Adom-Workspace`, user `adom`.)
skills/hd-adom-desktop-windows/SKILL.mdadded+82
@@ -0,0 +1,82 @@+---+name: hd-adom-desktop-windows+description: >+  Windows/WSL2-specific details for HD's reach onto the host via Adom Desktop —+  the C:\ / %USERPROFILE% / %APPDATA% paths, host control-port discovery via+  ports.json, the WSL2 0.0.0.0:8765 → host-loopback relay forwarding, the cmd /C+  shell, winget auto-install verbs (KiCad / Node), the wsl_exec --distro+  Adom-Workspace runner, and the taskkill warning. READ alongside the generic+  [[hd-adom-desktop]] skill when acting on a Windows host. Trigger words —+  windows host, WSL2, cmd /C, taskkill adom-desktop, ports.json, winget,+  desktop_install_kicad, desktop_install_node, wsl_exec, Adom-Workspace,+  mirrored networking, host loopback, %APPDATA%, C:\Users.+---++# Hydrogen Desktop ↔ Adom Desktop — Windows / WSL2 specifics++This is the Windows companion to the generic [[hd-adom-desktop]] skill. Read+that first for the HD↔AD capability map, dual-client model, relay verbs, and+capability verification. This page only carries the Windows/WSL2 specifics that+were stripped out of the generic version.++> **Windows-only:** On Windows, AD reaches the user's real **Windows** machine,+> and HD's host layer (screenshots, notifications, captions, window control) runs+> against Win32. The default workspace runs as a **WSL2 Ubuntu distro**+> (`Adom-Workspace`).++## Host paths++- Host shell commands run via `cmd /C` on Windows.+- `send_files` / `pull_file` use Windows paths, e.g.+  `C:\Users\<you>` (`%USERPROFILE%`) for the user's home, the Desktop folder+  under it, etc.++```bash+adom-desktop pull_file  '{"filePaths":["C:\\path\\file"],"saveTo":"/tmp"}'+adom-desktop send_files '{"filePaths":["/tmp/x"],"saveTo":"C:\\Users\\<you>"}'+```++## HD control-port discovery (ports.json)++`hd_api` hits HD's control API, discovering its real port from+`%APPDATA%\hydrogen-desktop\ports.json` key `control` (fallback **47084**).++## WSL2 relay forwarding++The in-workspace relay binds `0.0.0.0:8765` inside the distro (HTTP on `8766`),+and WSL2 forwards it to the host's `localhost:8765` / `localhost:8766`, which is+how the host-side clients (HD's built-in bridge + the embedded AD) reach your+in-workspace relay. It's reached **same-port** under WSL2 — there's no host+47081/47082 remap (that was the old Docker model). WSL2 mirrored networking also+shares loopback with the Windows host, so `http://127.0.0.1:<port>` is the same+address from inside the distro and on the host.++## Auto-installing missing apps (winget)++On Windows you don't have to stop at a download link — trigger an unattended+winget install:++```bash+adom-desktop desktop_install_kicad '{}'   # winget install KiCad+adom-desktop desktop_install_node  '{}'   # winget install Node.js (unblocks the Pup bridge)+```++(`install_kicad` fires when `kicad_list_versions` returns+`errorCode:"kicad_not_installed"`; `install_node` when a `browser_*` call returns+`errorCode:"node_not_found"`.)++## Structured runner: wsl_exec++To run a script *inside* the WSL2 workspace distro with no quoting hazard:++```bash+adom-desktop wsl_exec '{"distro":"Adom-Workspace","user":"adom","scriptB64":"<b64>"}'+```++Base64 the script body so no quoting survives to be mangled.++## What NOT to do (Windows)++- **Never `taskkill /F /IM adom-desktop.exe`** — the host AD is your relay+  bridge; killing it severs your control channel (you can't get it back from in+  here).
skills/hd-api-windows/SKILL.mdadded+104
@@ -0,0 +1,104 @@+---+name: hd-api-windows+description: >+  Windows/WSL2-specific Hydrogen Desktop control-API surface — the /wsl/*+  runtime endpoints, the legacy /docker/* (HD_RUNTIME=docker) endpoints,+  /volume-delete, /system/* host-reboot ("Reboot Windows"), the+  /test/probe-dialogs UAC/MSI/Docker-EULA blocker catalog, WebView2 CDP/DevTools+  notes, host-side port discovery (HD log + %APPDATA%\hydrogen-desktop\ports.json),+  and the WSL2 mirrored-networking loopback note. READ alongside the generic+  [[hd-api]] skill on a Windows host. Trigger words — wsl status, docker status,+  reboot windows, system reboot, probe-dialogs, UAC dialog, MSI installer,+  Docker EULA, mirrored networking, ports.json, volume-delete, WebView2,+  terminal-profile, virgin-reset-progress.+---++# HD Control API — Windows / WSL2 specifics++This is the Windows companion to the generic [[hd-api]] skill. Read that first+for the platform-neutral endpoint groups (health, claude, setup, tabs, eval,+ui). This page carries the runtime-specific and host-specific endpoints that+were stripped out of the generic version.++## Host-side port discovery++In addition to the in-workspace `~/.adom/hd-control-url` discovery file, you can+find the dynamic control port from the Windows host:++```bash+# From HD log+adom-desktop hd_log '{"tail":100}' | grep "control="++# From ports.json+type %APPDATA%\hydrogen-desktop\ports.json+```++**Loopback note:** WSL2 mirrored networking shares loopback with the Windows+host, so `http://127.0.0.1:<port>` is the same address from inside the+`Adom-Workspace` distro and on the Windows host. `/ports/all` reports+mirrored-mode info per port.++## WSL runtime endpoints (default runtime)++When HD runs the workspace as a WSL2 distro, it additionally exposes:++| 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.++## Docker Desktop (legacy Docker runtime only — `HD_RUNTIME=docker`)++This `/docker/*` table applies only when HD is running the legacy Docker runtime.++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/docker/status` | — | Docker Desktop running state |+| GET | `/docker/version` | — | Docker engine version |+| GET | `/docker/containers` | — | List all Docker containers |+| GET | `/docker/images` | — | List all Docker images |+| POST | `/docker/start` | — | Launch Docker Desktop + start container |+| POST | `/docker/stop` | — | Stop Docker Desktop |+| POST | `/docker/restart` | — | Restart Docker Desktop |+| POST | `/docker/wait-ready` | — | Block until Docker daemon responds |+| POST | `/docker/install` | — | Install Docker Desktop (downloads ~730 MB) |+| POST | `/docker/uninstall` | — | Uninstall Docker Desktop |+| POST | `/docker/clean-reinstall` | — | Uninstall + reinstall Docker Desktop |+| GET | `/docker/install-progress` | — | Current install step + progress |++Also Docker-runtime only:++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/volume-delete` | — | Remove project volume (DESTRUCTIVE). No volume exists under WSL. |++## System (host OS reboot)++| 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 |++## WebView & VS Code (Windows specifics)++HD's webview is **WebView2** on Windows; `/cdp-eval`, `/cdp-info`, and+`/devtools` target the WebView2 DevTools (Chrome DevTools Protocol) endpoint.++## Diagnostics — dialog blocker catalog++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/test/probe-dialogs` | — | Enumerate visible windows, classify against the cascade-blocker catalog (UAC, WSL2 update, MSI installer, Docker EULA, …); returns `kind` + `is_blocking` per window |++(Gated behind `HD_TEST_API_ENABLED=1`, like the rest of `/test/*`.)
skills/hd-browser-picker/SKILL.mdadded+474
@@ -0,0 +1,474 @@+---+name: hd-browser-picker+description: HD's Browser Picker — the canonical handler for EVERY URL that gets opened inside HD. Click a link anywhere (main page, code-server iframe, webview panel, PortMappingsDialog, OAuth redirect, localhost link, external link) and HD 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, HD 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 HD. Trigger words — browser picker, BrowserProfileDialog, hd-open-url, hd-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, hd-links (legacy name).+---++# Hydrogen Desktop — Browser Picker (URL Interception)++## The Problem++HD wraps a WebView2 that contains:+- The Hydrogen SvelteKit frontend (localhost:1420)+- A code-server iframe (localhost:8080) with VS Code + Claude Code+- Webview panel iframes (any external URL like wiki, google, etc.)++When a link is clicked anywhere in this stack, HD 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, HD Browser Window, or Pup++## The 5 Interception Layers++Each layer catches a different type of URL event. All registered in `setup_frame_navigation_interception()` in `hd-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 `hd-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 `hd-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 Desktop" (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 Desktop 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 HD 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('hd-open-url', {+                detail: JSON.stringify({+                    url: href,+                    trigger: 'target_blank_click',+                    shiftHeld: e.shiftKey+                })+            }));+        } catch(err) {+            window.parent.postMessage({+                type: 'hd-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+Original code returned `Allow` for localhost URLs. Code-server's `openExternal` wraps URLs in localhost routes. Returning `Allow` created uncontrolled WebView2 windows with no handlers. Fix: Deny everything, always.++### 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/hd-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/hd-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 `hd-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 HD 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/hd-app/capabilities/default.json` (the hd-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+#    HD 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+adom-desktop shell_execute '{"command":"curl -s -X POST -d {\"url\":\"https://example.com\"} http://127.0.0.1:47084/open-url"}'++# 3. Within 5s, picker auto-routes to Pup (or saved pref)+# 4. Drive Pup with adom-desktop browser_* verbs+adom-desktop browser_eval '{"sessionId":"default","js":"document.title"}'++# Force a picker dialog (no auto-timer)+adom-desktop shell_execute '{"command":"curl -s -X POST -d {\"url\":\"https://example.com\",\"force\":true} http://127.0.0.1:47084/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-desktop 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-desktop 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-desktop 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` | `webview-window` | `pup-window`, or+`browser:<browser>:<dir>` (profile dir encoded in the ID). `fresh` is per-open, not stored.+Writes hit the Manager's live state (localStorage + `hd-url-routing-prefs-changed`). Full+routing-policy guidance lives in **[hd-open-url](../hd-open-url/SKILL.md)**.++## How HD skills get deployed (workflow note)++HD public skills (this one, hd-open-url, hd-monitor, etc.) live in the HD repo under `skills/public-facing/{shared,wsl2,docker}/<name>/SKILL.md`. They're deployed into the user's workspace by the `install-hd-skills` setup step, which is runtime-aware: it installs `shared/` plus the active runtime's bucket (`wsl2/` by default via the `/mnt/c` auto-mount; `docker/` via `docker cp` under `HD_RUNTIME=docker`), flat into `~/.claude/skills/<name>/`. See `hd-skill-catalog` (public) and `hd-skills-structure` (dev-internal) for the full layout.
skills/hd-captions-windows/SKILL.mdadded+92
@@ -0,0 +1,92 @@+---+name: hd-captions-windows+description: >+  Windows-specific companion to [[hd-captions]]. The full-DESKTOP overlay caption on+  Windows — `adom-desktop desktop_caption` — HD's built-in Win32 always-on-top,+  click-through overlay that floats over ALL windows (above KiCad, Fusion, a browser, the+  whole screen) and is captured by a full-screen/desktop recording. Covers the+  desktop-overlay args (text, id, position top/center/bottom/corners, normalized x/y,+  size/fontSize, duration in MILLISECONDS, action: hide|force-clear) and the id rule (same+  id replaces, different ids coexist). For the cross-platform WORKSPACE caption+  (`adom-cli hydrogen caption`, SECONDS units), see [[hd-captions]]. Windows-only.+  Trigger words — desktop caption, desktop_caption, screen overlay, always on top text,+  click-through overlay, corner caption, caption id, caption position corners, caption x y,+  force-clear captions, full-screen caption, caption over other apps, win32 caption overlay.+---++# Hydrogen Desktop — Desktop Overlay Caption (Windows)++Windows-specific companion to **[[hd-captions]]**. The generic skill covers the+cross-platform **workspace** caption (`adom-cli hydrogen caption`, SECONDS units, rendered+inside the HD webview). This page covers the **full-desktop overlay** caption, which is+Windows-only.++## Desktop overlay caption — `adom-desktop desktop_caption`++HD's built-in **Win32 always-on-top, click-through overlay** — floats over every window,+doesn't steal focus or intercept clicks (the user works "through" it). The full-featured+surface (ids, corners, x/y, force-clear). It's captured by a **full-screen / desktop**+recording (it's real screen pixels), but NOT by a workspace/panel webview capture (it's a+separate OS window). **Windows-only** (non-Windows no-ops).++## Firing a caption++Source: `src-tauri/crates/hd-app/src/caption.rs` (`handle_caption`). The AD CLI verb is+**`desktop_caption`** (the internal message type is `caption`; `adom-desktop status` lists+the `caption` capability). Plain `adom-desktop caption` is NOT a verb — it errors.++```bash+adom-desktop desktop_caption '{"text":"Step 3: approve the UAC dialog →","position":"center","size":"large","duration":6000}'+```++### Arguments++| Arg | Type | Meaning |+|---|---|---|+| `text` | string | The caption text. Omit (with no `action`) to hide. |+| `id` | string | Caption identity. **Same id → replaces** that caption; **different ids → coexist.** Defaults to `_default`. |+| `position` | string | `top`, `bottom` (default), `center`, `top-left`, `top-right`, `bottom-left`, `bottom-right`. |+| `x` / `y` | float | **Normalized 0.0–1.0** screen coordinates (clamped). Use for precise placement instead of a named `position`. |+| `size` | string | `large` / `medium` (default) / `small`. |+| `fontSize` | int | Explicit pixel font height (also accepts `font_size`). Overrides `size`. |+| `duration` | int | **Milliseconds** on screen (default 4000), then a short fade-out. ⚠️ **NOTE: this surface is ms — the OPPOSITE of the workspace caption in [[hd-captions]], whose `-d` is SECONDS.** `4000` here = 4s; `4000` there = 66 min. |+| `action` | string | `hide` (dismiss this id, or all if id empty) or `force-clear` (destroy **all** captions regardless of id). |++### The id rule (this is the powerful part)++Captions are keyed by `id`. **Reuse an id to update a caption in place**; **use different+ids to show several at once.** That lets you run, e.g., a persistent status caption in a+corner *and* a moving center callout simultaneously:++```bash+# persistent status in the corner+adom-desktop desktop_caption '{"id":"status","text":"Recording…","position":"top-right","size":"small","duration":60000}'++# step callouts in the center, each replacing the last (same id)+adom-desktop desktop_caption '{"id":"step","text":"Step 1: open the board","position":"center","duration":4000}'+adom-desktop desktop_caption '{"id":"step","text":"Step 2: run DRC","position":"center","duration":4000}'++# clear just the step callout+adom-desktop desktop_caption '{"id":"step","action":"hide"}'++# nuke everything+adom-desktop desktop_caption '{"action":"force-clear"}'+```++## Patterns++- **Walkthrough / demo callouts over other apps** — drop a `center` caption per step (same+  `id` so each replaces the last). Combine with a full-screen recording so the callouts end+  up in the finished video. Pair with TTS for narration.+- **Status corner** — a long-`duration` small caption in a corner (`top-right`), separate+  `id`, while you drive the rest of the screen.+- **Point at a thing** — use normalized `x`/`y` to sit a label next to a button or dialog+  you're about to act on.+- **Cleanup** — `action:"hide"` per id when a step is done; `action:"force-clear"` to wipe+  the screen of all captions at the end of a tour.++## Related skills+- [[hd-captions]] — the cross-platform workspace caption (`adom-cli hydrogen caption`, SECONDS units)+- [hd-recording](../hd-recording/SKILL.md) — captions are captured *into* screen recordings+- [hd-adom-desktop](../../hd-bootstrap/skills/hd-adom-desktop/SKILL.md) — AD carries the `caption` / `desktop_caption` verb to the host; verify the `caption` capability via `adom-desktop status`+- `adom-tts` — *audio* narration to pair with on-screen captions (captions are visual-only)
skills/hd-container-stats-windows/SKILL.mdadded+56
@@ -0,0 +1,56 @@+---+name: hd-container-stats-windows+description: Windows/WSL2-specific mechanism behind HD's title-bar CPU/RAM "Container" indicator — the `workspace_stats()` WSL health/stat probe of the `Adom-Workspace` distro, `wsl --terminate` as the "restart workspace" action, the benign `[docker] unhealthy` log line under WSL, the `[wsl]` real health signal, and WSL distro disk semantics. Companion to [[hd-container-stats]]. Trigger on workspace_stats WSL probe, wsl --terminate, [docker] unhealthy log, [wsl] unhealthy, WslDistroRuntime get_stats, distro disk, restart needed after Windows wake.+---++# HD Workspace Stats Indicator — Windows / WSL2 specifics++Companion to **[[hd-container-stats]]**. The bars, thresholds, polling cadence, tooltip fields, badges, CSS selectors, and control-API shape are all in the generic skill. This skill covers how the stats are actually gathered on Windows.++## The backend probe++Under the WSL2 runtime, `invoke('get_container_stats')` → `WorkspaceRuntime::get_stats` → `WslDistroRuntime::get_stats` → `workspace_stats()`, a WSL health/stat probe of the `Adom-Workspace` distro.++There is **NO `docker exec`, NO cgroup file reads, NO bollard `stats()`** — those were the Docker-runtime mechanism and do not run under WSL. (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.++`workspace_stats()` checks two health levels: (1) is WSL2 itself available; (2) is `Adom-Workspace` registered + Running with code-server reachable on host port 7380. If either fails, it returns a needs-restart signal and the UI shows the red "restart needed" badge.++The tooltip's source values under WSL: CPU/RAM come from the `workspace_stats()` WSL probe; `Image` reads `Ubuntu 24.04 (Adom-Workspace)`; `ID` and `Container` are the distro name `Adom-Workspace`.++## "Restart Workspace" = `wsl --terminate`, not `docker restart`++When `restart needed` shows, Adom menu → Restart Workspace runs `wsl --terminate Adom-Workspace` + re-ensure code-server — NOT `docker restart`.++The trigger is almost always a WSL2 hiccup after Windows wake/sleep, leaving the distro or code-server unreachable.++## The benign `[docker] unhealthy` log line++You will sometimes see this in HD'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 hd-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. The authoritative "which runtime am I on" line is `[app] Active workspace runtime: WslDistro`.++## Distro disk semantics++Under WSL there is no separate Docker volume; `/home/adom/project` lives inside the distro, so the `Disk:` figure in the tooltip is the distro's on-disk size including your project files.++## Common questions (WSL detail)++**"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.++## Related skills++- `[[hd-container-stats]]` — the platform-generic indicator this companion extends+- `[[hd-workspace-monitoring-windows]]` — distro + code-server health monitoring+- `[[hd-networking]]` — port 7380 code-server, host reachability
skills/hd-container-windows/SKILL.mdadded+64
@@ -0,0 +1,64 @@+---+name: hd-container-windows+description: Windows/WSL2-specific spine for the HD workspace — the `Adom-Workspace` WSL2 distro, how it's imported (`wsl --import` of the golden image), the distro user/exec model, code-server port 7380 auto-forward to Windows localhost, the Rust runtime source, and the Cloud-vs-WSL2 comparison table. Companion to [[hd-container]]. Trigger on Windows host, WSL2, Adom-Workspace distro, wsl --import, wsl exec, distro user, code-server 7380 forward, mirrored networking, golden image distro.+---++# Hydrogen Desktop Workspace — Windows / WSL2 specifics++Companion to **[[hd-container]]**. On Windows, HD hosts the workspace as a **WSL2 Ubuntu distro named `Adom-Workspace`**. Everything in the generic skill (Ubuntu 24.04, code-server, the relay, bridges, env vars, the "setup-steps not bootstrap.sh" rule) applies; this skill adds the WSL2-specific facts.++## The distro — 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` via `wsl --import` |+| Architecture | matches host — almost always `linux-x64` on Windows hosts, `linux-arm64` on Apple Silicon (when WSL2 is unavailable, macOS/Ubuntu hosts use their own platform layer) |+| 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 UID | `adom` is UID 1001 |+| Workspace location | `/home/adom/project` lives directly in the distro's ext4 filesystem (no separate Docker volume) |+| Runtime source | `hydrogen-desktop/src-tauri/crates/hd-app/src/runtime/wsl.rs` (selector `runtime/mod.rs::select_runtime()`) |++## How it's provisioned (vs the legacy Docker path)++HD provisions the distro through `setup_steps_wsl.rs` (runtime mechanics in `runtime/wsl.rs`). The WSL cascade is **16 steps** (state file `setup-steps-wsl.json`) vs the legacy Docker flow's 24.++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. (For the legacy Docker `setup_steps.rs` flow see the docker bucket.)++When debugging install issues on Windows, the source of truth is `setup_steps_wsl.rs` (and `runtime/wsl.rs`).++## code-server port auto-forward++Code-server runs inside the distro on `0.0.0.0:7380`; **WSL2 auto-forwards 7380 to Windows `localhost:7380`**, which is how the host webview reaches it.++## Host control API reachability++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; read the live URL from `~/.adom/hd-control-url`.++## Host file paths in bridge commands++KiCad/Fusion/file-transfer bridge commands that reference host files use **Windows path convention** (e.g. `C:\\Users\\john\\board.kicad_pcb`, with escaped backslashes in JSON).++## WSL2-specific environment variable++- `HD_RUNTIME=wsl2` — indicates the WSL2 runtime (default on Windows); `docker` selects the legacy container runtime.++## What's different from Adom cloud containers (WSL2 detail)++| Feature | Adom Cloud | HD Local (WSL2) |+|---------|-----------|-----------------|+| Workspace provisioning | Adom platform API | `wsl --import` of `adom-golden.tar.gz` 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}}/ |+| Socket.IO (collab) | Connected to iron.adom.inc | Disabled (single-user) |+| Service containers | Adjacent containers | Remote via Adom cloud |++## Golden-image specifics++The distro is imported from a full pre-baked image — gallia, the Adom CLIs, the claude CLI + Code extension, code-server (pinned), VS Code settings, and all `hd-*` skills are baked in at image-build time, NOT installed by setup steps.++## Related skills++- `[[hd-container]]` — the platform-generic workspace context this companion extends+- `[[hd-container-stats-windows]]` — the WSL probe behind the title-bar CPU/RAM indicator
skills/hd-desktop-sse-windows/SKILL.mdadded+66
@@ -0,0 +1,66 @@+---+name: hd-desktop-sse-windows+description: Windows/WSL2-specific details for HD's local workspace API + SSE — mirrored-networking loopback sharing, the ports.json location under %APPDATA%, the HD_RUNTIME=docker vs WSL2 relay same-port-vs-host-mapping behavior, and hd-docker create_container env injection. Read alongside the platform-generic [[hd-desktop-sse]]. Trigger words — WSL2 SSE, mirrored networking, ports.json APPDATA, HD_RUNTIME docker, Adom-Workspace distro relay, same-port loopback.+---++# Hydrogen Desktop — Local Workspace API + SSE (Windows / WSL2 specifics)++Platform companion to **[[hd-desktop-sse]]**. Everything here is Windows/WSL2-only; the+architecture, routing, env-var contract, REST field shapes, and the fixed discovery port+(47080) all live in the generic skill.++## The workspace runtime on Windows++On Windows the workspace is the **WSL2 `Adom-Workspace` distro** by default, or a **Docker+container under `HD_RUNTIME=docker`** (legacy). The workspace's adom-cli is what controls HD's+UI via the local workspace API.++## Mirrored networking shares loopback with the Windows host++Under WSL2 with **mirrored networking**, `127.0.0.1` is shared between the distro and the+Windows host. That means:++- From inside the `Adom-Workspace` distro, `http://127.0.0.1:47083` reaches HD's proxy running+  on the Windows host — no special host IP, no `/proxy/` hop needed for the host-loopback ports.+- The same `127.0.0.1:<port>` works identically from inside the distro and on Windows.+- The control URL HD writes to `~/.adom/hd-control-url` (`http://127.0.0.1:<dynamic>`) is the+  Windows-host loopback, reachable from the distro because of mirrored loopback.++## Relay ports: same-port under WSL2 (no host 47081/47082)++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** under WSL2.++- Clients discover the relay ports from `~/.adom/cli-relay-ports.json` (fallback 8765/8766),+  NOT from `/discover`.+- Contrast with the Docker runtime, where the relay's container ports (8765/8766) are+  host-mapped to 47081/47082. The same-port-vs-host-mapped distinction is the WSL2-vs-Docker+  difference to keep straight.++For reference, the relay-port rows in the generic PortConfig table resolve like this on Windows:++| Service | WSL2 (default) | Docker runtime | Workspace Port |+|---|---|---|---|+| Relay WS | same-port (no host listener) | host 47081 | 8765 |+| Relay HTTP | same-port (no host listener) | host 47082 | 8766 |++## ports.json location++Host-side port config is stored at `%APPDATA%\hydrogen-desktop\ports.json`. On first launch+defaults are written without scanning; PortConfig auto-resolves conflicts on startup. Only+host-side ports are dynamic — workspace-side ports (8080, 8765, 8766) are always fixed.++## Env-var injection under the Docker runtime++Under `HD_RUNTIME=docker`, the workspace env vars from the generic skill+(`VSCODE_PROXY_URI`, `ADOM_CARBON_URL`, `ADOM_HYDROGEN_URL`, `ADOM_DESKTOP_MODE`) are set in+`create_container()` in `hd-docker/src/lib.rs` when the container is created. Under WSL2 the+same contract is applied to the distro environment instead.++## Files++| File | What |+|---|---|+| `src-tauri/crates/hd-docker/src/lib.rs` | `create_container()` — container env vars under the Docker runtime |+| `src-tauri/crates/hd-control/src/ports.rs` | PortConfig load/save against `%APPDATA%\hydrogen-desktop\ports.json` |
skills/hd-eda-discovery-windows/SKILL.mdadded+44
@@ -0,0 +1,44 @@+---+name: hd-eda-discovery-windows+description: >+  Windows-specific EDA detection probe for the post-setup discovery pass — how to find+  no-bridge EDA tools (Altium, OrCAD, Cadence/Allegro, EAGLE, DipTrace, Eplan) by scanning+  C:\Program Files / C:\Program Files (x86) and Get-Process via a PowerShell run_script.+  Read alongside the platform-generic [[hd-eda-discovery]]. Trigger words — detect altium+  windows, scan program files, powershell eda probe, Get-Process altium, orcad install path,+  run_script eda detect.+---++# EDA discovery — Windows no-bridge probe++Platform companion to **[[hd-eda-discovery]]**. The bridge-based detection+(`kicad_list_versions`, `fusion_status`) and the entire branching/onboarding/pitch logic live+in the generic skill. This file is ONLY the Windows recipe for the tools that have **no bridge**+and must be detected by install path / running process.++## The probe++Base64-encode the PowerShell and run it through `adom-desktop run_script` (base64 avoids all the+shell-quoting pain). Use absolute paths.++```bash+adom-desktop run_script '{"interpreter":"powershell","scriptB64":"<b64 of:>"}'+#   Get-ChildItem 'C:\Program Files','C:\Program Files (x86)' -Directory -EA 0 |+#     ? { $_.Name -match 'Altium|OrCAD|Cadence|Allegro|EAGLE|DipTrace|Eplan' } | % Name+#   plus: Get-Process | ? { $_.Name -match 'Altium|OrCAD|Capture|Allegro' } | % Name+```++## Notes++- **Base64 the PowerShell** for `run_script` — no quoting survives raw.+- **Use absolute paths** (`C:\Program Files`, `C:\Program Files (x86)`).+- **Treat empty output as a timeout, NOT a negative result** — the relay can return empty on a+  slow/timed-out probe, which is not the same as "tool not installed."+- The `Get-ChildItem` scan catches installed-but-not-running tools; the `Get-Process` pass+  catches anything currently open (and tools installed somewhere unusual).++## Equivalent on other platforms (for orientation)++The generic skill covers this, repeated here only so you know what the Windows probe maps to:+- **macOS** would scan `/Applications` (and `~/Applications`) for the app bundles, plus `pgrep`.+- **Linux** would `which`/`command -v` the CLI binaries and check common install dirs, plus `pgrep`.
skills/hd-embedded-ad/SKILL.mdadded+141
@@ -0,0 +1,141 @@+---+name: hd-embedded-ad+description: How Hydrogen Desktop (HD) and Adom Desktop (AD) interact via "embedded mode". Use whenever the user asks about the system tray icon, why AD isn't visible, why two desktop apps are installed, the "Embedded · HD" footer pill, or how to make AD show up standalone. Trigger words — embedded mode, system tray, AD tray, why is adom desktop hidden, where's my tray icon, HD vs AD, both apps installed, why two installs, ad missing from tray, switch AD to standalone, AD won't show, run AD without HD, decouple AD from HD.+---++# HD ↔ AD Embedded Mode++There are two desktop apps in the Adom ecosystem: **Hydrogen Desktop (HD)** — the user-facing workspace app — and **Adom Desktop (AD)** — the bridge that talks to local apps (KiCad / Fusion 360 / Chrome / files). HD's installer silently bundles AD and runs it in **embedded mode**, where AD is invisible and HD's tray menu mirrors AD's controls.++This skill explains how the two interact so you can answer "why isn't AD in my tray?", "should I have two installs?", "why did the tray icon disappear?" and similar.++## Two modes++### Standalone AD (no HD anywhere)++How AD launches when nobody owns it:+- Tray icon visible in Windows notification area+- Main window opens on launch (unless `--start-hidden`)+- Autostart shortcut in `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\AdomDesktop.lnk` → launches on every boot+- Registered in Add/Remove Programs (HKLM `Uninstall\AdomDesktop`)+- Auto-update polls `wiki-ufypy5dpx93o.adom.cloud/static/apps/adom-desktop/version.json` every 4h+- Footer shows the user's email + a "Sign in" / "Sign out" link++This is what AD looked like before HD existed and what it reverts to when HD vanishes.++### Embedded AD (HD owns it)++HD's installer puts AD into embedded mode by:+1. Writing `%LOCALAPPDATA%\Adom Desktop\embedded.json` marker (`{"owner":"hydrogen-desktop", ...}`)+2. Deleting AD's Desktop / Start Menu / Startup `.lnk` shortcuts so the user can't double-launch standalone AD+3. Configuring HD's own systray to mirror AD's menu (Open AD, Connect All, Disconnect All, Settings, etc.)++When HD spawns AD with `--embedded --start-hidden --relay-url ws://127.0.0.1:8765 --relay-name hydrogen-desktop`, AD detects embedded mode and:+- **Hides** its system tray (HD owns the one tray)+- **Suppresses** its autostart shortcut creation+- **Disables** its auto-updater (HD owns AD's update channel — fresh AD bundled in each HD release)+- **Hides** its main window on boot (HD's tray "Open Adom Desktop" menu item opens it on demand)+- **Skips** HKLM Uninstall registration (HD's installer offers cascade-uninstall of AD)+- Footer shows a read-only email + an "Embedded · HD" pill++Result: the user only ever sees one app (HD) in the tray + Start Menu, even though two processes (HD + AD) are running.++## How AD detects "am I embedded?" — 3-signal cascade++AD calls `embedded::detect_embedded_mode()` at boot. First match wins:++1. **CLI flag**: `--embedded` (or `--embedded-mode`) on the command line. Writes the marker file, owner defaults to `hydrogen-desktop`. **This is what HD spawns AD with.**+2. **Env var**: `ADOM_EMBEDDED=1` (or `true`). Same behavior as the flag.+3. **Marker file**: `%LOCALAPPDATA%\Adom Desktop\embedded.json` exists. AD then probes HD's **liveness beacon** on `127.0.0.1:9001` (a real port AD listens for — NOT HD's control API; HD's actual control API is **47084**):+   - If HD answers the beacon → embedded mode+   - If HD doesn't answer → AD treats the marker as stale, deletes it, falls back to standalone++If none of the three signals fire → standalone.++## Lifecycle ownership in embedded mode++HD's runtime supervisor:+- Reads HD's session token from `%APPDATA%\hydrogen-desktop\hydrogen-session.txt`+- Spawns AD detached with the embedded flags above+- Polls `http://127.0.0.1:47200/health` every 5s+- If AD's health probe fails → HD respawns AD with the same args++HD's tray menu (mirrors AD's controls so the user gets all the AD verbs without seeing AD's tray):+```+Open Hydrogen Desktop+Reload Workspace+── Adom Desktop ──+Open Adom Desktop      → POST 127.0.0.1:47200 desktop/window_show+Connect All            → POST desktop/connect_all+Disconnect All         → POST desktop/disconnect_all+Settings (Adom Desktop)→ POST desktop/window_show+─────+Quit                   → POST desktop/shutdown, 200ms grace, then app.exit(0)+```++HD's uninstaller PREUNINSTALL hook prompts "Also remove Adom Desktop?" → cascade-uninstalls AD on Yes.++## Runtime transitions++**AD ≥ v1.8.47** re-checks HD's liveness every 60s during runtime, not just at boot. This is the normal daily cycle for most users, not an edge case:++- Morning: user launches HD → HD spawns AD with `--embedded` → AD's tray hides, pill appears+- Evening: user quits HD → AD's next poll fails the HD probe → AD reverts to standalone → tray reappears+- Next morning: user launches HD again → AD's next poll succeeds → AD re-embeds → tray hides++The same AD process can flip mode back and forth indefinitely across an HD-quit / HD-relaunch cycle. Transitions:++| Current mode | HD probe says | What AD does |+|---|---|---|+| Standalone | dead | stay standalone (no-op) |+| Standalone | alive | → **embed**: hide tray, write marker, hide main window, show "Embedded · HD" pill |+| Embedded | dead | → **unembed**: show tray, delete marker, hide pill (main window left as-is) |+| Embedded | alive | stay embedded (refresh marker heartbeat in `last_seen`) |++Both transitions emit a `mode-changed` event so the frontend can update the footer pill in real time.++**AD ≤ v1.8.46** only checks at boot. If HD dies while AD is running, AD stays invisible in embedded mode until restart.++## Common questions++### "I don't see AD in my system tray on a Windows box with HD installed"++Expected. HD's installer deleted AD's autostart shortcut, so AD doesn't launch on boot. AD only runs when HD spawns it. HD's tray includes AD's menu items.++To verify: `tasklist | findstr adom-desktop`. If AD's running, it's just hidden (per embedded mode). If not running, HD isn't either.++### "I want AD standalone — how do I unembed?"++- Quit HD+- Delete `%LOCALAPPDATA%\Adom Desktop\embedded.json`+- Launch AD via `%LOCALAPPDATA%\Adom Desktop\adom-desktop.exe`+- AD will detect no marker + no CLI flag → standalone mode → tray reappears++Or uninstall HD with "Yes, also remove Adom Desktop" → No → HD is gone, AD's marker is cleared, AD reverts to standalone on next launch. (Note: AD's autostart shortcut was deleted by HD's installer, so AD still won't auto-launch on boot until you re-run AD's installer to recreate it.)++### "Both HD and AD are installed — is that normal?"++Yes. HD's NSIS POSTINSTALL hook silently runs the bundled AD installer with `/S /NOLAUNCH`. AD lives at `%LOCALAPPDATA%\Adom Desktop\`, HD at `%LOCALAPPDATA%\Hydrogen Desktop\`. Two separate exes, two separate Add/Remove Programs entries (well, only HD's is in Add/Remove if HD's installer ran the embedded-mode AD which skips HKLM register — AD ≥ v1.8.46).++### "AD's tray suddenly appeared even though HD is installed"++HD probably crashed or was closed. AD ≥ v1.8.47's runtime poll noticed HD is dead and reverted to standalone tray. The fix: relaunch HD from the Start Menu → AD's next poll detects HD → tray hides again.++### "Direct API for AD verbs"++AD listens on `127.0.0.1:47200` for `POST /command` with `{"app":"desktop","command":"<verb>","args":{...}}`. The 7 user-visible verbs HD's tray uses:+- `window_show`, `window_hide`+- `connect_all`, `disconnect_all`+- `shutdown`+- `embedded_status` (returns `{embedded, owner, source, ...}`)+- `logout`++Port discovery if 47200 is taken: read `%USERPROFILE%\.adom\direct-api-port` (single line `host:port`).++## Source-of-truth references++- AD's mode detection: `adom-desktop/src-tauri/src/embedded.rs:detect_embedded_mode()`+- HD's AD spawn: `hydrogen-desktop/src-tauri/crates/hd-app/src/ad_supervisor.rs:spawn_ad_embedded()`+- HD's tray menu: `hydrogen-desktop/src-tauri/crates/hd-app/src/tray.rs:install_tray()`+- HD's NSIS hook (embed-AD on install / offer-uninstall): `hydrogen-desktop/src-tauri/crates/hd-app/installer/hd-install.nsh.template`+- AD's release manifest (used by HD's release pipeline to fetch bundled AD): `wiki-ufypy5dpx93o.adom.cloud/static/apps/adom-desktop/version.json`
skills/hd-file-transfer/SKILL.mdadded+92
@@ -0,0 +1,92 @@+---+name: hd-file-transfer+description: >+  Move files BOTH WAYS between the HD 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-desktop relay (send_files / pull_file)+  which also works on the legacy Docker runtime. 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++HD'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 (the relay) also works on the legacy Docker runtime.++## 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-desktop relay (works on ANY runtime)++HD embeds Adom Desktop, which bridges the workspace to the real OS. Use this when+you want the AI to push/pull files programmatically (and it's the ONLY method on+the legacy Docker runtime, which has no `/mnt/c`). See **hd-adom-desktop**.++```bash+# Workspace → PC (lands in the user's Downloads by default; large files OK)+adom-desktop send_files '{"filePaths":["/home/adom/project/out.zip"],"saveTo":"C:\\Users\\john\\Desktop"}'++# PC → workspace+adom-desktop pull_file '{"filePaths":["C:\\Users\\john\\Desktop\\photo.png"],"saveTo":"/home/adom/project"}'+```++Notes:+- Confirm the bridge first: `adom-desktop ping` → `pong`. If not connected, the+  user must have HD 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 runtime is Docker, 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 **hd-volume** for the full+persistent-vs-ephemeral map.++## Related+- **hd-volume** — what persists, where files live, backing up before virgin reset+- **hd-adom-desktop** — the relay (send_files / pull_file) and OS reach+- **hd-workspace-lifecycle** — distro import/export/unregister
skills/hd-golden-image/SKILL.mdadded+128
@@ -0,0 +1,128 @@+---+name: hd-golden-image+description: >+  Your Hydrogen Desktop workspace is a PRE-BAKED golden WSL2 image, not a+  machine that installed itself step-by-step. gallia, the 8 Adom CLIs, the+  `claude` CLI + Claude Code VS Code extension, code-server, the VS Code+  settings / trusted-domains / activity-bar config, AND all the hd-* self-+  awareness skills are BAKED at image-build time — none of them were installed+  by a setup step. READ THIS when a user asks "why is X already here", "how do+  I update gallia", "re-run install-hd-skills / install-gallia / install-claude",+  or "where did these skills/CLIs come from": the answer is the image, and+  updates come from a tarball-version bump, NOT a setup step. Trigger words —+  golden image, pre-baked, baked image, adom-golden, adom-golden.tar.gz, hd-wsl2-+  image, wsl --import, tarball version, adom-tarball-version, migrate tarball,+  update gallia, reinstall gallia, install-hd-skills, install-gallia, install-+  claude-cli, install-claude-ext, verify-adom-desktop, why is gallia here, where+  did the CLIs come from, how do I update the workspace, re-run setup, lightweight+  image, install on demand, command not found, ffmpeg not installed, install a tool,+  apt-get install, missing tool, is ffmpeg installed, install imagemagick/pandoc.+---++# HD Workspace = a pre-baked golden WSL2 image++Your workspace did **not** build itself by installing things one step at a time.+It was imported, fully-formed, from a **golden WSL2 image** that already contains+everything. Understanding this answers a whole class of "why is X here / how do I+update X" questions correctly.++## What's baked in (NOT installed at first-run)++The golden rootfs ships, pre-installed and smoke-gated, with:++- **gallia** (deployed via its `install.mjs` snapshot)+- the **8 Adom CLIs** (`adom-cli`, `adom-desktop`, etc. — at `/usr/local/bin`)+- the **`claude` CLI** + the **Claude Code VS Code extension** (the extension+  auto-updates via VS Code's `extensions.autoUpdate`)+- **code-server** + node, plus a baked **systemd** unit that owns code-server+  across distro boots and HD restarts+- VS Code **settings.json** (dark theme, Claude permissions), the+  **trusted-domains** list, and the **activity-bar trim** (all patched into+  `workbench.html` at bake time)+- all **hd-\* self-awareness skills** (the `shared/` + `wsl2/` buckets, flattened+  into `~/.claude/skills/<name>/`)++Because all of this is baked, the OLD setup steps that used to install it were+**removed**: `install-gallia`, `install-adom-cli`, `install-hd-skills`,+`install-claude-cli`, `install-claude-ext`, and `verify-adom-desktop` are gone.+You can see the removal comments in `setup_steps_wsl.rs` (around lines 421-450),+e.g. *"install-gallia + install-adom-cli REMOVED — both baked into the golden+image"* and *"install-hd-skills REMOVED — the hd-\* skills are baked"*. The+`install-adom-vscode` step still exists but **installs nothing** — it only proves+the baked extension *activated* (the `:8821` editor-control API answers).++## Lightweight by design — install other tools on demand++The golden image is deliberately **minimal**: it bakes the **Adom essentials** above+and little else. General-purpose tools are **not** pre-installed — **ffmpeg/ffprobe,+imagemagick, pandoc, jq, and friends are absent on purpose**, to keep the image small+and fast to download and migrate.++That is **not a limitation.** When a task needs a tool that isn't here, **install it on+demand and keep going.** The workspace has **passwordless sudo** + apt:++```bash+sudo apt-get update -qq && sudo apt-get install -y ffmpeg   # or imagemagick, pandoc, jq, …+```++So the rule when you hit **"command not found"**: **install it and proceed** — never+tell the user the workspace "can't do that." (This mirrors how `adompkg` helps install+an app's dependencies for Adom Wiki apps: lightweight base, add what the user needs,+when they need it.) Don't push to bake every tool into the image — the image stays+lean and the AI provisions per task.++## Where the image comes from++The image is built and released from the **public** `adom-inc/hd-wsl2-image`+repo. HD downloads the release asset `adom-golden.tar.gz` **anonymously** (HD+users have no GitHub identity), verifies its SHA-256, then `wsl --import`s it as+the `Adom-Workspace` distro. Evidence in `runtime/wsl.rs`:++- `TARBALL_URL_PLACEHOLDER` / `TARBALL_SHA256_PLACEHOLDER` (~lines 142-145) — the+  GitHub-release download URL + expected hash+- `download_tarball()` (~line 1893) — streams the ~550 MB asset to a cached+  `adom-golden.tar.gz` (under `%LocalAppData%\Adom\HydrogenDesktop\cache`),+  reusing the cache if present so a re-setup doesn't re-download+- `fresh_import()` (~line 307) — `wsl --import Adom-Workspace … --version 2`++So when a user asks **"why is gallia/the CLIs/these skills already here?"** the+answer is: they came pre-installed in the golden image — nothing installed them+on your machine.++## How updates actually happen — a tarball-version bump, NOT a setup step++The installed image carries a version label at **`/etc/adom-tarball-version`**+(const `TARBALL_VERSION_FILE`), written on import. HD's expected version is the+`TARBALL_VERSION` const in `runtime/wsl.rs` (~line 152). On each launch HD+compares them:++- `read_installed_version()` reads `/etc/adom-tarball-version` from inside the+  distro+- if it differs from `TARBALL_VERSION`, HD runs **`migrate_to_new_tarball()`**+  (~line 2076): terminate → full `wsl --export` rollback backup → back up+  `/home/adom` → `wsl --unregister` → `fresh_import()` the new image → restore+  `/home/adom` → `write_installed_version()`. Your files in `/home/adom` survive;+  the system layer is swapped wholesale. On any failure it rolls back from the+  export so you're never left with no distro.++The practical upshot for answering users:++| User asks | Correct answer |+|---|---|+| "How do I update gallia / the CLIs / the skills?" | You don't run an installer. A new golden image (`adom-inc/hd-wsl2-image`) is released, HD's `TARBALL_VERSION` is bumped, and on next launch HD migrates the distro — gallia/CLIs/skills come along baked. |+| "Can I re-run `install-hd-skills` / `install-gallia`?" | No — those setup steps were **removed**. They don't exist anymore because the artifacts are baked. To refresh them, take a new image (tarball-version bump), not a setup step. |+| "Why is `claude` / the extension already signed-in-ready?" | The CLI + extension are baked; the only thing the cascade does is the one human **sign-in** (the `claude-auth` step). |+| "How do I reset to a clean image?" | `wsl --unregister Adom-Workspace` (or HD's Virgin Reset) → next setup re-imports the golden tarball (the cache can be cleared so it re-downloads). |++## The cascade now just CONFIGURES the baked image++Because installation moved into the image, the WSL setup cascade shrank to **16+steps** that *configure* the already-complete workspace (ensure/start code-server,+activate the baked extension, set env vars, inject the API key, configure VS+Code, the relay/connectivity gates, Claude sign-in, and Welcome). See+[hd-setup-steps](../../wsl2/hd-setup-steps/SKILL.md) for the per-step detail.++## Related skills+- [hd-runtime-mode](../hd-runtime-mode/SKILL.md) — WSL2 (this golden-image path) vs the legacy Docker runtime+- [hd-workspace-lifecycle](../../wsl2/hd-workspace-lifecycle/SKILL.md) — start/stop/reset/migrate the imported distro+- [hd-setup-steps](../../wsl2/hd-setup-steps/SKILL.md) — the 16 configure-the-image steps (what's left after baking)
skills/hd-networking/SKILL.mdadded+175
@@ -0,0 +1,175 @@+---+name: hd-networking+description: >+  Port architecture, hostnames, and networking rules for Hydrogen Desktop.+  MUST READ before adding ports, exposing a service to your Windows host,+  referencing host URLs from inside the workspace, or wiring any service+  communication.+  Trigger words — HD port, HD network, port mapping, proxy, 127.0.0.1, loopback,+  mirrored networking, hd-control-url,+  VSCODE_PROXY_URI, relay URL, code-server proxy, container networking,+  ADOM_CARBON_URL, ADOM_HYDROGEN_URL, direct connect, 8770, 7380.+---++# Hydrogen Desktop — Networking & Port Architecture++> This is the WSL2-runtime version (default). For the legacy Docker container+> runtime (`HD_RUNTIME=docker`) see the docker/ bucket.++## Core Principle: WSL2 auto-forwards `0.0.0.0` listeners++The HD workspace is a WSL2 distro (`Adom-Workspace`), not a Docker container.+WSL2 has a built-in localhost-forwarding feature: **any service that binds+`0.0.0.0:<port>` inside the distro is automatically reachable at+`127.0.0.1:<sameport>` on Windows** — no Docker `-p` mapping, no proxy hop+required. There is no port-mapping table to maintain; the port number is the+same on both sides.++The two corollaries that drive everything below:+- **Bind `0.0.0.0`, not `127.0.0.1`.** WSL2 does NOT auto-forward a+  `127.0.0.1`-only listener — Windows can't see it. A distro service that+  binds loopback-only is invisible to the host until you route it explicitly+  (see `hd-port-watcher`).+- **There are NO Docker port mappings.** code-server binds `0.0.0.0:7380`+  inside the distro and is therefore reachable at `localhost:7380` on Windows+  automatically. Same for a dev server on `0.0.0.0:5173` → `localhost:5173`.++## In-distro services (no port mapping needed)++| Distro Port | Service | Reachable on Windows at |+|-------------|---------|-------------------------|+| 7380 | code-server (VS Code) | `localhost:7380` (auto-forwarded) |+| 8765 | relay WS | bind `0.0.0.0` → `localhost:8765`, or `/proxy/8765/` |+| 8766 | relay HTTP | bind `0.0.0.0` → `localhost:8766`, or `/proxy/8766/` |+| 5173 | your dev server | bind `0.0.0.0` → `localhost:5173` |++All of these are also reachable through code-server's `/proxy/<port>/` URL+(see below), which remains the preferred path for browser-facing HTTP/WS.++## Host-Side Ports (Tauri app, native Windows listeners)++These ports run on the Windows host inside the HD Tauri process. They are+native TCP listeners on Windows — not inside the distro.++| Port | Service | Notes |+|------|---------|-------|+| 1420 | Dev file server | Dev mode only, serves `build/` |+| 8770 | adom-desktop direct connect | Owned by Adom Desktop, NOT HD |+| 47080 | HD discovery server | |+| 47083 | HD reverse proxy | Proxies to code-server |+| 47084 | HD control API | Health, setup, eval, CDP, `/wsl/status`, `/port-forward` |+| 47085 | WebView2 CDP | Chrome DevTools Protocol |++These are configurable via `%APPDATA%\hydrogen-desktop\ports.json`.++**Reaching a host service FROM the distro:** just use `127.0.0.1:<port>`. WSL2+**mirrored networking** shares the loopback interface between the distro and the+Windows host, so a Windows listener on `127.0.0.1:<port>` is reachable at the same+`127.0.0.1:<port>` from inside the distro — no gateway IP, no firewall rule, no+`0.0.0.0` requirement on the host side. (`host.docker.internal` is a Docker-ism HD+does NOT use under WSL2 — inside the distro it resolves to a *different* interface,+not where HD listens, so curls to it fail.)++## Hostnames — reach the host at `127.0.0.1`++### From inside the distro → host machine+```+127.0.0.1+```+WSL2 mirrored networking shares loopback with the Windows host, so `127.0.0.1`+inside the distro reaches Windows host listeners directly. The distro's+`/etc/hosts` also has `127.0.0.1 adom-host` (the non-Docker alias → loopback), but+you generally don't need it — use `127.0.0.1`. Used for:+- `ADOM_CARBON_URL=http://127.0.0.1:{proxy_port}`+- `ADOM_HYDROGEN_URL=http://127.0.0.1:{proxy_port}`+- Direct connect probe: `http://127.0.0.1:8770/health`+- HD control API: the live URL in `~/.adom/hd-control-url` (`http://127.0.0.1:<dynamic>`)++The control port is **dynamic per launch** — read it from `~/.adom/hd-control-url`+(your non-interactive Bash shells don't source `.bashrc`/`profile.d`, so the env var+alone is unreliable): `BASE="$(cat ~/.adom/hd-control-url)"`.++### From the host → distro services (via proxy)+```+http://127.0.0.1:{code_server_port}/proxy/{distro_port}/+ws://127.0.0.1:{code_server_port}/proxy/{distro_port}/+```+Example: relay health → `http://127.0.0.1:7380/proxy/8766/health`+Example: relay WS → `ws://127.0.0.1:7380/proxy/8765/`++This path always works for `0.0.0.0` AND `127.0.0.1`-bound distro services,+because code-server itself is inside the distro and proxies to them locally.+It is the preferred path for browser-facing HTTP/WS even though direct+auto-forward also works for `0.0.0.0` listeners.++### From the host → host-side services+```+http://127.0.0.1:{port}/+```+Control API → `http://127.0.0.1:47084/health`+Direct connect → `http://127.0.0.1:8770/health`++## Relay Architecture++The relay (`adom-desktop serve`) runs inside the distro on ports 8765+(WebSocket) and 8766 (HTTP). It binds `0.0.0.0`, so it is reachable both via+WSL2 auto-forward (`localhost:8765` / `localhost:8766` on Windows) and via the+code-server proxy. The proxy path is preferred for connection stability:++1. **adom-desktop on Windows** connects as a WS client via the+   code-server proxy: `ws://127.0.0.1:7380/proxy/8765/`+2. **Registration** happens via `server_add` on the direct connect+   API (port 8770):+   ```json+   {+     "name": "HD Local WSL",+     "url": "ws://127.0.0.1:7380/proxy/8765/",+     "autoConnect": true+   }+   ```+3. **Health checks** use the proxy path:+   `http://127.0.0.1:7380/proxy/8766/health`++This matches cloud containers where adom-desktop connects via+`wss://<slug>.adom.cloud/proxy/8765/`.++## VSCODE_PROXY_URI++Inside the distro, `VSCODE_PROXY_URI` is set by code-server:+```+https://<slug>.adom.cloud/proxy/{{port}}/     (cloud)+http://localhost:7380/proxy/{{port}}/          (HD local, approximate)+```++Tools that build proxy URLs should use `VSCODE_PROXY_URI` when+available, falling back to `http://127.0.0.1:{code_server}/proxy/{port}/`.++## In-distro command form++The workspace is a WSL2 distro, so you reach it with `wsl`, NEVER `docker exec`:+```bash+wsl -d Adom-Workspace -u adom -- <cmd>+# or, since the distro boots as root:+wsl -d Adom-Workspace -- su - adom -c '<cmd>'+```++## NEVER Do These++1. **NEVER bind `127.0.0.1`-only** for a service that must be reachable from+   Windows. WSL2 won't auto-forward it. Bind `0.0.0.0`, or register it with the+   port-forward registry (see `hd-port-watcher`).+2. **NEVER use `host.docker.internal` for distro→host** communication — it's a+   Docker-ism HD doesn't use; inside the distro it resolves to the wrong interface.+   Reach the host at `127.0.0.1` (WSL2 mirrored networking shares loopback); read+   HD's live control URL from `~/.adom/hd-control-url`.+3. **NEVER assume you need a firewall rule or gateway IP to reach the host.**+   Under WSL2 mirrored networking the host's `127.0.0.1` listeners are shared with+   the distro's loopback — `127.0.0.1:<port>` just works, no firewall allow needed.+4. **NEVER `docker exec` / `docker cp` against the workspace.** It's a WSL2+   distro; use `wsl -d Adom-Workspace`.+5. **NEVER hardcode port numbers** in distro code. Read from env vars+   (`ADOM_CARBON_URL`, `ADOM_HYDROGEN_URL`) or discover via the direct+   connect API.+6. **NEVER assume adom-desktop direct connect (8770) is owned by HD.**+   It's owned by Adom Desktop. HD's direct-api thread binds to 8770+   as a fallback but Adom Desktop takes priority when both are running.
skills/hd-notifications-windows/SKILL.mdadded+49
@@ -0,0 +1,49 @@+---+name: hd-notifications-windows+description: >+  Windows-specific companion to [[hd-notifications]]. How HD's native toasts are delivered+  on Windows — WinRT toasts routed through a PowerShell AUMID (so they may surface under an+  "Adom" title), and the `emergency` level's persistent ORANGE TASKBAR FLASH via+  request_user_attention(Critical) that keeps flashing until the user clicks the taskbar+  icon. Source: notifications.rs. For the cross-platform `notify_user` / `/ui/toast` /+  Pup-alert API, see [[hd-notifications]]. Windows-only. Trigger words — windows toast,+  winrt toast, AUMID, Adom title toast, orange taskbar flash, request_user_attention,+  taskbar attention, emergency notification windows, powershell toast, notifications.rs.+---++# Hydrogen Desktop — Native Toasts on Windows++Windows-specific companion to **[[hd-notifications]]**. The generic skill covers the+cross-platform `notify_user` API, the `/ui/toast` in-app toast, and Pup's+`browser_alert_window`. This page covers how the native OS toast is delivered on Windows.++> **Windows-only.** On macOS HD uses a more limited `osascript`-style notification; Linux+> has no native toast path. See [[hd-notifications]] for the platform-neutral story.++## WinRT toasts via a PowerShell AUMID++On Windows, HD's native toasts are **WinRT toasts** routed through a **PowerShell AUMID**.+The `<actions>` button labels from the `actions` payload render as buttons in the toast+template.++Source: `src-tauri/crates/hd-app/src/notifications.rs` (`handle_notify` → `show_toast`).++### The "Adom" title quirk (AUMID routing)++A side effect of the PowerShell AUMID: toasts may surface under an **"Adom"**+identity/title rather than a per-app one. Relatedly, if the `title` field doesn't already+start with "Adom", HD prefixes it → `"Adom: <title>"`. That's **expected** — don't treat an+"Adom"-labeled toast as a bug.++## The `emergency` level — persistent orange taskbar flash++For `level:"emergency"`, in addition to the toast HD calls+**`request_user_attention(Critical)`** on the main window and shows/unminimizes it. On+Windows this produces a **persistent ORANGE taskbar flash that keeps flashing until the+user actually clicks the taskbar icon themselves** — it is *not* cleared by a programmatic+focus. Reserve `emergency` for "I genuinely cannot proceed without you" — it's intentionally+hard to ignore.++## Related skills+- [[hd-notifications]] — the cross-platform `notify_user` / `/ui/toast` / Pup-alert API and the macOS/Linux fallback story+- [hd-adom-desktop](../../hd-bootstrap/skills/hd-adom-desktop/SKILL.md) — AD carries `notify_user` to the host
skills/hd-permissions-windows/SKILL.mdadded+51
@@ -0,0 +1,51 @@+---+name: hd-permissions-windows+description: >+  Windows-specific companion to [[hd-permissions]]. How HD's webview-permission auto-grant+  is implemented on Windows: the WebView2 `PermissionRequested` handler that returns+  `COREWEBVIEW2_PERMISSION_STATE_ALLOW` for every permission kind (mic, camera,+  clipboard-read, geolocation, notifications, sensors), with lib.rs code evidence. For the+  cross-platform policy + what it means when building webview apps, see [[hd-permissions]].+  Windows-only. Trigger words — webview2 permission, PermissionRequested,+  COREWEBVIEW2_PERMISSION_STATE_ALLOW, lib.rs permission handler, windows webview+  auto-grant, webview2 allow, permission auto-grant windows.+---++# HD WebView2 permission auto-grant (Windows)++Windows-specific companion to **[[hd-permissions]]**. The generic skill covers the policy+(HD auto-grants every webview permission so users never see an Allow prompt) and what it+means when building webview apps. This page covers the Windows/WebView2 implementation.++> **Windows-only.** On other platforms HD uses a different webview engine (e.g. WebKit) but+> applies the same allow-everything policy — see [[hd-permissions]].++## The WebView2 `PermissionRequested` handler++On Windows, HD's webview is **WebView2**. HD registers a `PermissionRequested` handler that+returns **ALLOW for every permission kind** — clipboard-read, mic, camera, geolocation,+notifications, sensors, etc.++Evidence: `hd-app/src/lib.rs` (~lines 326-349):++```rust+// PermissionRequested: auto-grant EVERY permission kind (clipboard+// read, mic, camera, geolocation, notifications, sensors, …) so the+// user never sees a WebView2 "site wants to" dialog.+…+args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)?;+hd_log::hd_log!("[perm] auto-granted permission kind {:?}", kind);+```++Every request is unconditionally set to `COREWEBVIEW2_PERMISSION_STATE_ALLOW`. In the HD log+you'll see lines like `[perm] auto-granted permission kind …` and, at startup, `[perm]+Registered PermissionRequested auto-grant handler`.++This is WebView2/Windows-specific behavior baked into HD's webview host — it's not something+the web page or the user configures. If a webview app *isn't* getting mic/camera data on+Windows, note that the OS-level Windows privacy toggle for the camera/mic can still be off+for the app entirely (that's an OS setting, not a denied WebView2 prompt — HD never denies).++## Related skills+- [[hd-permissions]] — the cross-platform auto-grant policy + webview-app guidance+- `hd-capture-share` — the SEPARATE, user-approved screen-share / mic consent the AI requests
skills/hd-port-watcher/SKILL.mdadded+270
@@ -0,0 +1,270 @@+---+name: hd-port-watcher+description: >+  Dynamic port forwarding for Hydrogen Desktop. A daemon inside the WSL2+  workspace watches for new TCP listeners (OAuth callbacks from VS Code+  extensions like Codex, Copilot, GitLens) and notifies HD to create+  host-side TCP proxies. Covers the cases WSL2 auto-forward can't:+  `127.0.0.1`-only listeners and auto-forward gaps. Makes localhost:{port}+  on Windows reach the distro. Extensions just work — no manual setup.+  Trigger words: port forward, dynamic port, OAuth callback, Codex auth,+  extension auth, port mapping, localhost port, port watcher, port proxy.+---++# HD Port Watcher — Dynamic Port Forwarding++> This is the WSL2-runtime version (default). For the legacy Docker container+> runtime (`HD_RUNTIME=docker`) see the docker/ bucket.++## Why the daemon is still here under WSL2++WSL2 auto-forwards any `0.0.0.0:<port>` listener in the distro to+`localhost:<sameport>` on Windows — so you might think a port watcher is+unnecessary. It isn't. The daemon is **RETAINED** because WSL2's auto-forward+has two gaps the watcher closes:++1. **`127.0.0.1`-only listeners.** WSL2 does NOT forward loopback-only+   listeners. VS Code extensions (Codex, Copilot, GitLens) start OAuth callback+   servers that frequently bind `127.0.0.1` — the browser redirects to+   `localhost:{port}/auth/callback?code=...` but WSL2 never mirrored that port,+   so on Windows it doesn't exist.+2. **Auto-forward lag / wedge.** WSL2 localhost-forwarding can lag or break+   even for `0.0.0.0` listeners (a real HD-logged failure mode:+   "code-server alive in distro but Windows can't reach 127.0.0.1:7380"). The+   watcher gives HD a reliable host-side proxy that doesn't depend on WSL2's+   forwarder.++## The Solution++A lightweight daemon inside the distro monitors `/proc/net/tcp` for new+listening ports. When one appears (that wasn't there at boot and isn't a known+service), it notifies HD's control API. HD creates a host-side TCP proxy on+that same port, forwarding through code-server's `/proxy/{port}` to reach the+distro. The browser redirect just works — even for loopback-only callback+servers WSL2 wouldn't have forwarded.++## Architecture++```+WSL2 distro (Adom-Workspace):       Windows Host:+┌─────────────────────┐            ┌──────────────────────┐+│ Codex creates server │           │ HD Control API       │+│ on 127.0.0.1:1455    │           │ (:47084)             │+│ (WSL2 won't forward) │           │                      │+│ port-watcher.sh      │──POST──→ │ /port-forward        │+│ detects port 1455    │  open     │ creates TCP proxy    │+│                      │           │ on localhost:1455     │+│ code-server          │           │                      │+│ /proxy/1455/ ←───────┼───────── │ proxy forwards here  │+└─────────────────────┘            └──────────────────────┘+                                          ↑+                                   Browser redirects to+                                   localhost:1455/callback+```++## What Gets Forwarded (and what doesn't)++**FORWARDED (temporary, appeared after boot):**+- OAuth callback ports from extensions (Codex, Copilot, etc.) — especially+  `127.0.0.1`-only ones WSL2 cannot auto-forward+- Any new TCP listener that wasn't running when the distro started+- Auto-expires after 5 minutes (OAuth callbacks are fast)++**NOT FORWARDED (known services, stay behind /proxy/{port} or auto-forward):**+- code-server (8080 inside distro; auto-forwarded as 7380 mapping/`0.0.0.0`)+- relay WS/HTTP (8765, 8766)+- adom-vscode (8821)+- shotlog (8820)+- Any port in the KNOWN_PORTS list+- Any port that was listening at distro boot time+- Any port ≤ 1023++This keeps the local WSL workspace behaving like cloud Hydrogen — known+services use `/proxy/{port}` (or WSL2 auto-forward for `0.0.0.0` binds), and+only short-lived / loopback-only OAuth ports get a dedicated host proxy.++## API Endpoints++### POST /port-forward+Create or remove a host-side TCP proxy. Called by the distro's watcher.++```json+{"port": 1455, "action": "open"}   // Create proxy+{"port": 1455, "action": "close"}  // Remove proxy+```++### GET /port-forward+List currently forwarded ports.++```json+{"ok": true, "forwarded_ports": [1455], "count": 1}+```++## The Watcher Daemon++`scripts/port-watcher.sh` runs inside the distro:++1. Polls `/proc/net/tcp` every 2 seconds+2. Diffs against previous scan+3. New port (not in KNOWN_PORTS whitelist) → POST `/port-forward` open+4. Port gone → POST `/port-forward` close+5. Port open > 5 minutes → auto-expire (likely a service, not OAuth)++No boot snapshot — the whitelist is the sole source of truth. Services+like shotlog can start at any time (days/weeks later) and will never+get a host proxy as long as their port is in the whitelist.++The watcher reaches HD at `127.0.0.1:<control>/port-forward` — WSL2 mirrored+networking shares loopback with the Windows host, so `127.0.0.1` from inside the+distro reaches HD's host listener. The control port is dynamic per launch; the+live control URL is in `~/.adom/hd-control-url`.++## Installation++The watcher is a bash script (`port-watcher.sh`) deployed into the distro+during setup steps. Installation uses `wsl -d Adom-Workspace`, NOT+`docker cp`/`docker exec`:++```bash+# Copy the script into the distro (from Windows host, via the \\wsl$ share or wsl cat)+wsl -d Adom-Workspace -u root -- bash -c 'cat > /usr/local/bin/port-watcher.sh' < port-watcher.sh+wsl -d Adom-Workspace -u root -- chmod +x /usr/local/bin/port-watcher.sh+# Launch detached inside the distro+wsl -d Adom-Workspace -u adom -- bash -c 'nohup bash /usr/local/bin/port-watcher.sh >/tmp/port-watcher.log 2>&1 &'+```++The script parses `/proc/net/tcp` directly in `awk` — it filters listener rows+(state `0A`), splits the hex `local_address`, and converts the port with+`strtonum("0x" …)`. No Python is involved.++## UI: Toast + Port Mappings Dialog++**Toast notification:** When a port is forwarded, HD shows an info toast:+"Port 1455 forwarded to workspace". When removed: "Port 1455 forwarding stopped".+Powered by the `port-forward-changed` Tauri event.++**Port Mappings dialog:** Accessible from the Adom menu → Admin → "Port Mappings".+Shows auto-forwarded `0.0.0.0` ports AND dynamic watcher-created proxies. Dynamic+ports have a "Stop" button. Matches the API Explorer visual style.++## Adding New Known Services++When a new service is added to the distro that runs a persistent TCP server,+add its port to the `KNOWN_PORTS` whitelist string in `port-watcher.sh`+(`scripts/port-watcher.sh:16`):++```bash+KNOWN_PORTS="8080 8765 8766 8821 8820 36035 47084 47083 47085 1420 7380 8770 9000"+```++This prevents it from getting a dedicated host proxy. Services should bind+`0.0.0.0` (auto-forwarded) or use `/proxy/{port}` instead, consistent with web+Hydrogen.++## Why Both Layers++There are TWO distinct forwarding mechanisms. The `/proc/net/tcp` watcher above+is **Layer 2**. Layer 1 is a completely separate path — the OAuth-callback+**localhost proxy** baked into HD itself — and it's why VS Code extension logins+"just work" without any watcher round-trip.++### Layer 1 — the OAuth-callback localhost proxy (zero-latency, the primary path)++HD intercepts top-level WebView2 navigations via+`setup_frame_navigation_interception()` (`hd-app/src/lib.rs:75`). On every+navigation it runs `extract_oauth_callback_port()` (`lib.rs:384`), which+**triple-URL-decodes** the target URL (`urlencoding::decode` x3) and scans each+decode layer — plus the raw and the percent-encoded+`redirect_uri=http%3A%2F%2Flocalhost%3A…` form — for+`redirect_uri=http://localhost:<port>`. When it finds one, it calls+`start_oauth_callback_proxy(port)` (`lib.rs:411`), which **pre-binds**+`127.0.0.1:<port>` on the Windows host (via `hd_control::reuse_bind_tcp`) and+waits up to ~5 min for one inbound request. When the browser redirects to+`localhost:<port>/callback?code=…`, HD's pre-bound listener accepts it and+forwards the full path into the distro via code-server:+`http://localhost:<code_server_port>/proxy/<port>/<path>` (`lib.rs:454`), then+streams code-server's response back to the browser.++So the callback port is bound the *instant* the OAuth URL is seen leaving the+WebView — before the provider ever redirects — which is why Codex / Copilot+OAuth completes with zero manual setup and zero watcher latency.++**Control / callback endpoints** (HD control API, port 47084):+- `POST /start-oauth-proxy` — manually pre-bind a callback port (same effect as+  the navigation interceptor; used by the manual fallback).+- `GET /oauth/callback` — HD's own built-in callback receiver (used by the+  Claude Code PKCE flow); do not call directly.++**Failure mode the AI may see:** if the proxy binds but code-server can't be+reached, HD returns a styled **502 Bad Gateway** page (dark `#0d1117`+background, teal `#00b8b0` "OAuth callback proxy" heading) reading roughly *"HD+tried to forward this OAuth callback to the container via code-server proxy+(port …/proxy/…), but it failed. The extension that started this OAuth flow may+need its callback port (…) mapped to the host."* Seeing that page in a browser+means Layer 1 fired but the distro-side proxy hop failed — check the HD log for+`[oauth-proxy]` lines and that code-server is alive.++### Layer 2 — the port watcher (`/proc/net/tcp` daemon, the safety net)++The bash daemon documented above. Safety net for extensions that open OAuth+URLs through mechanisms that bypass top-level WebView navigations, AND the only+thing that surfaces `127.0.0.1`-only callback servers WSL2 silently won't+forward. Note: when an app has already registered its port via the+adom-port-hints protocol, the watcher skips it (it only owns the fallback for+third-party extensions that don't know HD exists).++### Layer 3 — manual++`POST /start-oauth-proxy` or `POST /port-forward` (or+`adom-cli port-forward register`) for cases where both automatic layers miss.++## Port Conflicts++If the host port is already taken by another Windows app, the forwarding+fails silently. The OAuth callback will break. Mitigation: extensions+typically pick a different random port on retry. HD logs the conflict.++## How OAuth Callbacks Work End-to-End++1. Extension (Codex) starts HTTP server on `127.0.0.1:1455` inside the distro+2. port-watcher.sh detects port 1455 (not in boot snapshot, not known) —+   note WSL2 would NOT have auto-forwarded this loopback-only listener+3. Watcher POSTs `{"port":1455,"action":"open"}` to HD+4. HD creates TCP proxy: `localhost:1455` → code-server `/proxy/1455/`+5. Extension opens browser with `redirect_uri=http://localhost:1455/callback`+6. User authenticates, browser redirects to `localhost:1455/callback?code=...`+7. HD's proxy receives the request, forwards into the distro via code-server+8. Extension receives callback, completes auth+9. Extension closes its server, port 1455 disappears+10. Watcher POSTs `{"port":1455,"action":"close"}`, HD removes proxy++## Debugging++All debugging runs inside the distro via `wsl -d Adom-Workspace -u adom -- ...`,+NEVER `docker exec`:++```bash+# Check what ports the watcher sees+wsl -d Adom-Workspace -u adom -- bash -c "cat /proc/net/tcp | awk '\$4 == \"0A\" { split(\$2, a, \":\"); printf \"%d\\n\", strtonum(\"0x\" a[2]) }' | sort -n"++# Check the watcher's own log+wsl -d Adom-Workspace -u adom -- cat /tmp/port-watcher.log++# Check what HD has forwarded (from Windows host)+curl -sf http://127.0.0.1:47084/port-forward++# Manually forward a port (for testing, from Windows host)+curl -sf -X POST http://127.0.0.1:47084/port-forward \+  -H Content-Type:application/json \+  -d '{"port":1455,"action":"open"}'++# Check HD log for port-fwd events+# Look for [port-fwd] entries+```++## Related skills++- `hd-networking` — `0.0.0.0` auto-forward, reaching the host at `127.0.0.1`+  (mirrored networking / loopback), NEVER do these+- `hd-ports` — bind `0.0.0.0` first; fall back to the port-forward registry for+  `127.0.0.1`-only services or unreliable auto-forward
skills/hd-ports/SKILL.mdadded+223
@@ -0,0 +1,223 @@+---+name: hd-ports+description: How HD 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.+---++# HD 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 HD workspace is a WSL2 distro (`Adom-Workspace`), not a Docker container.+There is **no Docker `-p` port mapping** — 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+HTTP/WS, code-server's `/proxy/<port>/` URL pattern is still preferred and still+exposes any internal port. For the cases WSL2 auto-forward can't cover+(`127.0.0.1`-only listeners, or when auto-forward is flaky), HD's port-forward+registry creates a real host-side proxy on demand.++## The first rule: bind `0.0.0.0`++| Bind address inside distro | Reachable on Windows? |+|----------------------------|------------------------|+| `0.0.0.0:N` (all interfaces) | YES — `localhost:N` automatically |+| `127.0.0.1:N` (loopback only) | NO — WSL2 won't forward it |++So before you do anything else: **make your service bind `0.0.0.0`.** For most+dev servers that's a flag (`--host 0.0.0.0`, `vite --host`, `HOST=0.0.0.0`,+`server.address("0.0.0.0")`). Once it binds `0.0.0.0`, it auto-forwards — no+registration needed.++## The code-server proxy URL pattern++Any service listening on port N inside the distro is reachable at:++```+http://localhost:7380/proxy/N/+```++This works for HTTP services. WebSockets work too (`ws://localhost:7380/proxy/N/`).+No port mapping needed, and it works for `0.0.0.0` AND `127.0.0.1`-bound+services (code-server is inside the distro, so it reaches loopback listeners+that Windows can't). This is the preferred path for anything you open in a+browser.++Examples:+- Your dev server on port 5173 → `http://localhost:7380/proxy/5173/`+- The relay HTTP server on port 8766 → `http://localhost:7380/proxy/8766/`+- adom-vscode's API on port 8821 → `http://localhost:7380/proxy/8821/`++**This is how everything-inside-the-distro reaches everything-outside.** Inside+the distro, services that need to talk to "the host" use `127.0.0.1:<port>` —+WSL2 mirrored networking shares loopback with the Windows host — for HD's own+services (HD's live control URL is in `~/.adom/hd-control-url`), and the proxy+URL for everything else.++## The port-forward registry (for what auto-forward can't cover)++Auto-forward handles the common case. You still need the registry — HD's+port-forward registry, serviced by the port-watcher daemon (see+`hd-port-watcher`) — when:++- The service binds **`127.0.0.1` only** and you can't change it (some OAuth+  callback servers, some native tools). WSL2 won't auto-forward loopback-only+  listeners; the registry creates a real host proxy that reaches them through+  code-server.+- WSL2 localhost-forwarding is **lagging or broken** for a port (a real HD+  failure mode: "code-server alive in distro but Windows can't reach+  127.0.0.1:7380"). The registry gives a reliable host-side proxy that doesn't+  depend on WSL2's forwarder.++Decision order:+1. **Bind `0.0.0.0`** → auto-forwarded, done.+2. Can't bind `0.0.0.0` (loopback-only), or auto-forward is unreliable →+   **register the port** so HD proxies it.++### Register a port (from inside your workspace)++```bash+# Tell HD: "please bind localhost:5555 on Windows and forward into the distro"+adom-cli port-forward register 5555 --name "oauth-cb" --reason "127.0.0.1-only oauth callback" --visibility external+```++Visibility options:+- `external` — bind `localhost:5555` on Windows via an HD-managed proxy AND+  keep the `/proxy/` URL working. Best for OAuth callbacks, native clients,+  loopback-only servers.+- `internal` — proxy URL only, no host binding. Best for webview panels that+  don't need a separate host port.+- `either` — let HD decide based on heuristics (defaults to external).++### Unregister+```bash+adom-cli port-forward unregister 5555+```++### List+```bash+adom-cli port-forward list+```++## The Port Mappings dialog++Open via: HD Adom menu → "Port Mappings", or `POST http://127.0.0.1:47084/setup/panel/show`+then navigate to the Ports tab. Shows:++- **Auto-forwarded ports** — `0.0.0.0` listeners WSL2 is mirroring to Windows+- **Registered ports** — anything you've registered via `port-forward register`+  (HD-managed host proxies, for loopback-only / unreliable cases)++Each row shows the direct URL (`http://localhost:<port>`) AND the proxy URL+(`http://localhost:7380/proxy/<port>/`). Click either to open in your browser+via the Browser Picker.++## HD's host-side port registry++HD reads/writes its host-side port assignments at+`%APPDATA%\hydrogen-desktop\ports.json`. The `PortConfig` struct+(`hd-control/src/ports.rs`) holds exactly four service ports —+`code_server`, `proxy`, `control`, `cdp` — plus the bind `hostname`. There are+no relay-WS / relay-HTTP host ports in `PortConfig`: under WSL2 the relay binds+**8765 (WS) / 8766 (HTTP) inside the distro**, reached at the same port on+Windows (mirrored loopback) or via `/proxy/8766/`, and discovered from+`~/.adom/cli-relay-ports.json` inside the distro. There is no host 47081/47082+listener.++Defaults (you can override via ports.json):++| Service | Default host port |+|---------|-------------------|+| code-server | 7380 |+| Discovery server | 47080 |+| Reverse proxy (`proxy`) | 47083 |+| HD control API (`control`) | 47084 |+| WebView2 CDP (`cdp`) | 47085 |+| KiCad bridge | **dynamic** (adom-desktop picks at bridge launch) |+| Fusion bridge | **dynamic** (adom-desktop picks at bridge launch) |+| Puppeteer bridge | **dynamic** (adom-desktop picks at bridge launch) |++**Conflict resolution is NOT "next sequential port."** On startup+`PortConfig::resolve_available()` checks `proxy`, `control`, and `cdp`; if a+preferred fixed port is taken, HD binds an **ephemeral `127.0.0.1:0`** port (the+OS hands back a guaranteed-free one outside any HNS reservation) and persists it+to `ports.json`. It does **not** walk to 47086, 47087, … — HNS reserves+contiguous chunks invisible to netstat, so a walk would march into the next+reserved block. `code_server` is **exempt** and never resolved (it's tied to the+distro's mirrored port). So a busy box does NOT predictably land on+47083/47085 — the AI must discover the live ports.++**Never hardcode `127.0.0.1:9001` in distro code** — that was the old default+and has since moved. To find the real runtime ports, hit **`GET /ports`** on the+control API (default `127.0.0.1:47084`) — it returns+`{code_server, proxy, control, cdp, hostname}` straight from the resolved+`PortConfig` (`hd-app/src/lib.rs` `get_ports`). `ports.json` is the single source+of truth every consumer reads.++## Inside vs outside — which URL to use++| You're calling from | To reach a service on port N | Use |+|---------------------|------------------------------|-----|+| Distro shell / app | Another distro service | `http://localhost:N` |+| Distro app | The Windows host | `http://127.0.0.1:N` (WSL2 mirrored networking shares loopback with the host — no gateway IP, no firewall rule) |+| Windows browser | A `0.0.0.0`-bound distro service | `http://localhost:N` (auto-forwarded) |+| Windows browser | Any distro service (HTTP/WS) | `http://localhost:7380/proxy/N/` (proxy URL, preferred) |+| Windows browser | A `127.0.0.1`-only distro service | `http://localhost:N` (only after `port-forward register ... external`) |+| Distro | HD's own control API (on Windows host) | `$(cat ~/.adom/hd-control-url)` → `http://127.0.0.1:<dynamic>` (port is dynamic per launch) |++## Common bugs++**"My dev server at port 5173 doesn't load in Chrome"**+- Most likely it's bound `127.0.0.1` only. Restart it bound to `0.0.0.0`+  (e.g. `vite --host`) and it auto-forwards to `localhost:5173`.+- OR use the proxy URL: `http://localhost:7380/proxy/5173/`.+- If it IS on `0.0.0.0` but still unreachable, WSL2's forwarder may be lagging —+  register it: `adom-cli port-forward register 5173 --visibility external`.++**"OAuth callback fails with refused connection"**+- The provider is hitting `http://localhost:5555/callback` but the callback+  server is loopback-only inside the distro, so WSL2 doesn't forward it.+- Register first: `adom-cli port-forward register 5555 --visibility external --reason "oauth callback"`.+- HD auto-detects callback URLs and starts a proxy for them automatically in+  many cases (see HD log for `[oauth] Callback port X detected`), but+  registering explicitly is the reliable path.++**"`localhost:7380` shows nothing / code-server unreachable from Windows"**+- code-server is alive in the distro but WSL2 localhost-forwarding wedged (a+  real HD-logged failure mode). The port-forward registry / port-watcher+  daemon installs a host-side proxy to recover this — see `hd-port-watcher`.+  Or restart HD to re-establish the forward.++## Related skills++- `hd-networking` — the broader picture: `0.0.0.0` auto-forward, hostnames,+  proxy architecture, relay vs direct connect, firewall rules, NEVER do these+- `hd-port-watcher` — the retained daemon that registers `127.0.0.1`-only+  listeners and covers WSL2 auto-forward gaps+- `hd-container` — what's inside the workspace reachable on which port+- `hd-desktop-sse` — the workspace API which runs on these ports+- `hd-browser-picker` — what happens when you click a port-mappings URL++## Bridge ports are dynamic++KiCad, Fusion 360, Puppeteer, and any 3rd-party bridges no longer have known+fixed ports. When a bridge launches, **adom-desktop picks an available port at+runtime** and registers it. To find a running bridge's current port, query+adom-desktop:++```bash+adom-desktop status+# Look at desktop.apps.<bridge>.bridgePort+```++Or use the bridge directly via adom-desktop CLI — you don't need the port at all:++```bash+adom-desktop kicad_open_board '{"path":"..."}'  # adom-desktop routes to the right port+adom-desktop browser_eval '{"js":"..."}'+```++This means **old code or skill examples that hardcoded fixed bridge ports+(8771 / 8773 / 8851) are stale.** Use the adom-desktop CLI or `status` lookup+instead.
skills/hd-recording/SKILL.mdadded+342
@@ -0,0 +1,342 @@+---+name: hd-recording+description: >+  How to record screen + window video from Hydrogen Desktop. In HD 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/), AD host desktop recording (desktop_record_start, Windows-only),+  the tab-vs-desktop footgun, and what `--mic` does. 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.+---++# Hydrogen Desktop — Screen & Window Recording++In HD 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+[hd-screen-lock](../hd-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+HD native recorder (h264 mp4 → `~/project/recordings/`). Do **NOT** reach for+`adom-desktop desktop_record_start` for an ordinary "record my screen / window / the+workspace" — that's the AD **host** fallback: it writes a `.webm` into AD's+bridges-cache (`…/Adom Desktop/bridges-cache/…`), **not** your `~/project/recordings/`,+and is only for when HD'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-desktop` for+anything inside HD.)++## Which recorder? — the priority ladder (read first)++Same shape as [hd-self-screenshot](../hd-self-screenshot/SKILL.md): **lead with `adom-cli`+(consistent with web Hydrogen), drop to the richer HD/AD surfaces only for what it can't+do.** Default to HD-native.++1. **HD 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 HD (web-H+   records via `getDisplayMedia`; HD records **natively** via WGC/DXGI — no picker, no "you're+   sharing" banner, no display wake-lock). `--share tab` = the HD 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): **HD 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+   **HD-native exclusive** (neither adom-cli nor AD 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 HD's native recorder is already busy:** `adom-desktop+   desktop_record_start {confirmDesktopNotTabRecording:true}`. **Whole-desktop ONLY (no+   per-window), via Chrome `getDisplayMedia` → it DOES hold the display wake-lock** (unlike HD+   native) and writes a **`.webm` into AD's cache** (not `~/project/recordings/`). AD's own help+   says "prefer Hydrogen's `recording start --share screen`" — this is for the+   parallel-with-an-HD-capture case, not the default.+4. **A single Chrome tab** (AI-driven Pup browser — a clean per-tab clip): `adom-desktop+   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 AD grabs a window *or* the screen — **recording is NOT uniform**:++| Recorder | Window | Full desktop | Browser tab | Backend | Wake-lock | Output |+|---|---|---|---|---|---|---|+| `adom-cli hydrogen recording` (HD) | — (use control API) | ✅ `--share screen` | ✅ `--share tab` | native WGC/DXGI | **NO** | mp4 → `~/project/recordings/` |+| HD native control API | ✅ `source:"window:X"` | ✅ `source:"screen"` | — | native WGC/DXGI | **NO** | mp4 → `~/project/recordings/` |+| AD `desktop_record_start` | ❌ **none** | ✅ whole desktop | ❌ | Chrome `getDisplayMedia` | **YES** | `.webm` → AD cache |+| pup `browser_record_start` | — | ❌ | ✅ one tab | CDP screencast | **NO** | tab clip |++So: **per-window recording = HD-native only; whole-desktop = HD-native (no wake-lock) preferred+over AD (wake-lock); single tab = pup.** When someone says "record a window," that's #2 (HD+control API) — AD cannot do it.++---++## (a) HD native recording (WGC + DXGI) — the default++**Mechanism:** Rust `crates/hd-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 HD 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 HD 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 HD 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, HD shows a persistent **"● Recording · <source>"** badge so it's+  never silently on.+- HD does **not** stop on blur/minimize — recording while HD is backgrounded (to+  capture KiCad/Fusion) is the normal case. See [hd-screen-lock](../hd-screen-lock/SKILL.md).++### Taking screenshots WHILE recording (multi-AI-thread capture contention)++Adom users run several AI threads against one HD 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`+  (HD-native, the default), `adom-desktop desktop_screenshot_window` (AD per-window),+  `adom-desktop desktop_screenshot_screen` (AD 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+[hd-self-screenshot](../hd-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 HD 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) HD's in-app combined recorder (web-Hydrogen / legacy getDisplayMedia)++> **In HD 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 HD, `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+  [hd-capture-share](../hd-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, HD **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) AD host desktop recording (Windows-only)++Served by the embedded **Adom Desktop** process (see+[hd-adom-desktop](../hd-adom-desktop/SKILL.md)), not by the HD frontend. These+record the **real OS desktop**, not HD's webview:++```bash+adom-desktop desktop_record_start '{"reason":"Demo recording","confirmDesktopNotTabRecording":true}'+adom-desktop desktop_record_stop+adom-desktop desktop_record_status+adom-desktop desktop_recorder_open      # open the recorder UI on the host+```++**Verify the capability first** — desktop recording is **Windows-only** and only+present when AD advertises it:++```bash+adom-desktop 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 (HD)** | `adom-cli hydrogen recording start --share screen …` or `POST /recording/native/start {source:"screen"}` | **h264 mp4** → `~/project/recordings/` |+| **Record a window (HD), e.g. "record kicad"** | `POST /recording/native/start {source:"window:KiCad"}` | **h264 mp4** → `~/project/recordings/` |+| **Smaller archival recording (HD)** | `POST /recording/native/start {source:…, codec:"h265"}` | **h265 mp4** (not web-playable) |+| Record one AI browser tab | `adom-desktop browser_record_start` (pup) | tab screencast |+| Record host desktop via AD (HD busy) | `adom-desktop desktop_record_start '{"confirmDesktopNotTabRecording":true}'` | host recording |+| Record the workspace (web-Hydrogen) | In-app Recording dropdown (or `promptRecording()`) | `.webm` → container `recordings/` |++## Related skills+- [hd-screen-lock](../hd-screen-lock/SKILL.md) — native capture holds NO display wake-lock; the max-duration cap + "● Recording" indicator+- [hd-self-screenshot](../hd-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`+- [hd-capture-share](../hd-capture-share/SKILL.md) — the (web-Hydrogen) screen-share + AI-consent flow incl. the cancelable CountdownToast+- [hd-bridges](../hd-bridges/SKILL.md) — `browser_record_start` (Pup tab recording) and the AD capability list (`record`, `screenshot`, …)+- `pup` — driving the Pup browser windows you'd record per-tab
skills/hd-runtime-mode/SKILL.mdadded+81
@@ -0,0 +1,81 @@+---+name: hd-runtime-mode+description: >+  Which workspace runtime your Hydrogen Desktop is using — a WSL2 Ubuntu distro+  (default) or a legacy Docker container (HD_RUNTIME=docker) — how to tell which+  one you're in, what changes between them, and which runtime-specific skills to+  trust. READ THIS FIRST when anything about the workspace's container/distro,+  networking, volumes, ports, or setup steps seems contradictory: the answer+  almost always depends on the runtime. Trigger words — runtime, wsl, wsl2,+  docker mode, HD_RUNTIME, Adom-Workspace, which runtime, container or distro,+  workspace runtime, container runtime unhealthy, am I in docker or wsl.+---++# HD Runtime Mode — WSL2 (default) vs Docker (legacy)++Hydrogen Desktop hosts your workspace one of two ways. Everything Claude knows+about "the container" — its filesystem, networking, ports, stats, and setup+steps — depends on which one is active. **Check the runtime before trusting any+runtime-specific advice.**++| `HD_RUNTIME` | Runtime | What the workspace is | Setup cascade | State file |+|---|---|---|---|---|+| unset / `wsl2` | **WslDistro — DEFAULT** | WSL2 Ubuntu distro `Adom-Workspace` (`wsl --import`) | 16 steps | `setup-steps-wsl.json` |+| `docker` | DockerDesktop — legacy fallback | Docker container (Docker Desktop) | 24 steps | `setup-steps.json` |++WSL2 is the default — you only get Docker if `HD_RUNTIME=docker` was set before+HD launched. The selector is `runtime/mod.rs::select_runtime()`; the WSL setup+executor is `setup_steps_wsl.rs`, the Docker one is `setup_steps.rs`. Both paths+are kept alive on purpose: the Docker runtime is a pristine fallback in case a+machine can't run WSL2.++## Which runtime am I in? (authoritative checks)++1. **Startup log line:** `[app] Active workspace runtime: WslDistro` (or+   `DockerDesktop`). This is the single source of truth.+2. **WSL only:** `wsl -l -v` shows `Adom-Workspace  Running`; HD logs+   `[wsl-trigger] backend poller armed (...setup-trigger-wsl.json)` and+   `[run_all_wsl] ...`.+3. **Docker only:** a `hydrogen-*` container appears in `docker ps`.+4. From inside the workspace, ask HD: `GET "$(cat ~/.adom/hd-control-url)/wsl/status"`+   (loopback `http://127.0.0.1:<dynamic>`) succeeds (with distro info) on the WSL runtime.++### ⚠️ Benign noise: `[docker] Container runtime unhealthy — exec failed, needs restart`+This line can appear **even on the WSL2 path** — a legacy `hd-docker` heartbeat+polls for a Docker container that doesn't exist and logs "unhealthy". It is+cosmetic and does NOT mean you're in Docker mode. The real WSL health signal is+`[wsl] Workspace unhealthy …`. Confirm the runtime via check #1, never by this line.++## What actually differs between the runtimes++| Concern | WSL2 (default) | Docker (legacy) |+|---|---|---|+| Workspace | `Adom-Workspace` distro, imported from the pre-baked `adom-golden.tar.gz` | container from a ~390 MB image |+| Run a command in it | `wsl -d Adom-Workspace -u adom -- <cmd>` | `docker exec <container> <cmd>` |+| Your files | `/home/adom/project` in the distro's ext4 fs (no volume); browse from Windows at `\\wsl$\Adom-Workspace\…` | Docker named volume; copy out via `docker cp` |+| Ports | code-server binds `0.0.0.0:7380`; WSL2 auto-forwards `0.0.0.0` listeners to Windows `localhost` | single `-p 7380:8080` map; other ports via `/proxy/<port>/` |+| Reach the host | `127.0.0.1` (WSL2 mirrored networking shares loopback with the Windows host); HD's live control URL is in `~/.adom/hd-control-url` | `host.docker.internal` (Docker-provided) |+| Wipe/reset | `wsl --unregister Adom-Workspace` (only that distro) | `docker rm` + volume/image removal |++## Which skills to trust++HD only installs the skills for the **active** runtime (plus the shared ones), so+in most workspaces you'll already have the right set. The runtime-specific skills+exist in two buckets in the HD repo — `wsl2/` and `docker/` — and share names:++- `hd-networking`, `hd-ports`, `hd-port-watcher`, `hd-volume`, `hd-container`,+  `hd-topology`, `hd-container-stats`, `hd-setup`, `hd-setup-steps`+- WSL-only workspace skills: `hd-workspace-lifecycle`, `hd-workspace-monitoring`+- Docker-only skills: `hd-docker-desktop`, `hd-docker-lifecycle`,+  `hd-docker-monitoring`++If you ever see Docker-specific advice (`docker exec`, named volumes, `-p` port+maps) while check #1 says `WslDistro`, you're reading the wrong bucket — defer to+the WSL2 mechanics above.++## Switching runtimes (rare; usually a dev/support action)+HD reads `HD_RUNTIME` from its environment at process start.+- → WSL2 (default): ensure `HD_RUNTIME` is unset or `wsl2`, relaunch HD. If a+  prior run set Docker: `setx HD_RUNTIME wsl2` then relaunch.+- → Docker (fallback): `setx HD_RUNTIME docker`, relaunch HD (requires Docker+  Desktop running).
skills/hd-screen-lock/SKILL.mdadded+89
@@ -0,0 +1,89 @@+---+name: hd-screen-lock+description: >+  HD 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. HD is NATIVE: its+  screenshots use CDP and its recording uses Windows.Graphics.Capture (WGC) /+  DXGI — neither holds the display wake-lock — so HD does not have the web leak.+  HD 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 HD screenshot / screen-recording features. Trigger words: hd+  screenshot, hd 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.+---++# HD 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`.++## HD is native — it does NOT have this leak++HD never uses `getDisplayMedia`:++- **Screenshots** use **CDP** (`Page.captureScreenshot` via `/capture/viewport` and+  `/shot`) — a one-shot grab, no `MediaStream`, no wake-lock. See+  [hd-self-screenshot](../hd-self-screenshot/SKILL.md).+- **Recording** uses **Windows.Graphics.Capture (WGC)** per-window + **DXGI Desktop+  Duplication** full-screen (Rust, `crates/hd-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+  [hd-recording](../hd-recording/SKILL.md).++So the burn-in failure mode that bites web Hydrogen **cannot happen** from HD's own+capture. The remaining risk is only a *forgotten* recording wasting CPU/disk — which+HD guards with the rules below.++## Rules HD 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, HD shows a persistent **"●+  Recording · <source>"** badge (bottom-right) so it's never silently on.+- **No blur-stop for screen recording:** HD being backgrounded is the *normal* case+  for native screen recording (you're recording KiCad / Fusion / the desktop), so HD+  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 HD doesn't use.)+- **Treat a held wake-lock as a bug.** HD 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 HD problem.+3. If it's **HD**, 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. HD'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. HD handles this without holding any lock —+CDP `POST /screenshot`, AD `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+[hd-self-screenshot](../hd-self-screenshot/SKILL.md) ("During a recording") and+[hd-recording](../hd-recording/SKILL.md).++## Related skills+- [hd-recording](../hd-recording/SKILL.md) — native WGC/DXGI recording, the max-duration cap + reason, the "● Recording" indicator+- [hd-self-screenshot](../hd-self-screenshot/SKILL.md) — native CDP screenshots (no wake-lock)+- [hd-notifications](../hd-notifications/SKILL.md) — OS toasts + the in-app `/ui/toast`+- gallia `screen-capture-wake-lock` — the **web-Hydrogen** side of this issue (where the leak actually happens)
skills/hd-setup-steps-windows/SKILL.mdadded+101
@@ -0,0 +1,101 @@+---+name: hd-setup-steps-windows+description: >+  Windows/WSL2-specific details for what HD's setup did — the concrete 16-step+  `setup_steps_wsl.rs` cascade, the `wsl --import Adom-Workspace` provisioning+  step, the `wsl --unregister` virgin-reset mechanics, `setup-steps-wsl.json`,+  and the golden `adom-golden.tar.gz` tarball. READ alongside the generic+  [[hd-setup-steps]] skill when you need the exact WSL2 steps or file paths.+  Trigger words — setup_steps_wsl, 16 steps wsl, wsl import, wsl unregister,+  Adom-Workspace, setup-steps-wsl.json, adom-golden.tar.gz, golden distro,+  ensure-workspace.+---++# HD Setup Steps (Windows / WSL2 specifics)++This is the Windows companion to the generic [[hd-setup-steps]] skill. Read+that first for the user-facing Q&A ("what did setup do", "why is X installed",+re-running steps, the virgin reset, and the setup-panel APIs). This page+carries only the concrete WSL2 cascade, commands, and file paths.++HD runs on Windows (WSL2), Mac, and Ubuntu; on Windows the workspace is the+`Adom-Workspace` WSL2 distro. The legacy 24-step Docker cascade+(`HD_RUNTIME=docker`) is in the docker/ bucket.++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 3x 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 16 steps (in order)++| # | Step | Purpose |+|---|---|---|+| 1 | ensure-workspace | Install WSL2 if needed (one auto-reboot), `wsl --import Adom-Workspace` from the golden image `adom-golden.tar.gz` (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 | 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 |+| 4 | set-env-vars | Set `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 into the distro at `/var/run/adom/api-key` (mode 644) so adom-cli works |+| 6 | configure-vscode | settings.json / trusted-domains / activity-bar are BAKED; re-asserts theme + workbench backstops idempotently, and applies the per-session layout via :8821 |+| 7 | ensure-adom-desktop | Verify the Adom Desktop companion app is running on Windows (the relay's desktop bridge) |+| 8 | start-relay | Start the adom-desktop relay inside the workspace (ports 8765 WS / 8766 HTTP) |+| 9 | test-direct-connect | Prove the fast container→desktop command path |+| 10 | test-relay | Register the relay with Adom Desktop for file streaming (file transfer + shell exec) |+| 11 | test-adom-cli | GATE: carbon path + hydrogen-proxy reachability via adom-cli (6 retries) |+| 12 | 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 |+| 13 | ensure-sse | GATE: confirm the editor browser SSE session is connected so Welcome's webview-open doesn't 409 |+| 14 | verify-workspace | GATE: battery confirming the proxy holds a real layout |+| 15 | welcome | Open Claude Code, authenticate if needed, send the first prompt |+| 16 | open-welcome | GATE (final): open welcome.html in HD's right pane; hard-fails if the Welcome tab doesn't appear |++## The WSL import step (step 1)++`ensure-workspace` 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).+It 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.++## Platform-specific failure causes++Most common WSL2 failures:+- **Step 1** — WSL2 unavailable / the Virtual Machine Platform feature is off,+  or the `adom-golden.tar.gz` download failed.+- **Step 11** (test-adom-cli) — carbon / hydrogen-proxy not reachable yet.+- **Step 12** (claude-auth) — Claude credentials expired.++## Virgin reset — WSL distro wipe++The `container` toggle does a PRISTINE `wsl --unregister Adom-Workspace` ++reimport — it deletes the distro AND everything in it, including+`/home/adom/project`. It backs up Claude creds first UNLESS `claude_token` is+checked.++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.++The `tarball` toggle (legacy key `image`) deletes the cached+`adom-golden.tar.gz` (full-image re-download). The `webview_storage` /+`vscode_state` wipes are queued and flushed at the next HD launch+(restart_required), not immediate.++## 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). HD reads+this on launch to decide whether the setup panel auto-opens. (The legacy Docker+runtime uses `setup-steps.json`.)++## Related skills++- [[hd-setup-steps]] — the generic user-facing setup-steps Q&A and APIs+- `hd-golden-image` — what's baked into `adom-golden.tar.gz` vs. what setup does+- `hd-workspace-lifecycle` — import/export/unregister/terminate, the never-touch-global-WSL rule+- `hd-networking` — port 7380 code-server, how the distro reaches the host, port forwarding+- `hd-volume` — where files live in the distro and what survives a virgin reset
skills/hd-setup-windows/SKILL.mdadded+101
@@ -0,0 +1,101 @@+---+name: hd-setup-windows+description: >+  Windows/WSL2-specific details for HD setup — the concrete 16-step WSL2 install+  cascade (setup_steps_wsl.rs), the ensure-workspace `wsl --import` of the golden+  distro, the `wsl --unregister Adom-Workspace` virgin-reset mechanics, the+  setup-steps-wsl.json state file, and the never-touch-global-WSL rule. READ+  alongside the generic [[hd-setup]] skill when you need the exact WSL2 steps,+  commands, or file paths. Trigger words — 16 steps, setup_steps_wsl, wsl import,+  wsl unregister, Adom-Workspace, setup-steps-wsl.json, adom-golden.tar.gz, WSL2+  cascade, ensure-workspace, virgin reset wsl.+---++# Hydrogen Desktop -- Setup (Windows / WSL2 specifics)++This is the Windows companion to the generic [[hd-setup]] skill. Read that+first for the setup-panel UX, Run All / Rollback All, the virgin-reset toggle+panel, the keep-auth testing pattern, and the control-API triggers. This page+carries only the concrete WSL2 cascade, commands, and file paths.++HD runs on Windows (WSL2), Mac, and Ubuntu; on Windows the workspace is the+`Adom-Workspace` WSL2 distro. The legacy 24-step Docker cascade+(`HD_RUNTIME=docker`) is in the docker/ bucket.++## The 16 Install Steps (WSL2)++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 3x 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-desktop | Verify the Adom Desktop companion app is running on Windows | ✗ |+| 8 | start-relay | Start the adom-desktop 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 Desktop for file streaming | ✗ |+| 11 | test-adom-cli | GATE: carbon path + hydrogen-proxy reachability via adom-cli (6 retries) | ✗ |+| 12 | claude-auth | The single human gate; restore creds or drive in-editor Claude.ai sign-in; runs LAST before payoff | ✓ |+| 13 | ensure-sse | GATE: editor browser SSE session connected so Welcome's webview-open doesn't 409 | ✗ |+| 14 | verify-workspace | GATE: battery confirming the proxy holds a real layout | ✗ |+| 15 | welcome | Opens Claude Code, authenticates if needed, sends the first prompt | ✗ |+| 16 | open-welcome | GATE (final): opens welcome.html in HD's right pane; hard-fails if the Welcome tab doesn't appear | ✗ |++## Step 1 — ensure-workspace (WSL2 import)++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`.++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.++## Virgin Reset — WSL2 distro mechanics++The `container` toggle does a PRISTINE `wsl --unregister Adom-Workspace` ++reimport — it deletes the distro AND everything in it, including+`/home/adom/project`. It backs up Claude creds first UNLESS `claude_token` is+checked.++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 are NO Docker actions.++The `tarball` toggle (legacy key `image`) deletes the cached+`adom-golden.tar.gz`, forcing a full-image re-download. The `webview_storage` /+`vscode_state` wipes are queued and flushed at the next HD launch+(restart_required); there is no reboot for the wipe.++## 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). HD+reads this on launch to decide whether the setup panel auto-opens. (The legacy+Docker runtime uses `setup-steps.json`.)++## Related skills++- [[hd-setup]] — the generic setup-panel UX, buttons, virgin-reset toggles, and control-API triggers+- `hd-golden-image` — what's baked into `adom-golden.tar.gz`+- `hd-workspace-lifecycle` — import/export/unregister/terminate mechanics and the never-touch-global-WSL rule
skills/hd-skill-catalog/SKILL.mdadded+117
@@ -0,0 +1,117 @@+---+name: hd-skill-catalog+description: The catalog of skills available to your Claude inside this Hydrogen Desktop workspace. Lists every public `hd-*` skill that ships with HD and explains what each one is for, so you can pick the right skill before doing anything HD-related. READ THIS FIRST when you're in a fresh HD workspace and want to know what Claude knows. Trigger words — hd skills, what skills do I have, hd catalog, skill index, skill list, what can claude do in hd, hd-* skills, browser picker, hd setup, hd container info, hd networking, hd volume, hd ports.+---++# HD Skill Catalog++You're running inside a Hydrogen Desktop workspace — a **WSL2 Ubuntu distro+`Adom-Workspace`** by default, or a **Docker container** under the legacy+`HD_RUNTIME=docker` runtime. HD ships a curated catalog of public skills (all+named `hd-*`) so your Claude knows about HD's own interface, networking, setup+flow, and gotchas. The `install-hd-skills` setup step deploys them **flat into+`~/.claude/skills/<name>/SKILL.md`** automatically.++## How the catalog is bucketed (and what you actually have)++The public skills live in three buckets in the HD repo under+`skills/public-facing/`:++- **`shared/`** — runtime-neutral. **Always installed.**+- **`wsl2/`** — the WSL2 implementations of runtime-specific topics. Installed+  when HD runs the **default WSL2 runtime**.+- **`docker/`** — the legacy Docker implementations of the same topics, plus the+  Docker-Desktop-only skills. Installed only when `HD_RUNTIME=docker`.++`install-hd-skills` deploys `shared/` **plus the active runtime's bucket** —+mirroring `runtime/mod.rs::select_runtime()`. So you have exactly one runtime's+flavor of each topic, never both. **Read `hd-runtime-mode` first** to confirm+which runtime you're in.++## Shared skills (always present)++| Skill | What it covers |+|-------|----------------|+| `hd-skill-catalog` | This file — read first to know what's available. |+| `hd-runtime-mode` | WSL2 vs Docker: which runtime you're in and what differs. |+| `hd-golden-image` | Your workspace is a pre-baked golden WSL2 image — gallia, CLIs, claude, code-server, settings, and these skills are baked in (not installed by setup); how to update. |+| `hd-skill-loop` | (Maintainers) The loop to fix a wrong/stale HD skill: verify vs the live HD, fix it live, write it back to the repo, bake it into the next image. |+| `hd-who-am-i` | Answer "who am I / what can I do here" — the auto-question HD injects on first launch. |+| `hd-adom-auth` | Adom login/logout, auth intent flow, session token, profile menu. |+| `hd-claude-auth` | How Claude Code signs in: OAuth via Browser Picker, credential backup, the `claude-auth` step. |+| `hd-api` | Control API reference (`127.0.0.1:47084`). |+| `hd-desktop-sse` | HD's local workspace API + SSE — adom-cli inside the workspace talks to HD, not cloud Hydrogen. |+| `hd-monitor` | Watch the workspace in real-time via SSE; composes with Claude's Monitor tool. |+| `hd-ui` | HD's menus, dialogs, panels, programmatic UI control via CDP, CSS selectors. |+| `hd-open-url` | The decision guide for "open a website": the six ways to open a URL (webview tab, HD window, Pup, native browser, picker, headless curl) and when to pick each. |+| `hd-browser-picker` | Every URL HD opens routes through a 5-layer interception into a picker dialog (AI-friendly 5s countdown). |+| `hd-permissions` | Why mic/camera/clipboard/notifications "just work" — HD auto-grants WebView2 permissions, so webview apps see no Allow prompts. |+| `hd-adom-desktop` | What HD and Adom Desktop (AD) are + how they work together — AD is your reach onto the user's real OS (create files/folders, run shell, OS screenshots, launch apps, browsers, KiCad/Fusion). |+| `hd-bridges` | Built-in KiCad/Fusion/Puppeteer bridges + extensible wiki catalog (e.g. Blender) + full AD capability list (usb, caption, record…). |+| `hd-eval` | Inject JS into ANY HD surface (shell/workbench/wiki/Claude) via POST /eval-in; build add-ons, overlays, highlights. |+| `hd-adom-menu` | The upper-left Adom logo menu: Settings, Ports, API Explorer, DevTools, Setup Steps, Virgin Reset, About dialog, version readout. |+| `hd-settings` | HD's Settings dialog + the full preferences tree (theme, vim mode, 3D control style, launch-on-boot, AD window behavior). |+| `hd-file-transfer` | Move files BOTH ways between the WSL2 workspace and the Windows PC (/mnt/c, \\wsl$, adom-desktop relay). |+| `hd-tab-icons` | Webview tab icon customization (displayIcon API, brand colors, MDI/SVG). |+| `hd-profile-menu` | The avatar dropdown: profile/repos/molecules links and logout. |+| `hd-embedded-ad` | HD ↔ Adom Desktop embedded mode: tray, hidden AD, footer pill. |+| `hd-self-screenshot` | How Claude screenshots HD panels + any host window (window control, multi-monitor), ralph loops, resize-before-Read rule. |+| `hd-capture-share` | The screen-share + "AI wants access" approval + countdown the AI triggers to capture the editor; tab/screen scopes, Webview/Pup window mode. |+| `hd-recording` | Record screen/window — HD's NATIVE WGC/DXGI recorder (h264/h265 mp4 → `recordings/`, no picker/banner, "record kicad"/"record whole screen", max-duration cap), plus AD desktop recording + the tab-vs-desktop footgun. |+| `hd-screen-lock` | HD screen capture and the display wake-lock: HD's native screenshots (CDP) + recording (WGC/DXGI) hold NO wake-lock, so HD has no burn-in leak; the cap + "● Recording" indicator; `powercfg /requests` to diagnose. |+| `hd-notifications` | Reach the user — OS Windows toasts + emergency taskbar flash via `notify_user`, AND the in-app `POST /ui/toast` (toast inside the HD window). |+| `hd-captions` | Paint labeled text on the user's screen — a Win32 always-on-top caption overlay for demos / step callouts. |+| `hd-welcome` | The static welcome page shown after setup completes. |+| `hd-overview` | Master skill index, grouped by category. |++## Runtime-specific skills (you get the variant for your runtime)++Same names across `wsl2/` and `docker/`; you have whichever matches your runtime.++| Skill | What it covers |+|-------|----------------|+| `hd-container` | This workspace's spec — Ubuntu 24.04, code-server 4.112.0, what's pre-installed, env vars. |+| `hd-topology` | The three layers and how commands route to reach the workspace. |+| `hd-networking` | Port architecture, hostnames, code-server proxy, host access, NEVER-do-these rules. |+| `hd-ports` | Forwarding workspace services to Windows. |+| `hd-port-watcher` | The dynamic port-forward daemon for extension OAuth callbacks. |+| `hd-volume` | Where your code lives, what persists, what a reset wipes, how to copy files out. |+| `hd-container-stats` | The title-bar CPU/RAM indicator and how its stats are gathered. |+| `hd-setup` | The setup panel UI — Run All, Rollback, Virgin Reset toggles. |+| `hd-setup-steps` | What each setup step did to tool your workspace. |++**WSL2-only:** `hd-workspace-lifecycle`, `hd-workspace-monitoring`.+**Docker-only (legacy):** `hd-docker-desktop`, `hd-docker-lifecycle`, `hd-docker-monitoring`.++## What's NOT in your workspace — internal dev skills++The HD repo also has skills under `skills/dev-internal-facing/` (hd-debug, hd-dev,+hd-docker, hd-pitfalls, hd-settings, an internal hd-setup-steps deep-dive,+hd-vm-test, and more). These are **Adom-employee-only development skills** — HD+build pipeline, debugging the Tauri/WebView2 source, internal architecture,+postmortems. They live in the HD repo but are **NEVER** deployed to user+workspaces.++The split is enforced by the `install-hd-skills` step, which only reads+`skills/public-facing/{shared,<runtime>}/hd-*/` — never `dev-internal-facing/`.++| Folder | Audience | Deployed to user workspaces? |+|--------|----------|------------------------------|+| `public-facing/shared/` | End users of HD | **Yes** (always) |+| `public-facing/wsl2/` | End users on the WSL2 runtime | **Yes** when WSL2 (default) |+| `public-facing/docker/` | End users on the Docker runtime | **Yes** when `HD_RUNTIME=docker` |+| `dev-internal-facing/` | Adom employees who develop HD | **No** (repo-only) |++## How to use this catalog++1. In a fresh HD workspace, scan this table for the most relevant `hd-*` skill+   before answering an HD-related question — and check `hd-runtime-mode` so you+   trust the right runtime's facts.+2. Skill descriptions are loaded automatically by skill-pilot at session start.+3. If a topic seems missing, it may be a dev-internal-facing skill (repo-only) —+   ask the user to check the HD repo.++## When skills get refreshed++The `install-hd-skills` step deploys skills. Re-run it via+`POST /setup/run-step {"id":"install-hd-skills"}` to refresh after an HD update.
skills/hd-skill-loop/SKILL.mdadded+120
@@ -0,0 +1,120 @@+---+name: hd-skill-loop+description: >+  The maintainer loop for making HD's own hd-* skills perfect: when a skill is+  wrong, stale, or confusing, verify the truth against the RUNNING HD, fix the+  skill live in the workspace, then write the corrected file back into the core+  hydrogen-desktop repo so the next golden-image bake ships it. These skills are+  the ONLY thing the in-workspace AI knows about HD — a wrong one confuses every+  Adom user. For Adom devs improving HD. Trigger words — this skill is wrong,+  stale skill, fix the hd skill, update a skill, the AI got confused by a skill,+  improve hd skills, skill is inaccurate, debug a skill, skill loop, write skill+  back to repo, bake skills, golden image skills, skill feedback loop, perfect+  the skills, skill says X but it actually does Y.+---++# HD Skill Loop — verify → fix live → write back to the repo → bake++HD's `hd-*` skills are baked into the WSL2 golden image and are the **only** way+the in-workspace AI knows anything about HD. So a wrong skill silently breaks+every user's AI (it curls a dead hostname, hunts a command that doesn't exist,+claims it needs a tool that isn't installed…). This skill is the loop to find+those and make them perfect *before* the next bake.++## Cardinal rule: TEST, don't assert++Never write a skill claim from memory, old skill text, or docs — **verify it+against the running system first.** Run the command. Read the live env. Read the+code. Observe the real return shape. If you can't test a claim, don't state it as+fact. (Most skill bugs are confident, untested assertions.)++## Where skills live — two copies++| Copy | Path | Role |+|---|---|---|+| **Live (ephemeral)** | `~/.claude/skills/<name>/SKILL.md` (flat) | What the running AI reads NOW. Editing here fixes THIS session and lets you test — but it's wiped on virgin reset / replaced by the next image. |+| **Canonical (baked)** | repo `skills/public-facing/{shared,wsl2,docker}/<name>/SKILL.md` | What the golden image is built from. On the laptop the repo is auto-mounted at `/mnt/c/Github/hydrogen-desktop/...` (the same tree the install step reads). The bake pulls from `origin`. |++A fix isn't done until it's in the **canonical** copy and pushed.++## The loop++### 1. Reproduce / notice+A skill made the AI do the wrong thing. Name the skill and the exact wrong claim.++### 2. Get ground truth from the LIVE system+- **Run the real command** and read its `--help` and actual output:+  `adom-cli hydrogen <verb> --help` → then run it → note the EXACT return shape+  (e.g. HD's `screenshot` returns a `dataUrl` inline when `uploadFailed:true`,+  not a saved path).+- **Read the live env**, never hardcode: `cat ~/.adom/hd-control-url` (the real+  control URL — `http://127.0.0.1:<dynamic-port>`, NOT `host.docker.internal`,+  NOT a fixed 47084), `env | grep ADOM_`, `adom-cli hydrogen probe`.+- **Read the HD source** for the endpoint/behavior (the repo checkout).+- **Not inside HD?** `adom-cli` is the SAME binary in web Hydrogen — test there.+  Or reach the user's running HD through Adom Desktop:+  ```bash+  adom-desktop ping && adom-desktop status        # confirm AD is on the right host+  # run anything inside the HD distro:+  adom-desktop wsl_exec '{"distro":"Adom-Workspace","user":"adom","scriptB64":"<base64 of your script>"}'+  ```++### 3. Fix it live + re-test+Edit `~/.claude/skills/<name>/SKILL.md` to match what you verified. Re-read it,+then **re-run the failing action** to confirm the fix actually works.++### 3b. GRADE the fix (don't just eyeball it)+Re-running the action by hand proves little. If you're an Adom maintainer, run the+**`hd-skill-test`** grader (dev-internal): add/update a case encoding the *correct*+behavior plus the anti-pattern that was the bug (e.g. `must_not_appear:+host\.docker\.internal`), then grade against the live (fixed) skill via a fresh+headless `claude` session. PASS means the fix actually changed model behavior, not+just the prose. Gate the write-back/commit on a green battery so fixing skill A+can't silently regress skill B. (See the `hd-skill-test` skill.)++### 4. Pick the bucket+Runtime-neutral → `shared/`; runtime-specific → `wsl2/` (+ keep `docker/`+pristine). `name:` must equal the dir; skills install flat. (See the dev skill+`hd-skills-structure` for the full rules.)++### 5. Write the corrected file back to the repo+Copy the same corrected SKILL.md into the canonical bucket:+```bash+cp ~/.claude/skills/<name>/SKILL.md \+   /mnt/c/Github/hydrogen-desktop/skills/public-facing/<bucket>/<name>/SKILL.md+```+If you added / renamed / removed a skill, also update the indexes+`hd-overview` and `hd-skill-catalog` (both in `shared/`).++### 6. Commit + push so the next bake ships it+From the laptop checkout (it has git + credentials), via the relay:+```bash+adom-desktop run_script '{"interpreter":"powershell","scriptB64":"<base64 of:+  cd C:\Github\hydrogen-desktop; git add skills; git commit -m \"skills: fix <name> (verified vs live HD)\"; git push>"}'+```+The next **golden-image bake** (`golden-image-bake`) pulls `origin` and bakes the+corrected skills into the image, so the fix reaches every user's next HD.++## Validation checklist before you commit a skill+- [ ] Every command in the skill was actually RUN and produced the documented output.+- [ ] No `host.docker.internal` anywhere — HD reaches the host at `127.0.0.1` /+      `$(cat ~/.adom/hd-control-url)` (WSL2 mirrored networking; ports are dynamic).+- [ ] No invented CLI verbs — each confirmed with `--help`.+- [ ] No tool assumed present that isn't in the golden image (ImageMagick `convert`,+      Pillow, `jq`…) — test it in the distro before relying on it.+- [ ] Step counts / endpoint paths / ports match CURRENT code (they drift fast).+- [ ] Cross-links use sibling skill names that resolve flat at deploy.+- [ ] `name:` matches the directory.++## Why this matters+The skills ARE HD's self-knowledge for the AI. Ship them wrong and every Adom+user's AI is confidently wrong with them. Treat "the skill is inaccurate" as a+product bug and run this loop until the validation checklist is clean.++## Related skills+- `hd-skill-test` (dev-internal) — the automated GRADE step (step 3b): headless-`claude` battery that asserts tool choices / forbidden patterns+- `hd-skills-structure` (dev-internal) — bucket rules, naming, the deploy/bake mechanism+- [hd-golden-image](../hd-golden-image/SKILL.md) — the baked-image model these skills ship in+- `golden-image-bake` — the tooling that rebuilds the image from the repo+- [hd-self-screenshot](../hd-self-screenshot/SKILL.md) — a skill fixed via this loop (the `adom-cli hydrogen screenshot` / `dataUrl` truth)+- [hd-desktop-sse](../hd-desktop-sse/SKILL.md) — the adom-cli → HD-proxy → SSE interception model you test against
skills/hd-topology/SKILL.mdadded+132
@@ -0,0 +1,132 @@+---+name: hd-topology+description: >+  MUST READ before ANY HD work. Explains the three-layer topology: cloud+  Docker (where Claude runs), Windows machine (where HD + Adom Desktop run),+  and the HD local WSL2 workspace (the Adom-Workspace distro). Every command+  you run goes through the relay to Windows. You CANNOT directly access the+  WSL2 distro — you must shell through Windows. Trigger words — HD topology,+  cloud vs local, wsl exec, where am I, which workspace, adom-desktop+  relay, test from container, three tiers, architecture, windows machine.+---++# Hydrogen Desktop — Topology (READ FIRST)++WSL2-runtime version (default). Legacy Docker container runtime (`HD_RUNTIME=docker`) is in the docker/ bucket.++## You are NOT on the user's machine++You (Claude) run inside an **Adom cloud Docker container**. The user's+Windows machine is a remote target you reach through the `adom-desktop`+CLI relay. There are THREE separate environments:++### 1. Cloud Docker (where you run)+- Your shell, your filesystem, your git repos+- `adom-desktop` CLI connects to Adom Desktop on Windows via relay+- This container has NOTHING to do with HD's local WSL2 workspace+- Restarting the WSL2 distro on Windows does NOT affect you+- You can freely `git push`, edit code, run builds here++### 2. Windows Machine (user's desktop)+- **Adom Desktop** — relay client app, listens on port 8770 (direct connect)+- **Hydrogen Desktop** — the Tauri app you're building+- **WSL2** — hosts the `Adom-Workspace` distro+- You reach this via `adom-desktop shell_execute '{"command":"..."}'`+- Commands run in `cmd.exe` on Windows++### 3. HD Local WSL2 Workspace (the `Adom-Workspace` distro)+- Distro name: `Adom-Workspace` (fixed — no random suffix, no container name)+- Runs code-server, adom-desktop CLI, relay server+- You reach this via: `adom-desktop shell_execute '{"command":"wsl -d Adom-Workspace -u adom -- <cmd>"}'`+- This is TWO hops: cloud → relay → Windows → wsl exec → distro++## Command Patterns++### Run something on Windows+```bash+adom-desktop shell_execute '{"command":"dir C:\\Github"}'+```++### Run something in the HD local WSL2 workspace+```bash+adom-desktop shell_execute '{"command":"wsl -d Adom-Workspace -u adom -- ls /home/adom"}'+```++### Test adom-desktop CLI inside the WSL2 workspace+```bash+adom-desktop shell_execute '{"command":"wsl -d Adom-Workspace -u adom -- adom-desktop ping"}'+```+This tests the FULL chain: distro → local relay → Adom Desktop on Windows.++### Screenshot HD window+```bash+adom-desktop desktop_screenshot_window '{"hwnd":HWND}'+```++### Pull file from Windows to cloud+```bash+adom-desktop pull_file '{"filePaths":["C:\\path\\file"],"saveTo":"/tmp"}'+```++## The WSL2 distro ≠ Your Container++- If the `Adom-Workspace` distro on Windows goes down, YOUR cloud container is fine+- You can still talk to Adom Desktop, screenshot windows, run shell commands+- You just can't `wsl -d Adom-Workspace ...` into the workspace until it's back+- Starting/stopping the distro on Windows is done via shell_execute. Only ever+  touch `Adom-Workspace` (`wsl --terminate Adom-Workspace`) — NEVER a global+  `wsl --shutdown`, which would knock out any other WSL distro the user has+  (e.g. a Docker Desktop integration).++## The Relay Chain++```+Cloud adom-desktop CLI+  → WS relay (cloud :8765)+    → internet+      → Adom Desktop on Windows (relay client)+        → executes command locally+          → returns result+            → back through relay to your CLI+```++For the HD local WSL2 workspace, there's a SECOND relay:+```+Workspace's adom-desktop CLI+  → local relay (distro :8765)+    → code-server proxy (:7380/proxy/8765/)+      → Adom Desktop on Windows (connected as WS client)+        → executes command+          → returns through same chain+```++## Critical Setup Steps for the HD Local WSL2 Workspace++The HD install flow MUST verify this chain works:++1. **Verify Adom Desktop running on Windows** — probe port 8770+   If not running: tell user to install from wiki and launch it+2. **Install adom-desktop CLI into the workspace** — download binary+3. **Start relay inside the workspace** — `adom-desktop serve`+4. **Register relay with Adom Desktop** — `server_add` via direct connect+   URL: `ws://127.0.0.1:{code_server}/proxy/8765/`+5. **Test round-trip** — `wsl -d Adom-Workspace -u adom -- adom-desktop ping`+   Must return `pong` — proves the full chain works++## NEVER Do These++1. **NEVER confuse your cloud Docker with the HD local WSL2 workspace.**+   `curl http://127.0.0.1:8766/health` hits YOUR relay, not the workspace's.+2. **NEVER try to `wsl -d Adom-Workspace` from your shell.**+   The distro lives on Windows WSL2, not here.+3. **NEVER assume the workspace's status affects your container.**+   They're completely independent.+4. **NEVER run a global `wsl --shutdown` or `wsl --unregister` of anything+   other than `Adom-Workspace`.** Those hit every distro on the user's machine.+5. **NEVER skip the adom-desktop round-trip test.**+   The relay chain has many links — test end-to-end, not individual parts.++## Related skills++- `hd-container` — facts about the `Adom-Workspace` distro you run inside+- `hd-volume` — where your files live and how to back up / copy out the distro
skills/hd-volume/SKILL.mdadded+115
@@ -0,0 +1,115 @@+---+name: hd-volume+description: How HD's WSL2 workspace storage is laid out — what's persistent, what's ephemeral, where your work lives, what survives a workspace restart vs a virgin reset, and how to access your workspace files from your Windows host. Use when the user asks "where are my files", "did I lose my work", "how do I copy a file out of the workspace", or "what happens to my code if I virgin reset". Trigger words — wsl filesystem, hd volume, where are my files, workspace files, persistent storage, lost my work, /home/adom/project, distro filesystem, wsl export, copy file out of workspace, where is my code, workspace backup, wsl unregister.+---++# HD Workspace Storage — what persists, what doesn't++WSL2-runtime version (default). Legacy Docker container runtime (`HD_RUNTIME=docker`) is in the docker/ bucket.++Your work lives in the `Adom-Workspace` WSL2 distro at `/home/adom/project`. There is **no Docker named volume** — `/home/adom/project` sits directly in the distro's own ext4 filesystem. ALL your work belongs under that path. It persists for as long as the distro stays registered with WSL.++## The filesystem++| Property | Value |+|----------|-------|+| Distro name | `Adom-Workspace` (fixed) |+| Your work path | `/home/adom/project` (plain directory in the distro's ext4 fs) |+| Backing store | The distro's virtual disk (`ext4.vhdx`) inside the WSL2 LxssManager store on Windows |+| On-disk location (Windows) | Under `%LOCALAPPDATA%\...\Adom-Workspace\` — but you don't poke at the vhdx directly; use `\\wsl$\Adom-Workspace\...` (see below) |+| Persistence | Survives workspace restart, HD restart, and Windows reboot. Persists as long as the distro is **registered** with WSL. |++There is **no named volume to delete separately** — the project files and the OS image are one and the same ext4 filesystem. The only thing that wipes `/home/adom/project` is unregistering the distro.++## What lives WHERE++| Path inside the distro | Persists across a tarball re-import? | Notes |+|------------------------|---------------------------------------|-------|+| `/home/adom/project/` | **Yes** — HD exports `/home/adom` before re-importing the golden image on a version bump, then restores it | This is your code. Put everything important here. |+| `~/.local/share/code-server/extensions/` | **Yes** within the distro — BAKED into the golden image, restored on tarball re-import | Not a setup step; baked at image-build time |+| `~/.local/share/code-server/User/settings.json` | **Yes** within the distro — BAKED into the golden image | Not a setup step; baked at image-build time |+| `~/.claude/` (skills, credentials) | Skills are BAKED into the golden image (restored on re-import); credentials are restored by the `claude-auth` setup step from a host-side backup | HD skills are baked FLAT into `~/.claude/skills/<name>/` |+| `/tmp/` | **No** | Ephemeral |+| `/etc/`, `/usr/`, system stuff | Lives in the same ext4 fs, but is overwritten on a tarball version bump | Treat as part of the workspace image |++**Rule of thumb:** if you want it to survive a workspace rebuild, put it under `/home/adom/project/`.++## What survives each virgin-reset choice++When you open Virgin Reset (HD setup panel), the checkbox toggles control which artifacts get deleted:++| Toggle ON means: | Result |+|------------------|--------|+| Install step state | `setup-steps-wsl.json` deleted; next setup re-runs everything. Your project files unaffected. |+| WSL distro | **`wsl --unregister Adom-Workspace`. This wipes the ENTIRE distro including `/home/adom/project/`. This is the destructive option.** No reboot needed; the wipe is immediate. |+| Workspace image | Deletes the cached `adom-golden.tar.gz`, so setup re-downloads it on next run. Your distro (and your code, if still registered) is unaffected. |+| Adom session token | Re-login required next time. Project files unaffected. |+| Claude credentials | Claude Code requires re-auth. Project files unaffected. |++**The only toggle that destroys your work is "WSL distro" (the unregister).** All others are safe for your code. (Note: the "image" reset just deletes the cached `adom-golden.tar.gz` — NOT a 390MB Docker image like the legacy runtime.)++⚠️ The reset only ever targets `Adom-Workspace`. It NEVER runs a global `wsl --shutdown` or `wsl --uninstall`, which would hit every other WSL distro on the machine.++## Accessing workspace files from Windows++Under WSL2 you **can** browse the distro's filesystem directly from Windows Explorer — a real advantage over the legacy Docker named-volume runtime. There is no `docker cp`.++### 1. Windows Explorer via the `\\wsl$` share+Open Explorer and go to:+```+\\wsl$\Adom-Workspace\home\adom\project\+```+(or the newer form `\\wsl.localhost\Adom-Workspace\home\adom\project\`).+You can drag files in and out, double-click to open in Windows apps, etc. The distro must be running (it auto-starts when you access the share).++### 2. From inside the distro, reach Windows drives at `/mnt/c/...`+```bash+# Copy a workspace file out to the Windows Downloads folder+cp /home/adom/project/myfile.txt /mnt/c/Users/john/Downloads/++# Copy a Windows file into the workspace+cp /mnt/c/Users/john/Downloads/myfile.txt /home/adom/project/+```++### 3. `adom-desktop send_files` / `pull_file` (from inside the workspace, via the relay)+```bash+# Send a workspace file to your Windows desktop's Downloads folder+adom-desktop send_files '{"filePaths":["/home/adom/project/myfile.txt"],"targetApp":"general","destinationFolder":"."}'++# Pull a Windows file into the workspace's /tmp+adom-desktop pull_file '{"filePaths":["C:\\Users\\john\\Downloads\\myfile.txt"],"saveTo":"/tmp"}'+```++### 4. Open the workspace's `/home/adom/project/` in VS Code's File Explorer+Just use the built-in code-server tree — the project folder is what's open, so anything you create/edit there hits the distro fs directly.++## Backing up your workspace++Persistence and backup for WSL2 is `wsl --export` / `wsl --import` — NOT a Docker volume tar. Before a destructive operation (the WSL-distro virgin-reset/unregister):++```cmd+:: Export the whole distro to a tarball on your Windows host+wsl --export Adom-Workspace C:\Users\john\Downloads\adom-workspace-backup.tar++:: Restore it later (re-register the distro from the backup)+wsl --import Adom-Workspace C:\WSL\Adom-Workspace C:\Users\john\Downloads\adom-workspace-backup.tar+```++This is exactly the mechanism HD's own tarball-migration uses: on a tarball version bump it `wsl --export`s `/home/adom` first, then re-imports the new base and restores your work.++Or, for just a few files, copy them out first via `\\wsl$\Adom-Workspace\...` or `/mnt/c/...`.++## Why a distro filesystem (no named volume, no bind mount)?++HD keeps your work directly inside the `Adom-Workspace` ext4 filesystem rather than a Docker named volume or a bind mount of a Windows path. This:+- Gives native Linux ext4 I/O — much faster than crossing the Windows ↔ WSL2 filesystem boundary+- Avoids line-ending and permission issues that bind mounts of Windows paths often have+- Lets HD wipe + re-import the base image (on a tarball bump) without losing your code — it `wsl --export`s `/home/adom` first++The bonus over the legacy Docker runtime: you **can** open the files directly in Windows Explorer at `\\wsl$\Adom-Workspace\...`.++## Related skills++- `hd-container` — the `Adom-Workspace` distro whose filesystem holds your work+- `hd-topology` — the three-tier topology; tier 3 is this WSL2 distro+- `hd-golden-image` — what's baked into `adom-golden.tar.gz` (extensions, settings, skills) vs. what setup does
skills/hd-workspace-lifecycle/SKILL.mdadded+165
@@ -0,0 +1,165 @@+---+name: hd-workspace-lifecycle+description: WSL2 distro (`Adom-Workspace`) lifecycle management for Hydrogen Desktop on Windows. Import/export/unregister/terminate, code-server-as-HD-child, the resume reality-check, recovery after sleep/hibernate, and every hard-won rule about what NOT to do — above all, NEVER run global WSL operations; touch ONLY Adom-Workspace so a co-installed Docker Desktop WSL integration stays safe. MUST READ before any workspace operation. Trigger words — wsl, workspace, distro, Adom-Workspace, restart workspace, terminate distro, unregister, wsl --import, wsl --unregister, code-server, workspace broken, workspace unhealthy, wsl hung, fix workspace, /wsl/status.+---++# Hydrogen Desktop — WSL2 Workspace Lifecycle++> This is the WSL2-runtime version (default). The legacy Docker equivalent+> (`HD_RUNTIME=docker`) is in the docker/ bucket.++Under the WSL2 runtime the workspace is the **`Adom-Workspace` distro** (Ubuntu,+imported with `wsl --import`), running locally on the user's Windows machine.+`/home/adom/project` lives inside the distro — there is no Docker volume.++## ABSOLUTE RULE — NEVER touch global WSL state++**Operate ONLY on `Adom-Workspace`. Never run any global / cross-distro WSL command.**++Forbidden, no exceptions:+- ❌ `wsl --shutdown` (kills the entire WSL utility VM and every distro)+- ❌ `wsl --terminate` on any distro other than `Adom-Workspace`+- ❌ blanket killing of `wsl.exe` processes+- ❌ restarting the WSL service / utility VM+- ❌ `wsl --unregister` on anything but `Adom-Workspace`++**Why this matters:** the user may already run Docker Desktop, which cross-mounts a+proxy into every distro via the shared `/mnt/wsl` namespace+(`/mnt/wsl/docker-desktop/docker-desktop-user-distro`). Any global WSL teardown makes+that path vanish and pops a "Docker Desktop - Ubuntu: WSL integration unexpectedly+stopped" dialog. It's harmless to HD's own `Adom-Workspace`, but it looks to the user+like HD broke their Docker — terrible first-run UX. So every HD operation must be+scoped to `Adom-Workspace` and nothing else.++Allowed, scoped operations:+- ✅ `wsl --terminate Adom-Workspace` (this is what "Restart" does)+- ✅ `wsl --unregister Adom-Workspace` (destructive wipe, this distro only)+- ✅ `wsl --import Adom-Workspace <dir> <tarball>` (fresh import)+- ✅ `wsl -d Adom-Workspace -u adom -- <cmd>` (exec inside the distro)++## In-distro exec — NEVER `docker exec`++To run a command inside the workspace:+```bash+wsl -d Adom-Workspace -u adom -- <cmd>+# or+wsl -d Adom-Workspace -u adom -- su - adom -c '...'+```+There is no `docker exec` under this runtime, and no Docker container to exec into.++## Workspace Lifecycle++### Import (fresh setup)+`setup_and_start` → `WslDistroRuntime::setup_and_start` → `fresh_import`:+download `adom-golden.tar.gz` (the full pre-baked golden image, downloaded+anonymously from the `adom-inc/hd-wsl2-image` GitHub release, SHA256-verified)+then `wsl --import Adom-Workspace <dir> adom-golden.tar.gz`, then start code-server.+Log proof: `[wsl-tarball] downloaded … SHA256 OK` → `[setup] WSL setup_and_start completed`+and `wsl -l -v` shows `Adom-Workspace  Running  2`.++### code-server is an HD-OWNED CHILD process+code-server (`CODE_SERVER_CHILD` in `runtime/wsl.rs`) runs as a child of HD, deliberately+tied to HD's lifetime — **NOT a detached `nohup`** (a detached process dies when WSL+tears down the session anyway). Consequence: every HD restart (rebuild / crash /+relaunch) orphans the old code-server, so a fresh HD can come up with the distro+`Running` but the editor DEAD ("Starting AI Environment" forever) even when setup is+otherwise complete.++Fix (already in HD): HD **re-ensures code-server on every launch**+(`[wsl-startup] re-ensured code-server on launch`). And the resume reality-check in+`run_all_wsl` requires code-server reachable on the host port **7380** — not just+"distro registered" — before it trusts the workspace as ready.++### Restart+"Restart Workspace" = `wsl --terminate Adom-Workspace` + re-ensure code-server.+NOT `docker restart`. This is the fix for a workspace that's gone unreachable after+laptop sleep/hibernate.++### Stop+`wsl --terminate Adom-Workspace`. The distro stays registered (and your project files+persist); it just isn't Running.++### Export / backup+`wsl --export Adom-Workspace <file>.tar` snapshots the distro to a tarball. Re-import+with `wsl --import Adom-Workspace <dir> <file>.tar`. (Scoped to this distro, so safe.)++### Unregister (DESTRUCTIVE wipe)+`wsl --unregister Adom-Workspace` — deletes the distro AND everything in it, including+`/home/adom/project`. This is the only destructive wipe, and it names ONLY+`Adom-Workspace`. Never unregister any other distro.++## Control API++The HD control API is on loopback **`http://127.0.0.1:<dynamic>`** (NOT 9001). The+port is dynamic per launch — read the live URL from `~/.adom/hd-control-url`. From+inside the distro, reach the host at the same `127.0.0.1` URL: WSL2 mirrored+networking shares loopback with the Windows host.++| Endpoint | What it does |+|---|---|+| `GET /wsl/status` | Reports `Adom-Workspace` state — registered? Running? code-server reachable on :7380? |+| `POST /wsl/unregister` | Destructive: `wsl --unregister Adom-Workspace` (this distro only) |+| `GET /container-stats` | Live CPU/RAM/disk from the `workspace_stats()` probe (see hd-container-stats) |++```bash+BASE="$(cat ~/.adom/hd-control-url)"   # http://127.0.0.1:<dynamic>++# Workspace status (from inside the distro)+curl -s "$BASE/wsl/status"++# Destructive wipe of ONLY Adom-Workspace+curl -s -X POST "$BASE/wsl/unregister"+```++## Diagnosing Workspace Problems++### "Workspace unhealthy" / editor stuck on "Starting AI Environment"+- **Cause**: code-server child was orphaned by an HD restart, or the distro went+  unreachable after sleep.+- **Check**: `[wsl] Workspace unhealthy …` in the HD log (the real signal). NOTE: a+  `[docker] Container runtime unhealthy …` line under WSL is **benign noise** — a+  leftover heartbeat with no container bound. Ignore it; look at the `[wsl]` line.+- **Fix**: Restart Workspace (Adom menu) = `wsl --terminate Adom-Workspace` + re-ensure+  code-server. Verify code-server answers on :7380.++### Distro `Running` but editor dead+- **Cause**: code-server (HD's child) isn't up even though `wsl -l -v` shows Running.+- **Fix**: relaunch HD (it re-ensures code-server on launch), or Restart Workspace.+  Confirm via the resume reality-check — code-server reachable on :7380, not just+  "distro registered."++### WSL2 not available at all+- **Cause**: WSL2 isn't installed/enabled on the machine.+- **Fix**: the "WSL2 Not Available" floaty → "Run Setup Steps" walks the user through+  enabling it. See [hd-workspace-monitoring](../hd-workspace-monitoring/SKILL.md).++## Recovery Checklist++When the workspace is broken, follow this order:++1. **Check WSL2 itself**: `wsl --status` (NOT `wsl --shutdown`).+2. **Check the distro**: `wsl -l -v` — is `Adom-Workspace` listed and Running?+3. **If Running but editor dead**: code-server child orphaned → Restart Workspace+   (`wsl --terminate Adom-Workspace` + re-ensure code-server), or relaunch HD.+4. **Verify code-server**: it must answer on host :7380 (the resume reality-check).+5. **Last resort (destructive)**: `wsl --unregister Adom-Workspace` then re-import via+   `setup_and_start` — wipes the distro and re-imports `adom-golden.tar.gz`.+6. **NEVER** reach for global WSL ops to "fix" things — that breaks co-installed Docker+   Desktop, doesn't help your distro, and angers the user.++## What NOT To Do (learned the hard way)++- Do NOT run `wsl --shutdown` — global, kills every distro, breaks Docker Desktop's+  WSL integration.+- Do NOT `wsl --terminate` or `wsl --unregister` any distro except `Adom-Workspace`.+- Do NOT blanket-kill `wsl.exe` — same blast radius as `--shutdown`.+- Do NOT use `docker exec` / `docker restart` — there is no container in this runtime.+- Do NOT trust "distro registered" as healthy — require code-server reachable on :7380.+- Do NOT treat `[docker] … unhealthy` in the log as a real failure under WSL — it's+  benign; the real signal is `[wsl] Workspace unhealthy …`.++## Related skills++- [hd-workspace-monitoring](../hd-workspace-monitoring/SKILL.md) — the 15s poll, floaty states, lifecycle dialog, auto-start+- [hd-container-stats](../hd-container-stats/SKILL.md) — title-bar CPU/RAM bars + `workspace_stats()` probe+- [hd-networking](../hd-networking/SKILL.md) — port 7380 code-server, control port 47084, host reachability
skills/hd-workspace-monitoring-windows/SKILL.mdadded+99
@@ -0,0 +1,99 @@+---+name: hd-workspace-monitoring-windows+description: >+  Windows/WSL2-specific details for HD workspace monitoring — the "WSL2 Not+  Available" floaty, the `wsl --terminate Adom-Workspace` stop command, the+  code-server-on-host-7380 reachability check, and the no-daemon framing (WSL+  has no always-on daemon to launch). READ alongside the generic+  [[hd-workspace-monitoring]] skill when you need the concrete WSL2 states,+  commands, or ports. Trigger words: wsl not available, Adom-Workspace running,+  wsl terminate, code-server 7380, no daemon, distro stopped, WSL2 floaty.+---++# HD Workspace Monitoring (Windows / WSL2 specifics)++This is the Windows companion to the generic [[hd-workspace-monitoring]]+skill. Read that first for the monitoring architecture, the ContainerStatus+struct, the 15s poll, the lifecycle dialog, the Tauri commands, the menu+items, and the Settings toggles. This page carries only the WSL2-specific+states, commands, and ports.++HD runs on Windows (WSL2), Mac, and Ubuntu. Under the WSL2 runtime the+workspace is the **`Adom-Workspace` distro** (imported via `wsl --import`), not+a Docker container. The legacy Docker equivalent (`HD_RUNTIME=docker`) is in+the docker/ bucket.++## The two WSL2 health levels++The generic two-level model maps onto WSL2 as:++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**?++A workspace can be `Running` while code-server is dead, so the level-2 check is+a real host-loopback reachability probe of port 7380, not just "is the distro+registered."++## WSL2 floaty states++The old Docker floaties ("Docker Desktop not installed/running") become these+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 `hd-show-setup` event+  (which walks the user through enabling WSL2)++### 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`)++### No "daemon not running" middle state++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). There is+no `launch_docker_desktop` equivalent under WSL.++## Auto-Start specifics++Under WSL mode, `setup_and_start` 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 re-ensure-on-launch log line is+`[wsl-startup] re-ensured code-server on launch`.++## VS Code Auto-Reload target++After the `done` lifecycle event, the dialog reloads all iframes whose src+points at `localhost:7380` (the host-side code-server port).++## WSL2-specific Tauri command mappings++| Command | WSL mode behavior |+|---|---|+| `get_container_status` | WSL available, distro present/registered, Running + code-server reachable on :7380 |+| `setup_and_start` | `WslDistroRuntime::setup_and_start` — `fresh_import` + start code-server, up to 10 min timeout |+| `stop_container` | Stops the workspace via `wsl --terminate Adom-Workspace` |++## WSL2-specific menu behavior++- **Restart Workspace** — `wsl --terminate Adom-Workspace` + re-ensure+  code-server+- **Stop Workspace** — `wsl --terminate Adom-Workspace`++## Related skills++- [[hd-workspace-monitoring]] — the generic monitoring UX, struct, poll, lifecycle dialog, and commands+- `hd-workspace-lifecycle` — import/export/unregister/terminate, the never-touch-global-WSL rule, control endpoints+- `hd-networking` — port 7380 code-server, control port 47084