← Commit history
package.json+1−1
@@ -4,7 +4,7 @@   "type": "bootstrap",   "title": "Adom Hydrogen (platform base)",   "brief": "Layer Hydrogen's workspace onto a standard Adom workstation.",-  "version": "0.3.11",+  "version": "0.3.12",   "description": "Hydrogen's PLATFORM-GENERIC workspace layer on top of adom/core. Installs adom/core for the full Adom ecosystem, then adds everything common to running inside Hydrogen on ANY platform: the runtime-neutral hydrogen-* skills and the generic editor configuration (Claude Code CLI + extension, VS Code settings, trusted domains). Platform-specific layers depend on THIS: adom/hydrogen-windows-bootstrap (WSL2), adom/hydrogen-mac-bootstrap, adom/hydrogen-ubuntu-bootstrap. The reference design other orgs copy to build their own bootstrap on adom/core.",   "org": "adom",   "platform": "any",
page.json+1−1
@@ -4,7 +4,7 @@   "type": "bootstrap",   "title": "Adom Hydrogen (platform base)",   "brief": "Layer Hydrogen's workspace onto a standard Adom workstation.",-  "version": "0.3.11",+  "version": "0.3.12",   "description": "Hydrogen's PLATFORM-GENERIC workspace layer on top of adom/core. Installs adom/core for the full Adom ecosystem, then adds everything common to running inside Hydrogen on ANY platform: the runtime-neutral hydrogen-* skills and the generic editor configuration (Claude Code CLI + extension, VS Code settings, trusted domains). Platform-specific layers depend on THIS: adom/hydrogen-windows-bootstrap (WSL2), adom/hydrogen-mac-bootstrap, adom/hydrogen-ubuntu-bootstrap. The reference design other orgs copy to build their own bootstrap on adom/core.",   "org": "adom",   "tags": [
skills/hydrogen-avatar/SKILL.mdadded+168
@@ -0,0 +1,168 @@+---+name: hydrogen-avatar+description: >+  Generate or change the user's Adom avatar (profile picture), and make Hydrogen show+  the new one immediately. Use when the user says "generate a new avatar for me", "change my+  avatar", "make me a profile picture", "update my profile photo", "set my avatar to this+  image", "reset my avatar", or hands you an image. The headline flow is GENERATE: research who+  they actually are, propose a design, author it as an SVG, render, upload, and offer to archive+  the vector on a wiki page so future sizes come from a perfect source. Trigger words: avatar,+  generate an avatar, make me an avatar, profile picture, profile photo, change my avatar,+  update avatar, set avatar, reset avatar, remove avatar, my picture, headshot, profile image.+---++# hydrogen-avatar — generate a great avatar, then make Hydrogen show it++The avatar appears in Hydrogen's title bar and heads the profile menu (the **Identity Card** — see the+`identity-card` skill). The usual request is **"generate a new avatar for me"** with nothing+else: no image file, no brief. That is not a lack of information — it's an invitation to go find+out who they are and design something that actually fits.++**Do not immediately ask "what would you like?"** A blank prompt puts the work back on them.+Research first, then come back with two or three concrete proposals and a reason for each.++## 1. Learn who this person is++Start inside Adom, then widen. Take what's cheap and public; stop when you have enough to design+with intent.++**In the Adom ecosystem** (fast, authoritative, no browsing needed):+```bash+adom-cli carbon user get      # display_name, bio, company, location, website_url+adom-cli carbon user orgs     # their organizations (name, display_name, avatar_url)+adom-cli carbon user repos    # what they actually build+```+Then their wiki footprint — pages they own, apps/skills they've published, their hero images+(those already encode a visual identity worth echoing): `adom-wiki` discovery, their user page.++**On the wider internet** (only what's public, and only if it adds something):+- Their `website_url` from the profile — usually the highest-signal single source.+- **GitHub** — languages, projects, existing avatar style.+- **LinkedIn** — role, industry, how they present professionally.+- A plain web search on `"<display name>" <company>` for talks, articles, a personal site.+- **Their signed-in browser via adom-bridge-cli** (`nbrowser_*`, the abe extension) can reach a+  Google profile or anything else they're already logged into — see `driving-the-extension`.+  Prefer this over pup for logged-in surfaces.++**Boundaries that matter here.** This is a person, not a target. Use public sources, keep it+proportionate to designing a 512px picture, and **tell them what you found and where** when you+present the concepts. Never put anything you discovered into a public artifact without asking.+If they've said they don't want research, skip straight to asking for a direction.++## 2. Propose, don't guess silently++Come back with **2–3 concepts**, each one sentence, each tied to what you learned:++> - **Monogram** — "JL" in the Adom teal, geometric and quiet. Reads at any size; matches how+>   your wiki heroes look.+> - **Craft mark** — a minimal PCB trace forming your initial, nodding to the hardware work in+>   your repos.+> - **Portrait abstraction** — flat-color silhouette from your GitHub photo, so you stay+>   recognizable across surfaces.++Say which you'd pick and why. Let them redirect before you spend effort rendering.++## 3. Author it as an SVG (and enjoy this part)++**Vector, always.** SVG is the master: infinitely rescalable, tiny, diffable, and every future+PNG comes from it.++Design rules that actually matter for an avatar:+- **Square canvas, centered composition**, safe margin — it gets clipped to a circle everywhere.+- **Legible at 32px.** This is the real constraint. Two shapes and one accent survive; fine+  linework and long text do not. Zoom out and check before you ship. If it fails at 32px, the fix+  is usually a TIGHTER CROP (below), not a simpler drawing.+- **High contrast**, and it must hold on both dark and light chrome.+- Brand teal `#00b8b0` sits naturally in Adom surfaces, but a personal avatar may deliberately+  differ from the platform palette — this is *their* identity, not a UI chrome element.+- No emoji, no clip art, no gradients so subtle they mud at small sizes.++Keep the SVG in their container, e.g. `~/project/avatars/<name>-v1.svg`, so it's editable later.++### Frame the AVATAR tight — the master and the upload are NOT the same crop++The single most common mistake: author a lovely full composition — a character with a+background, a scene, a full-body mark — render it as-is, and upload it. In a 26px circle the+subject is now an unreadable speck (John hit exactly this: *"you need to zoom to the face for+the actual rendering of the avatar, it shows in a little circle in all the UI"*).++So separate the two jobs:++- **Master SVG** — keep the full composition. It's the artwork, the thing worth archiving.+- **Avatar export** — a **tight crop on the subject**: the face, the monogram, the mark. It+  should fill roughly **70–85% of the frame**, centered, with a small safe margin.++Vector makes this free — you do not need a second drawing. Export the avatar with a **viewBox+that frames just the subject**:++```xml+<!-- master: the whole scene -->+<svg viewBox="0 0 1000 1000"> … </svg>++<!-- avatar export: same artwork, zoomed to the face at 300,220 -->+<svg viewBox="300 220 260 260"> … </svg>+```++Sizes it will actually be seen at — check the crop at the SMALLEST, not the largest:++| Surface | Rendered size |+|---|---|+| Hydrogen title bar | ~26–36px |+| Profile menu (Identity Card) | 44px |+| Identity Card, large | 64px |+| Profile page picture | ~224px |++**It is clipped to a circle everywhere**, so keep the subject centered and put nothing that+matters in the corners. Before uploading, look at the PNG scaled down to 32px: if you can't tell+what it is, re-crop tighter — don't just redraw.++## 4. Render → upload → repaint++```bash+# SVG → PNG (512x512). Use the Puppeteer-managed Chrome: the container's chromium-browser is a+# dead stub, and `chrome --headless --screenshot` hangs on real pages. Puppeteer/CDP with an+# explicit browser.close() shoots AND exits (same pipeline as adom-wiki-hero-image).+CHROME=$(ls -d /home/adom/.cache/puppeteer/chrome/linux-*/chrome-linux64/chrome | head -1)++adom-cli carbon user avatar-upload ~/project/avatars/<name>-v1.png   # 204 = success+CTRL="$(cat ~/.adom/hydrogen-control-url)"+curl -s -X POST "$CTRL/user/refresh"      # Hydrogen repaints instantly — otherwise it looks unchanged+```++**Verify before claiming success:** `adom-cli carbon user get` should show a NEW `avatar_url`+hash. Then show them the result (inject the PNG into shotlog) rather than describing it.++## 5. Offer to archive the vector on the wiki++Once they like it, suggest a small personal wiki page holding the **SVG** plus a short note on+the concept. Why it's worth doing:+- The SVG is the perfect-resolution master — future PNGs (any size, a favicon, a talk slide, a+  business card) render from it with no quality loss.+- It survives container resets and moves with them.+- Iterations get a history: `-v1`, `-v2`, and the reasoning behind each.++Ask first — it's their identity and their page. If they'd rather keep it local, the container+copy in step 3 is enough.++## Change it from a file (when they DO have one)++```bash+adom-cli carbon user avatar-upload /path/to/image.png+adom-cli carbon user avatar-delete     # back to the generated default+```++## Why Hydrogen goes stale, and the one call that fixes it++Hydrogen caches the signed-in user at launch, so an upload succeeds while Hydrogen keeps painting the OLD+picture. `POST <control>/user/refresh` clears exactly that. Hydrogen also self-heals on its own the+next time the profile menu opens (it re-reads then) or on next launch — but the user notices the+lag, so make the call.++There is **no CDN cache to bust**: avatar URLs are content-hashed+(`cdn.adom.inc/avatars/<hash>.png`), so a different image is always a different URL.++## The user can also do it by hand++In Hydrogen: profile menu → click the avatar (camera badge on hover), or **Your profile** → *Upload a+photo* / *Remove photo*. Both go through the same Carbon endpoints. The UI is the fallback; the+prompt above is the main event.
skills/hydrogen-browser-picker/SKILL.mdadded+474
@@ -0,0 +1,474 @@+---+name: hydrogen-browser-picker+description: Hydrogen's Browser Picker — the canonical handler for EVERY URL that gets opened inside Hydrogen. Click a link anywhere (main page, code-server iframe, webview panel, PortMappingsDialog, OAuth redirect, localhost link, external link) and Hydrogen intercepts it via 5 layers (on_new_window / on_navigation / NewWindowRequested / AddScriptToExecuteOnDocumentCreated / ContextMenuRequested) and routes it through the BrowserProfileDialog so the user (or an AI driving automation) picks where it opens — native browser profile, Hydrogen tab, Hydrogen window, or Pup. 5-second auto-countdown so AIs don't block waiting on a click. Read BEFORE adding a URL-opening feature, debugging dead links, fixing `shell.open not allowed`, or automating browser interactions from Hydrogen. Trigger words — browser picker, BrowserProfileDialog, hydrogen-open-url, hydrogen-open-url-force, open with browser picker, shell.open not allowed, shell:default, shell:allow-open, plugin-shell, url interception, link click, target blank, claude auth url, adom auth url, oauth callback in browser, default browser, browser profile, AI browser automation, countdown picker, auto-timer, shift click force, right-click open with, context menu, webview links, on_new_window, hydrogen-links (legacy name).+---++# Hydrogen — Browser Picker (URL Interception)++## The Problem++Hydrogen wraps a WebView2 that contains:+- The Hydrogen SvelteKit frontend (localhost:1420 in DEV mode only; prod Hydrogen serves it via the tauri protocol)+- The code-server iframe (VS Code + Claude Code) — code-server is host port 7380 (8080 is the internal workspace port)+- Webview panel iframes (any external URL like wiki, google, etc.)++When a link is clicked anywhere in this stack, Hydrogen needs to:+1. Catch it before it opens uncontrolled+2. Route it through the Browser Picker dialog+3. Let the user choose: native browser (Chrome/Edge), Hydrogen Tab (wv), Hydrogen Browser Window (short name: **hbw**), or Pup++## The 5 Interception Layers++Each layer catches a different type of URL event. All registered in `setup_frame_navigation_interception()` in `hydrogen-app/src/lib.rs` via `webview.with_webview()`.++### Layer 1: `on_new_window` (Tauri wrapper)+**Catches:** `window.open()`, `<a target="_blank">` from the TOP-LEVEL webview and direct child frames+**Action:** Deny ALL. Dispatch to Browser Picker via CustomEvent.+**Shift detection:** Yes — `GetAsyncKeyState(0x10)` checks if Shift is held, dispatches `hydrogen-open-url-force` for Browser Picker override.+**Debounce:** 500ms to prevent event loops.++### Layer 2: `on_navigation` (Tauri wrapper)+**Catches:** Top-level frame navigations (someone navigating the main webview to an external URL)+**Action:** Allow localhost/tauri/data/about. Block all external HTTPS. Dispatch to Browser Picker.++### Layer 3: `add_FrameNavigationStarting` (raw COM)+**Catches:** ALL child frame navigations (iframe navigating itself)+**Action:** Log only — don't block. Webview panels legitimately load external URLs in iframes.+**Why not block:** Blocking breaks webview panels (they need external URLs). Same-domain navigations (wiki → wiki/skills) work fine.++### Layer 4: `add_NewWindowRequested` (raw COM)+**Catches:** New window requests that Tauri's wrapper might miss+**Action:** `SetHandled(true)` for external URLs. No event dispatch (Tauri's handler does that).+**Purpose:** Safety net — prevents any uncontrolled window from being created.++### Layer 5: `AddScriptToExecuteOnDocumentCreated` (WebView2 API)+**Catches:** `<a target="_blank">` clicks inside cross-origin iframes+**Why needed:** Cross-origin iframe `target="_blank"` clicks don't fire `NewWindowRequested` on the parent WebView2. This is a WebView2 security boundary.+**How:** Injects a click listener into ALL frames (including cross-origin) that intercepts `target="_blank"` clicks and dispatches a CustomEvent to `window.top`.+**Survives:** Page reloads, iframe navigations — WebView2 auto-injects on every document load.++### Layer 6: `add_ContextMenuRequested` (raw COM, ICoreWebView2_11)+**Catches:** Right-click on links+**Action:** Adds "Open Link with Browser Picker" or "Open Page with Browser Picker" to the native WebView2 context menu.+**How:** Cast `ICoreWebView2` → `ICoreWebView2_11`, register handler. On right-click, check `HasLinkUri()`, create custom menu item via `ICoreWebView2Environment9.CreateContextMenuItem()`, register `add_CustomItemSelected` handler that dispatches `hydrogen-open-url-force`.++## What Each Click Type Triggers++| User action | Where | Layer that catches | Result |+|---|---|---|---|+| Left-click `<a target="_blank">` in webview panel iframe | Cross-origin iframe | Layer 5 (JS script) | Browser Picker |+| Left-click regular `<a href>` in webview panel iframe | Cross-origin iframe | Layer 3 (logged) | Navigates iframe normally |+| Right-click → "Open in new window" | Any iframe | Layer 1 (on_new_window) | Browser Picker |+| Right-click → "Open Link with Browser Picker" | Any link | Layer 6 (context menu) | Browser Picker (force) |+| Shift+click any link | Anywhere | Layer 1 + shift detection | Browser Picker (force, no auto-timer) |+| `window.open()` from code-server | Code-server iframe | Layer 1 (on_new_window) | Browser Picker |+| `vscode.env.openExternal()` from Claude Code | Code-server → window.open | Layer 1 (on_new_window) | Browser Picker |+| Claude Code auth button | Code-server extension | Layer 1 (on_new_window) | Claude-branded Browser Picker |++## Browser Picker Dialog++`src/lib/components/BrowserProfileDialog.svelte`++### 3 Branded Variants+1. **Claude auth** — Claude logo, "Connect to Claude Code" (domains: `*.claude.ai`, `*.claude.com`)+2. **Adom auth** — Adom logo, "Sign in to Hydrogen" (domains: `*.adom.inc` + `/auth/intent`)+3. **General link** — "Browser Picker" title, no logo++### Consistent Elements (all variants)+- "Browser Picker" title at top+- Native Browsers section with real Google profile avatars+- Hydrogen section (collapsible, collapsed for auth)+- Details section (collapsible, collapsed) — URL, remember checkbox, trigger info, build version+- 5-second auto-timer with progress bar (cancelled by mouse movement)+- Keyboard navigation (arrow keys + Enter)+- 800ms delayed tooltips++### Auto-Timer Behavior+- Picks best default: work browser profile (non-gmail email) for auth, Pup for right-click/iframe opens+- 5-second countdown with progress bar+- ANY mouse movement cancels the timer+- Shift+click (force-show) disables timer entirely+- Selected destination highlighted with teal border++### ⭐ Fresh-window option (first-class)+A **freshly-launched browser window/process auto-foregrounds on Windows** — focus-stealing+prevention does NOT block a process's own first window. A URL opened as a *tab* in an+already-running browser does NOT reliably come to the front. So "fresh" is the proven way+to make the **Claude/Adom consent page pop to the front by itself** (removes the manual+foreground step that used to block hands-free auth).++- Mechanism: launch the browser exe with `--new-window` (Chromium) / `-new-window`+  (Firefox) before the URL, instead of `open::that(url)` (shell-open can't force a window).+- UI: an **"Open in a fresh window"** checkbox above the Native Browsers list. **Defaulted+  ON for auth URLs** (Claude/Adom), OFF otherwise. Only native-browser destinations honor+  it; internal Hydrogen destinations ignore it. The dialog dispatches `select` with+  `fresh: dest.category==='native' ? freshWindow : false`; `onProfileSelect` threads it to+  `open_in_browser_profile` / `/open-in-profile`.+- `fresh` is honored by: Tauri `open_in_browser_profile` (direct), `POST /open-in-profile`+  (direct), and `POST /open-url` (where it **pre-seeds the picker's toggle**; add+  `direct:true` to open immediately instead). Direct paths echo+  `{"fresh":…, "method":"<exe … fresh-window>"}`.+- Dev Toolbar A/B debug buttons: **▶ Open URL — FRESH window** vs **▶ Open URL — normal**.++### URL Event Deduplication+Frontend deduplicates URL events within 1-second window (same URL) to prevent double-open from multiple layers firing for the same click. Force events (Shift) bypass dedup.++### Prefs Sync+Preferences re-read from localStorage on every dialog open. If user deletes a domain in Browser Picker Manager, the next dialog open reflects it immediately.++## Custom Context Menu Implementation++Uses raw COM API via `ICoreWebView2_11` (cast from `ICoreWebView2` using `windows::core::Interface::cast`).++Key COM types used:+- `ICoreWebView2_11` — has `add_ContextMenuRequested`+- `ICoreWebView2Environment9` — has `CreateContextMenuItem`+- `ICoreWebView2ContextMenuTarget` — `HasLinkUri()`, `LinkUri()`, `PageUri()`+- `ICoreWebView2ContextMenuItemCollection` — `Count()`, `InsertValueAtIndex()`+- `ICoreWebView2ContextMenuItem` — `add_CustomItemSelected()`+- `COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND` — command-type menu item++webview2-com 0.38 handler types used: `ContextMenuRequestedEventHandler`, `CustomItemSelectedEventHandler`++### How we got the context menu working (the full story)++**Initial assumption:** webview2-com 0.38 doesn't have context menu types. WRONG.++**First attempt:** Upgrade to webview2-com 0.39 which obviously has them. FAILED — 0.39 requires `windows 0.62` but Tauri pins `windows 0.61`. The upgrade broke 15+ API call sites due to `PCWSTR`/`HSTRING` type changes.++**Second attempt:** Write raw COM vtable bindings for ICoreWebView2_11 manually. UNNECESSARY.++**The discovery:** webview2-com 0.38 DOES export all the types. They're in `webview2_com::Microsoft::Web::WebView2::Win32::*` — the same module as everything else. The standard `.cast()` pattern works:++```rust+use windows::core::Interface;+use webview2_com::Microsoft::Web::WebView2::Win32::{+    ICoreWebView2_11,+    ICoreWebView2Environment9,+    COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND,+};++// Cast from base ICoreWebView2 — no raw vtable, no version upgrade+let core11: ICoreWebView2_11 = core.cast()?;+let env9: ICoreWebView2Environment9 = env.cast()?;+```++**The BOOL gotcha:** The one type that didn't work was `windows::Win32::Foundation::BOOL`. In windows 0.61, `BOOL(0)` isn't valid (it's not a tuple struct). `BOOL::default()` also fails (can't find it in the module). The fix is a raw cast:++```rust+let has_link = {+    let mut val = 0i32;+    target.HasLinkUri(&mut val as *mut i32 as *mut _)?;+    val != 0+};+```++COM `BOOL` is just an `i32` under the hood (0 = false, non-zero = true). Casting the pointer works perfectly.++**The full chain:**+```rust+// 1. Cast to ICoreWebView2_11+let core11: ICoreWebView2_11 = core.cast()?;++// 2. Register ContextMenuRequested handler+core11.add_ContextMenuRequested(&handler, &mut token)?;++// 3. In the handler: get the right-click target+let target = args.ContextMenuTarget()?;++// 4. Check if it's a link (BOOL workaround)+let mut val = 0i32;+target.HasLinkUri(&mut val as *mut i32 as *mut _)?;++// 5. Get the link URL+let mut uri = PWSTR::null();+target.LinkUri(&mut uri)?;+let url = uri.to_string().unwrap_or_default();++// 6. Create custom menu item via Environment9+let env9: ICoreWebView2Environment9 = env.cast()?;+let item = env9.CreateContextMenuItem(&label, None, KIND_COMMAND)?;++// 7. Add click handler+item.add_CustomItemSelected(&click_handler, &mut token)?;++// 8. Insert at top of context menu+let items = args.MenuItems()?;+items.InsertValueAtIndex(0, &item)?;+```++**Lesson:** Don't assume a crate is missing types because the docs are sparse. The `webview2_com::Microsoft::Web::WebView2::Win32::*` re-export brings in the ENTIRE WebView2 COM type library. If the COM interface exists in the WebView2 SDK, the Rust binding exists too — you just need to cast to the right version interface.++## AddScriptToExecuteOnDocumentCreated Script++```js+(function() {+    if (window.__hdLinkIntercept) return;+    window.__hdLinkIntercept = true;+    document.addEventListener('click', function(e) {+        var a = e.target.closest('a[target="_blank"]');+        if (!a) return;+        var href = a.href;+        if (!href || href.startsWith('javascript:')) return;+        if (new URL(href).hostname === window.location.hostname) return;+        e.preventDefault();+        e.stopPropagation();+        try {+            window.top.dispatchEvent(new CustomEvent('hydrogen-open-url', {+                detail: JSON.stringify({+                    url: href,+                    trigger: 'target_blank_click',+                    shiftHeld: e.shiftKey+                })+            }));+        } catch(err) {+            window.parent.postMessage({+                type: 'hydrogen-open-url', url: href, trigger: 'target_blank_click'+            }, '*');+        }+    }, true);+})();+```++**Why same-domain is allowed:** `if (new URL(href).hostname === window.location.hostname) return;` — wiki links to other wiki pages should navigate the iframe normally. Only cross-domain links get intercepted.++## WebView2 Event Architecture — What We Learned++### How to access raw WebView2 COM APIs from Tauri++Tauri exposes the platform webview via `webview.with_webview(|platform_webview| { ... })`. The closure runs on the main (UI) thread where COM objects live.++```rust+let controller = platform_webview.controller();  // ICoreWebView2Controller+let env = platform_webview.environment();         // ICoreWebView2Environment+let core = controller.CoreWebView2().unwrap();    // ICoreWebView2+```++From the base `ICoreWebView2`, you can cast to higher interfaces:+```rust+use windows::core::Interface;+let core4: ICoreWebView2_4 = Interface::cast(&core).unwrap();   // for add_FrameCreated+let core11: ICoreWebView2_11 = core.cast().unwrap();             // for add_ContextMenuRequested+let env9: ICoreWebView2Environment9 = env.cast().unwrap();       // for CreateContextMenuItem+```++### WebView2 event handler types in webview2-com 0.38++The crate provides `*EventHandler::create(Box::new(|sender, args| { ... }))` factory methods:+- `NavigationStartingEventHandler` — for `add_NavigationStarting` / `add_FrameNavigationStarting`+- `NewWindowRequestedEventHandler` — for `add_NewWindowRequested`+- `ContextMenuRequestedEventHandler` — for `add_ContextMenuRequested`+- `CustomItemSelectedEventHandler` — for context menu item click+- `AddScriptToExecuteOnDocumentCreatedCompletedHandler` — callback for script registration+- `FrameCreatedEventHandler` — for `add_FrameCreated` (on ICoreWebView2_4)++All handlers register with a `&mut i64` token parameter (for later removal).++### The event token pattern+```rust+let mut token = 0i64;+core.add_FrameNavigationStarting(&handler, &mut token)?;+// token is now set — save it to call remove_FrameNavigationStarting later+```++### Which events fire at which iframe depth++| Event | Top-level | Direct child iframe | Cross-origin child iframe |+|---|---|---|---|+| `NavigationStarting` (on_navigation) | Yes | No | No |+| `FrameNavigationStarting` | N/A | Yes | Yes (same-domain only for some) |+| `NewWindowRequested` (on_new_window) | Yes | Yes (same-origin) | **NO** (cross-origin) |+| `ContextMenuRequested` | Yes | Yes | Yes |++The critical gap: **cross-origin iframe `target="_blank"` clicks don't fire `NewWindowRequested`** on the parent. This is a WebView2 security boundary, not a bug.++### AddScriptToExecuteOnDocumentCreated — the secret weapon++This is a WebView2 configuration API, not eval injection. Key properties:++1. **Registered once** on `ICoreWebView2`, applies forever+2. **Runs in ALL frames** — top-level, child iframes, cross-origin iframes, nested iframes+3. **Runs BEFORE page JavaScript** — your script is guaranteed to execute first+4. **Survives navigations** — when the frame navigates to a new page, the script runs again+5. **Survives reloads** — F5, location.reload(), everything+6. **Controlled by WebView2** — not by the page, not by JS, not by CORS++This is how Electron handles preload scripts. It's the ONLY reliable way to run code inside a cross-origin iframe without the iframe's cooperation.++```rust+let script = windows::core::HSTRING::from("(function() { /* your code */ })();");+let handler = webview2_com::AddScriptToExecuteOnDocumentCreatedCompletedHandler::create(+    Box::new(|_, _| Ok(()))+);+core.AddScriptToExecuteOnDocumentCreated(&script, &handler)?;+```++The script has access to:+- `window.top` — can dispatch CustomEvents to the top-level page (works even cross-origin in WebView2)+- `window.parent.postMessage()` — fallback if `window.top` access is blocked+- `document.addEventListener()` — full DOM access within the frame+- `window.location` — the frame's current URL++We use it to intercept `target="_blank"` clicks that would otherwise be invisible to the Rust layer.++### COM method signature patterns in webview2-com 0.38++The types use a mix of safe wrappers and raw out-parameters:++**Safe wrappers (return `Result<T>`):**+```rust+let target = args.ContextMenuTarget()?;    // returns Result<ICoreWebView2ContextMenuTarget>+let items = args.MenuItems()?;              // returns Result<ICoreWebView2ContextMenuItemCollection>+let item = env9.CreateContextMenuItem(&label, None, KIND_COMMAND)?; // returns Result<ICoreWebView2ContextMenuItem>+```++**Out-parameter methods:**+```rust+let mut has_link = windows::Win32::Foundation::BOOL(0);+target.HasLinkUri(&mut has_link)?;          // writes to has_link+if has_link.as_bool() { ... }++let mut uri = windows::core::PWSTR::null();+target.LinkUri(&mut uri)?;                  // writes to uri+let url = uri.to_string().unwrap_or_default();++let mut count = 0u32;+items.Count(&mut count)?;                   // writes to count+```++**Event registration:**+```rust+let mut token = 0i64;+menu_item.add_CustomItemSelected(&handler, &mut token)?;+items.InsertValueAtIndex(count, &menu_item)?;+```++### Cargo.toml requirements++The `Win32_Foundation` and `Win32_System_Com` features must be enabled on the windows crate for BOOL and COM types:+```toml+[target.'cfg(windows)'.dependencies]+webview2-com = "0.38"+windows = { version = "0.61", features = [+    "Win32_Foundation",+    "Win32_System_Com",+    "Win32_System_WinRT",+    ...+] }+```++## Hard-Won Lessons++### `NewWindowResponse::Allow` must NEVER be returned+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/hydrogen-app/src/lib.rs` | All 6 interception layers, dispatch_url_event, context menu |+| `src/lib/components/BrowserProfileDialog.svelte` | Browser Picker dialog (3 variants, auto-timer, keyboard nav) |+| `src/lib/components/editor/EditorNav.svelte` | Browser Picker Manager modal |+| `src/routes/+layout.svelte` | CustomEvent listeners, URL event parsing, deduplication |+| `src/lib/assets/claude-logo.svg` | Real Claude logo from claude.ai/favicon.svg |+| `src/lib/assets/adom-logo-teal.svg` | Adom brand logo |+| `src/lib/buildVersion.ts` | Build version tracking for verification |+| `src-tauri/capabilities/default.json` + `src-tauri/crates/hydrogen-app/capabilities/default.json` | Tauri permissions — MUST include `shell:default` AND `shell:allow-open` |++## The 5-second auto-countdown (why AI loves it)++The picker opens with a countdown on its best-default destination. If nothing interrupts, the URL auto-routes there. This is the key feature for AI automation: triggers like OAuth redirects, "open in browser" links, or wiki references work hands-free.++- **AI scripts can fire a URL open and the right thing happens within 5s** — no clicking, no blocking on a dialog.+- **Mouse movement cancels the timer** — if a human is at the keyboard, just wiggle the mouse and the picker waits.+- **Shift+click disables the timer entirely** — forces an explicit human choice.++Best-default heuristic:+- `*.claude.ai` / `*.claude.com` → "Claude auth" variant, defaults to work browser profile+- `*.adom.inc` + `/auth/intent` → "Adom auth" variant, defaults to work browser+- Right-click / iframe / unknown context → Pup (AI-drivable browser)+- General → user's last pick for that domain++## Localhost URLs route through the picker too++**Rule:** ALL http(s) URLs go through the picker, including `http://localhost:*` and `http://127.0.0.1:*`. PortMappingsDialog's container-service links must open in the user's chosen browser, NOT in a popup-blocked WebView2 child window.++The `on_new_window` handler in `lib.rs` (line ~1986) MUST dispatch `hydrogen-open-url` for both internal AND external URLs. If you see `is_internal` short-circuiting the dispatch with no `dispatch_url_event` call, that's the bug — internal URLs fall through to Tauri's default `shell.open()` which then fails with the permission error below.++## `shell:default` is non-negotiable++The picker's "open in native browser" path ultimately calls `@tauri-apps/plugin-shell.open()`. That requires BOTH `shell:allow-open` AND `shell:default` in BOTH capability files. Without `shell:default` you get this exact rejection in the Hydrogen log:++```+[JS UNHANDLED REJECTION] shell.open not allowed. Permissions associated with this command: shell:allow-open, shell:default+```++Both files must list both permissions:+- `src-tauri/capabilities/default.json`+- `src-tauri/crates/hydrogen-app/capabilities/default.json` (the hydrogen-app crate's own capabilities)++Tauri compiles capabilities INTO the binary at build time. Capability changes require a Rust rebuild (`hd_build_rust`), not just a frontend rebuild.++## AI automation recipe++```bash+# 1. (Optional) Set Pup as default for domains you want to drive+#    Hydrogen UI → Settings → Browser Picker Manager → add domain → choose Pup+#    OR let the 5s auto-countdown pick Pup for unknown URLs.++# 2. Trigger a URL open (read the live control URL — the port is dynamic, default 47084)+adom-bridge-cli shell_execute '{"command":"CTRL=\"$(cat ~/.adom/hydrogen-control-url)\"; curl -s -X POST -d {\"url\":\"https://example.com\"} \"$CTRL/open-url\""}'++# 3. Within 5s, picker auto-routes to Pup (or saved pref)+# 4. Drive Pup with adom-bridge-cli browser_* verbs+adom-bridge-cli browser_eval '{"sessionId":"default","js":"document.title"}'++# Force a picker dialog (no auto-timer)+adom-bridge-cli shell_execute '{"command":"CTRL=\"$(cat ~/.adom/hydrogen-control-url)\"; curl -s -X POST -d {\"url\":\"https://example.com\",\"force\":true} \"$CTRL/open-url\""}'++# Route through the picker with the fresh-window toggle PRE-CHECKED (force = skip the+# 5s timer). The user/AI then picks a native browser → opens fresh + auto-foregrounds.+adom-bridge-cli hd_api '{"method":"POST","path":"/open-url","body":{"url":"https://claude.ai/","fresh":true,"force":true}}'+# BYPASS the picker and open directly in a specific browser+profile, fresh:+adom-bridge-cli hd_api '{"method":"POST","path":"/open-in-profile","body":{"url":"https://claude.ai/","browser":"edge","profileDir":"Default","fresh":true}}'+# /open-url with direct:true also bypasses the picker (opens immediately, default browser):+adom-bridge-cli hd_api '{"method":"POST","path":"/open-url","body":{"url":"https://claude.ai/","fresh":true,"direct":true}}'+```++`/open-url` ROUTES THROUGH THE PICKER by default (emits `browser-open-request`):+`fresh` pre-seeds the "Open in a fresh window" toggle, `force:true` skips the 5s+auto-timer, `direct:true` (or naming a concrete `browser`) bypasses the picker and+opens immediately. The response `routed` field is `"browser-picker"` or `"direct"`.+`/open-in-profile` always opens directly (no picker). Both direct paths echo+`method:"msedge.exe … fresh-window"` — read it before claiming success.++## Prefs are now an API — the picker is a FALLBACK, not a gate++The per-domain routing memory is CRUD over the control API, so the **AI governs routing** and+the picker only pops for a domain with no policy + no direct call:+`GET /browser-picker/prefs` (read `{domains:{<host>:{destination,label}}}`),+`PUT /browser-picker/prefs {domain,destination,label?}` (set/override),+`DELETE /browser-picker/prefs/<host>` (forget one), `DELETE /browser-picker/prefs` (clear all).+`destination` is an ID: `hydrogen-tab` (wv) | `webview-window` (**hbw** — Hydrogen's standalone Tauri browser window; also `POST /hbw/open`) | `pup-window` (pup), or a native id (**nb**): users address these as `nb <browser> [profile]` — "nb chrome work", "nb edge" — resolve via GET /browser-profiles; the id form is+`browser:<browser>:<dir>` (profile dir encoded in the ID). `fresh` is per-open, not stored.+Writes hit the Manager's live state (localStorage + `hydrogen-url-routing-prefs-changed`). Full+routing-policy guidance lives in **[hydrogen-open-url](../hydrogen-open-url/SKILL.md)**.++## How Hydrogen skills get deployed (workflow note)++Hydrogen public skills (this one, hydrogen-open-url, hydrogen-monitor, etc.) live in the Hydrogen repo under `skills/public-facing/{shared,wsl2}/<name>/SKILL.md`. They're BUNDLED into the Hydrogen bootstraps (adom/hydrogen-bootstrap ships `shared/`, adom/hydrogen-windows-bootstrap ships `wsl2/`) and deployed flat into `~/.claude/skills/<name>/` at install/bake time. See `hydrogen-skill-catalog` (public) and `hydrogen-skills-structure` (dev-internal) for the full layout.
skills/hydrogen-claude-management/SKILL.mdadded+214
@@ -0,0 +1,214 @@+---+name: hydrogen-claude-management+description: >+  First-class API for managing the Claude Code conversations inside Hydrogen:+  list/open/close/switch conversation tabs, read each conversation's full state (title,+  message count, compose box, and the on-disk session GUID), type messages into a SPECIFIC+  conversation (instant or with a human typing animation) and submit them, and turn on+  Remote Control to get a claude.ai cloud URL so the user can view+drive that exact+  conversation from claude.ai or the Claude mobile app. Every conversation is addressed by a+  stable execution-context id (deterministic, background, no foreground/focus needed). Also+  explains how a live conversation is matched to its ~/.claude/projects/*.jsonl log.+  Trigger words: claude tabs, claude conversations, list conversations, open claude tab,+  close claude tab, switch claude tab, new conversation, type into claude, inject prompt,+  remote control, claude.ai url, view on mobile, session id, conversation guid, jsonl,+  claude session, context_id, multi-tab claude, conversation management.+---++# Hydrogen Claude Conversation Management++Hydrogen runs Claude Code as webview tabs inside VS Code. This skill is the+**first-class API** for managing those conversations programmatically — from Hydrogen's control+API, from the user's AI inside the WSL2 workspace, or from setup steps.++All endpoints are on the **Hydrogen control API** (a dynamic localhost port). From inside the+workspace, examples below use `$Hydrogen` for `http://127.0.0.1:<port>` (find the port in the Hydrogen+log / port discovery). From an **external** context (a cloud container), call the same+endpoints with the ab verb `adom-bridge-cli hd_api '{"path":"/claude/…","method":"POST","body":{…}}'`+— it auto-discovers the port. Everything here works in the **background** — no window focus,+no foreground, no keystroke stealing.++## The core idea: address conversations by CONTEXT ID, list from the TAB BAR++Each Claude conversation is its own webview **execution context** with a **stable integer id**.+That id — not the tab index, not the title, not visibility — is how you reliably *target* one+conversation (type into it, remote-control it). VS Code keeps multiple webviews "visible" at+once, so visibility/tab-index are NOT reliable addresses; the context id is.++But VS Code also **disposes the webview of a background tab**, so a context id only exists for+*loaded* tabs. That's why `GET /claude/conversations` **merges three sources** into one view:+the **tab bar** (every open conversation + which is active — what you see), the **live webview+contexts** (rich data for loaded tabs), and the **on-disk session logs** (the GUIDs). Each row+carries a `loaded` flag; `loaded:false` means activate the tab first to read its live details.++## GET /claude/conversations — everything in one call++```bash+curl -s $Hydrogen/claude/conversations+```+```jsonc+{+  "ok": true,+  "count": 4,+  "conversations": [+    {+      "ordinal": 3,                 // its position in the tab bar+      "active": true,               // the currently-focused tab+      "loaded": true,               // webview is live → rich fields are populated+      "context_id": 7,              // STABLE address for inject/remote-control (null if not loaded)+      "title": "Calculate 10 times 10",   // authoritative title (from the session log when matched)+      "tab_label": "Calculate 10 times 10", // raw tab label (may be ellipsis-truncated)+      "compose_text": "",           // what's typed but not yet sent (null if not loaded)+      "msg_count": 16,              // null if not loaded+      "session_id": "566cec21-…",   // on-disk log GUID (read ~/.claude/projects/*/<id>.jsonl)+      "rc_active": false,           // Remote Control on? (null if not loaded)+      "remote_control_url": null    // claude.ai cloud URL when rc_active+    },+    { "ordinal": 1, "active": false, "loaded": false, "context_id": null,+      "title": "Find the capital of France", "tab_label": "Find the capital of Fran…",+      "compose_text": null, "msg_count": null, "session_id": "b352055b-…", "rc_active": null, "remote_control_url": null }+  ]+}+```+- **Merged view:** every tab is listed (from the tab bar). `loaded:true` rows have live fields+  (`context_id`, `compose_text`, `msg_count`, `rc_active`); `loaded:false` rows are background tabs+  VS Code disposed — **activate them** (`/claude/tabs/activate`) to read those. Even a disposed tab+  resolves its `session_id` when its title matches the on-disk log.+- A conversation is **`Untitled`** with **`session_id: null`** until its first message is submitted —+  then Claude names it and writes the session log.+- `?with_session=false` skips the disk read (faster; omits `session_id`).++`GET /claude/contexts` is the lighter version (just the *loaded* contexts: `context_id` + `compose_text`).++## Open / close / switch tabs++```bash+# Open a fresh conversation (new tab)+curl -s -X POST $Hydrogen/claude/new-conversation++# Open the Claude panel if it isn't open+curl -s -X POST $Hydrogen/claude/open++# List raw editor tabs (label/active/dirty)+curl -s $Hydrogen/claude/tabs++# Close ALL conversation tabs (also removes the leftover empty editor group)+curl -s -X POST $Hydrogen/claude/tabs/close-all++# Switch the visible tab by index or label substring (background)+curl -s -X POST $Hydrogen/claude/tabs/activate -d '{"index":1}'+curl -s -X POST $Hydrogen/claude/tabs/activate -d '{"label":"Eiffel"}'++# Remove a stale EMPTY editor group (the dead blank panel) WITHOUT collapsing a+# real 2-group left/right layout. (merge-groups is a back-compat alias.)+curl -s -X POST $Hydrogen/claude/tabs/close-empty-groups+```++## Type a message into a SPECIFIC conversation++`POST /claude/inject` types into the conversation you name with `context_index`. Two speeds:++```bash+# INSTANT paste (fast) — no typing animation. Best for slash commands and scripted input.+curl -s -X POST $Hydrogen/claude/inject -d '{"text":"summarize this repo","context_index":16,"typed":false,"submit":true}'++# SLOW human typing animation (the "watch the AI type" effect), then submit.+curl -s -X POST $Hydrogen/claude/inject -d '{"text":"hello there","context_index":16,"typed":true,"delay_ms":50,"submit":true}'++# Type but DON'T send (leave it sitting in the compose box):+curl -s -X POST $Hydrogen/claude/inject -d '{"text":"draft…","context_index":16,"typed":false,"submit":false}'+```+- `typed`: `true` = char-by-char with ±50% jitter (avg `delay_ms`); `false` = instant paste.+- `submit`: `true` = press Enter; `false` = leave in the box.+- Omit `context_index` ONLY when a single conversation is open. With **more than one** conversation+  open, omitting it can type your text into **several compose boxes at once** (it falls back to a+  visibility heuristic that isn't reliable when the window isn't foreground). When in doubt, pass it.+- Targeting by `context_index` is deterministic — it lands in exactly that tab even when it's+  not the foreground one and even with many tabs open (no fan-out into other tabs). **This is the+  safe default: list `/claude/conversations`, take the row with `active:true` (or the tab you want),+  use its `context_id`, then inject.**++Typical "open a new tab and ask a question":+```bash+curl -s -X POST $Hydrogen/claude/new-conversation+CTX=$(curl -s $Hydrogen/claude/contexts | jq '.contexts[-1].context_id')+curl -s -X POST $Hydrogen/claude/inject -d "{\"text\":\"what does this project do?\",\"context_index\":$CTX,\"typed\":true,\"submit\":true}"+```++## ⭐ Remote Control → view the conversation on claude.ai / mobile++This is the big one. Turn on Remote Control for a conversation and Hydrogen returns a **claude.ai+cloud URL**. Open it on claude.ai in any browser or in the Claude **mobile app** to see and+drive that exact conversation in the cloud — not tied to Hydrogen. The user can be on their phone+editing the same conversation.++```bash+curl -s -X POST $Hydrogen/claude/remote-control -d '{"context_index":7}'+```+```jsonc+{+  "ok": true,+  "rc_active": true,+  "toggled": true,                 // false + already_in_desired_state:true if it was already on+  "context_id": 7,+  "remote_control_url": "https://claude.ai/code/session_014koKPJxmjYpnX2SjRRqrD3"+}+```+- ⚠️ **`/remote-control` is a TOGGLE.** This endpoint **detects** the current state first and only+  toggles when needed — re-calling it on an already-on session is a **no-op** that returns the+  existing URL (it will NOT turn it off; doing that blindly archives the cloud session).+- `{enable:false}` turns Remote Control **off**.+- The `session_…` id in the URL is the **cloud** session id — distinct from the local on-disk+  `session_id` GUID. The cloud URL is the shareable one for claude.ai / mobile.+- `{name}` → `/remote-control <name>` to label the remote session.+- Each conversation's current state is also reported as `rc_active` in `/claude/conversations`.+- **Opening it on the user's machine (external/cloud agent):** get the URL, then open it in their+  Chrome and screenshot to confirm — all via ab verbs (no synthetic keystrokes):+  `desktop_find_window {titleContains:"Claude Code - Google Chrome"}` → `desktop_screenshot_window+  {hwnd}`. To type INTO the claude.ai page, use the native-browser bridge `nbrowser_eval+  {tabId, expression}` (the composer is shadow-DOM; do NOT blind-type with SendInput). See the+  `driving-hydrogen-remotely` guide on the Hydrogen wiki page.++## How a live conversation maps to its on-disk log (JSONL)++Claude Code stores each conversation as `~/.claude/projects/<cwd>/<uuid>.jsonl` containing+`sessionId`, `aiTitle` (the generated title), and every message. **The session GUID is NOT+exposed in the conversation's webview DOM**, so Hydrogen correlates a live conversation to its log by+matching the **first user message** (exact + unique; present in both the DOM and the JSONL).+On a match, `/claude/conversations` fills in `session_id` and uses the log's `aiTitle` as the+authoritative `title`. (Matching on the title alone is unreliable — the rendered title and the+persisted `aiTitle` can differ.)++Read a conversation's full transcript from the workspace:+```bash+cat ~/.claude/projects/*/<session_id>.jsonl+```++## Why this is valuable++- **Multi-conversation orchestration:** open N conversations, drive each independently by+  context id, read each one's state — all from a script or an agent, in the background.+- **Cloud + mobile continuity:** Remote Control hands you a claude.ai URL so a conversation+  started in Hydrogen continues on the phone / web. Huge for working away from the desk.+- **Deterministic + safe:** context-id addressing never types into the wrong tab; the empty+  blank panel is cleaned up automatically; a real 2-group left/right layout is preserved.+- **Auditable:** every conversation maps to an on-disk JSONL you can read, diff, or archive.++## Endpoint quick reference++| Endpoint | Method | What it does |+|---|---|---|+| `/claude/conversations` | GET | Full per-conversation data incl. `session_id` (the canonical list) |+| `/claude/contexts` | GET | Light list: `context_id` + `compose_text` |+| `/claude/tabs` | GET | Raw editor tabs (label/active/dirty) |+| `/claude/new-conversation` | POST | Open a fresh conversation tab |+| `/claude/open` | POST | Open the Claude panel if closed |+| `/claude/tabs/close-all` | POST | Close all tabs + remove the leftover empty group |+| `/claude/tabs/activate` | POST | Switch visible tab by `{index}` or `{label}` |+| `/claude/tabs/close-empty-groups` | POST | Remove stale empty editor groups (preserves populated) |+| `/claude/inject` | POST | Type into `{context_index}` — `{typed,submit,delay_ms}` |+| `/claude/remote-control` | POST | Turn on RC for `{context_index}` → returns `remote_control_url` |+| `/claude/cross-eval` | POST | (Diagnostic) run JS across all workbench/webview contexts |++These are the same endpoints Hydrogen's own setup steps use to open Claude and inject the first+prompt — so behavior is identical whether a human, setup, or the workspace AI drives them.
skills/hydrogen-demo-recording/SKILL.mdadded+176
@@ -0,0 +1,176 @@+---+name: hydrogen-demo-recording+description: >+  THE recipe for recording a demo video of Hydrogen: screen-record an AI prompt+  being TYPED (visible human-jitter typing) into a Claude tab plus the AI's resulting+  output, then speed the footage 10x to 30x in video-post and lay an adom-tts voiceover+  on top, so a viewer sees exactly what using an app in Hydrogen feels like. Covers the whole+  pipeline end to end: driving Hydrogen's control API (in-workspace OR from a remote container+  via adom-bridge-cli hd_api), the high-fps native recorder, multi-tab multi-prompt demos,+  getting the mp4 out (streamed binary, never base64), and the post-production handoff.+  Trigger words: demo video, record a demo, hd demo, demo recording, record the typing,+  screen record hd, record ai prompt, app demo, demo pipeline, speedup demo, tts overlay+  demo, show what the app feels like, hydrogen-demo-recording.+---++# hydrogen-demo-recording: record an Hydrogen demo video, end to end++The canonical Adom demo: the viewer watches an AI prompt get TYPED into Hydrogen+Desktop, watches the AI do the work, and hears a narrator explain it, all in a tight+sped-up clip. This skill is the full pipeline. It exists because every piece is easy+to get slightly wrong and the pieces live in four different tools.++The shape of every demo:++1. Start Hydrogen's native high-fps recorder (no picker, no sharing banner).+2. Type the prompt into a Claude tab with VISIBLE typing (the sexy part) and submit.+3. Let the AI's output stream on camera.+4. Stop the recorder, collect the mp4.+5. Post-production: speed up 10x to 30x (video-post), overlay adom-tts narration.++## Step 0: pick your calling transport++Everything below is Hydrogen control-API calls. Two contexts:++**Inside the Hydrogen workspace** (the WSL distro): the live base URL is in a file, never an+env var:++```bash+CTRL=$(cat ~/.adom/hydrogen-control-url)+curl -s -X POST "$CTRL/claude/inject" -H 'Content-Type: application/json' -d '{...}'+```++**From a remote container** (hw, cloud): go through the adom-bridge-cli relay's+pass-through verb. Never curl through the relay (quoting gets mangled) and never hunt+for the port (hd_api auto-discovers it):++```bash+adom-bridge-cli hd_api '{"method":"POST","path":"/claude/inject","body":{...}}'+```++`hd_*` verbs auto-route to the desktop running Hydrogen; `desktop_*` and `pull_file` need+`--target <desktop name>` when more than one desktop is on the relay.++## Step 1: preflight (30 seconds, saves the take)++- `GET /health` answers with service `hydrogen-desktop-control` (not the SPA).+- `GET /capture/availability`: nothing else is recording; it also tells you which+  screenshot method coexists with a recording (CDP `POST /screenshot`, not the SSE path).+- `GET /recording/native/sources`: confirm the exact window name to capture.+- `GET /claude/auth-check`: Claude is signed in (a demo of a sign-in wall is not a demo).++## Step 2: start the recorder++```json+POST /recording/native/start+{"source":"self","cursor":true,"codec":"h264","maxSeconds":300,"reason":"demo take cap"}+```++- **`source:"self"` is THE way to record Hydrogen** (alias: `"hd"`). Never guess Hydrogen's window+  title: it carries the version and dev stamps, and title-guessing has recorded the+  WRONG app before (an Edge window titled "Claude"). `window:<substr>` matches other+  apps' windows; `"screen"` records the whole primary monitor.+- WGC per-window capture: high fps, background-safe, no picker, no "you are sharing+  your screen" banner.+- Overriding `maxSeconds` (default 600) REQUIRES a `reason` string, it shows in the+  UI toast if the cap fires. Omit both if the default cap is fine.+- `codec:"h264"` for web-playable output (h265 is smaller but not web-playable).+- `mic:true` only if you want live narration in the raw take; the usual pipeline adds+  TTS narration in post instead.+- Wait about 1 second after start so the first frames show the clean starting state.+- **Verify the take is ALIVE**: `GET /recording/native/status` while recording shows+  `frames` climbing within about 2 seconds. If it stays 0, the take is dead (target+  minimized or wrong window): stop now and fix, do not record 90 seconds of nothing.+  A 0-frame take errors at stop (500) instead of pretending success.++## Step 3: type the prompt(s) on camera++One call per prompt opens a NEW Claude tab, waits for its compose box, types with+human jitter, and submits:++```json+POST /claude/new-chat-and-send+{"text":"<the demo prompt>","typed":true,"submit":true,"delay_ms":35}+```++- `typed:true` is the visible typing (about `delay_ms` per char with 50 percent+  jitter). `typed:false` pastes instantly: fine for seeding, boring on camera.+- The call BLOCKS until typing completes: perfect for sequencing. If a relay call+  times out on a long prompt, the typing still finishes on Hydrogen's side. Do NOT re-fire.+  Confirm with `GET /claude/conversation-state` (turns went up), then continue.+  Keep prompts short enough to type in under about 20 seconds.+- Single-prompt demo into the CURRENT tab instead: `POST /claude/inject` with the+  same body shape.++**Multi-prompt demos** (the 5-tabs pattern): repeat the call once per prompt (each+opens its own tab), then flip through the finished answers on camera with the+BACKGROUND tab switch:++```json+POST /claude/tabs/activate  {"index":0}   // 0-based; or {"label":"substring"}+```++Verify each switch took with `GET /claude/tabs` (`active:true` moved). Pause a few+seconds per tab so the sped-up video gives each answer screen time.+`GET /claude/conversations` gives per-tab context_ids if you want to seed specific+tabs deterministically (`/claude/inject` with `context_index`).++## Step 4: stop and collect the mp4++```json+POST /recording/native/stop+```++The response carries `file` (relative), `fileAbsolute` (path INSIDE the WSL distro),+`fileWindows` (the `\\wsl$\...` UNC path), and `frames`. If your transport hiccups+and you lose the stop response, `GET /recording/native/status` when idle returns+`lastOutput` with the same paths, the take is recoverable.++- **In the workspace**: `fileAbsolute` is directly readable; you are done.+- **Remote container**: pull it as STREAMED BINARY, 1 MiB chunks, sha256-verified.+  Never base64 an mp4 through an exec channel:++```bash+adom-bridge-cli --target <desktop> pull_file '{"filePaths":["<fileWindows value>"],"saveTo":"<dir>"}'+```++## Step 5: post-production++- **Speedup**: the `video-post` skill owns this. The house style is 10x for demos+  where the output matters, 30x for long AI work sessions. Keep the typing segment+  readable (video-post can ramp: normal speed on the typing, fast on the waiting).+- **Narration**: synthesize with `adom-tts say` (it pronounces Adom terms correctly+  and caches repeats), then video-post mixes it as the audio track. Write narration+  that explains what the viewer is SEEING at each sped-up moment.+- **Recording choreography** (captions, panel staging, what makes a take clean) lives+  in the `demo-recording` skill; read it for anything fancier than a single-window take.++## Known hazards++- **An Hydrogen restart kills the take AND the demo tabs.** The recorder dies with the Hydrogen+  process (the unfinalized temp mp4 is lost) and reopened Claude tabs reset. Do not+  run a demo while anyone (including another AI thread) is rebuilding/relaunching Hydrogen+  on the same machine; if a take mysteriously vanished, check whether Hydrogen restarted+  mid-run (window title timestamp or /buildinfo).+- Tab state reads (`GET /claude/tabs`) are DOM reads: labels can be briefly empty+  while the editor reloads. Prefer `GET /claude/conversations` for stable+  context_ids + titles.+- Screenshot of the Claude panel: `POST /screenshot {target:"claude"}` captures the+  editor surface containing it (the panel is a nested webview with no own CDP target).++## Safety and cleanup (non-negotiable)++- These endpoints are disruptive tier and their responses carry a `_safety` block:+  you are typing into the user's REAL Claude panel (each submitted prompt SPENDS a+  real turn; a 5-prompt demo spends 5) and recording their REAL screen. That is the+  point of a demo, but only do it for a demo the user asked for.+- ALWAYS stop a recording you started, even when a step fails: stop first, then+  report. `GET /recording/native/status` tells you if one is live.+- NEVER call `/claude/tabs/close-all` to clean up: it closes the user's own tabs+  too. There is no per-tab close; leave demo tabs open and tell the user they exist.+- Delete junk takes you pulled; do not litter recordings/ with failed attempts.++## Related++[[hydrogen-api]] (transport + safety tiers), [[hydrogen-recording]] (recorder endpoint reference),+demo-recording (choreography), video-post (speedup + publish), adom-tts (narration).
skills/hydrogen-dock/SKILL.mdadded+163
@@ -0,0 +1,163 @@+---+name: hydrogen-dock+description: The Hydrogen dock (the app launcher rail on the right edge of the workspace) and how apps get into it. Covers the two-layer list (the adom-curated global manifest plus user-added wiki apps), the dock.json manifest an app declares at the root of its wiki page repo so the dock knows how to launch it (webview, container_command steps, ai_thread), the container_command step contract (--print-url, skip_if, await url, timeouts), the ai block (pre-canned prompt, staged vs auto-submit, new vs existing thread), the free orbital loading screen, testing a draft manifest on your own Hydrogen, and how a user or an AI adds any wiki app to the dock locally. Read this when a user says: add an app to the dock, add my app to the dock, put this wiki page in the dock, dock settings, make my app dock-ready, dock.json, dock block, dock manifest, launch steps, AI thread on launch, auto submit prompt, staged prompt, draft manifest, how do dock apps launch, app launcher, featured apps, pin an app, hover zoom, alt-pin.+---++# hydrogen-dock: the Hydrogen app launcher++The dock is the launcher rail on the right edge of the Hydrogen workspace. Every card+in it is a wiki app page. The dock resolves what to SHOW (name, brief, hero)+from the page itself and how to LAUNCH from a `dock.json` manifest the app+declares at the root of its wiki page repo. Display is never duplicated into+the dock; the page is the single source of truth.++## The list has two layers++1. **The global manifest (adom-curated).** Hydrogen fetches a public manifest and+   renders its apps. Only the adom team edits it; `featured` and `order` exist+   only here. It is world-readable at:++   `https://wiki.adom.inc/blob/bootstrap/hydrogen-bootstrap/dock/apps.json`++   Hydrogen refreshes it on launch and then on the poll cadence the manifest itself+   declares (`poll_seconds`, currently 6 hours), so curation changes never wait+   for an Hydrogen release. Want your app on the global list? Make it dock-ready+   (below), then open an issue on the adom/hydrogen-desktop wiki page asking+   for listing; the adom team vets and merges.++2. **User-added apps (self-serve, local).** Any user adds any public wiki app+   from Dock settings (the gear at the top of the rail) with `owner/slug`, or+   by asking their AI ("add adom/orbital-lab to my dock"). These render in All+   apps, are pinnable, and persist locally. They never affect other users.++## `dock.json` (what an app declares)++A file named exactly **`dock.json`** at the root of the app's wiki page repo:++```json+{+  "dock_manifest": "1.0.0",+  "webview": { "title": "Adom TSCi", "favicon": "icons/tsci.svg" },+  "launch": {+    "mode": "container_command",+    "steps": [+      { "label": "Installing Adom TSCi",+        "run": "adom-wiki pkg install adom/adom-tsci",+        "skip_if": "command -v adom-tsci",+        "timeout_seconds": 180 },+      { "label": "Starting the board viewer",+        "run": "adom-tsci serve --print-url",+        "await": "url",+        "timeout_seconds": 60 }+    ],+    "on_ready": "navigate_placeholder"+  }+}+```++Legacy fallback: when `dock.json` is absent, Hydrogen honors a `dock` block (same+`webview`/`launch` shape) in the page's `package.json`, and a legacy+`launch.ai_prompt` string (equivalent to `ai.prompt` with `submit: "auto"`,+`thread: "new"`). New apps should always ship `dock.json`.++### webview++| Field | Required | Meaning |+|---|---|---|+| `title` | yes | Webview tab title while the app is loaded |+| `favicon` | no | Tab icon, a path in the app's page repo |+| `url` | for `mode: "webview"` | Page to load directly (hosted app, or the wiki page itself) |++### launch++- `mode: "webview"`: nothing to install; the dock loads `webview.url` directly.+- `mode: "container_command"`: run `steps` in order in the user's workspace+  container, then `on_ready: "navigate_placeholder"` navigates the pre-loaded+  webview to the URL a step produced.+- `mode: "ai_thread"`: no webview at all — the click goes straight to an AI+  thread per the `ai` block.++| Step field | Required | Meaning |+|---|---|---|+| `label` | yes | Shown in the launch toast while the step runs |+| `run` | yes | The command |+| `skip_if` | no | Probe command; exit 0 skips the step (idempotent installs) |+| `await` | no | `url`: scrape the first URL the command prints, hold for on_ready |+| `url_pattern` | no | Scrape regex, default `https?://\S+` |+| `timeout_seconds` | no | Per-step budget; timing out fails the launch with this label |++**The `--print-url` convention** is the whole integration contract: a dock-ready+server verb prints its own reachable URL to stdout and keeps serving.++### The `ai` block (launch.ai)++Attach an AI thread to the launch — either as `mode: "ai_thread"` (the whole+launch) or via `on_ready: "hand_to_ai"` after container steps (the steps opened+the native app; the AI takes the placeholder webview over):++```json+"ai": {+  "prompt": "The user just launched Adom TSCi from the dock. Take over the webview tab named \"Adom TSCi\" and load the board they were last viewing.",+  "submit": "auto",+  "thread": "new",+  "typed": false+}+```++| Field | Required | Meaning |+|---|---|---|+| `prompt` | yes for ai launches | The pre-canned prompt. Omitted, Hydrogen uses a sensible default naming the app |+| `submit` | no (default `auto`) | `auto`: paste + submit instantly. `staged`: the prompt sits in the chat input, ready for the user to review and send |+| `thread` | no (default `new`) | `new`: open a fresh conversation. `existing`: foreground the Claude panel and inject into the conversation the user currently has open |+| `typed` | no (default `false`) | `true`: human-jitter typing animation instead of instant paste. Best-effort: staged and existing-thread paths always type |++`thread: "existing"` targets the conversation currently open in the panel —+true per-app conversation lookup (find *this app's* thread by title) needs+extension support and is a documented future addition, not part of 1.0.0.++## The loading screen is free++The instant a launch involves a webview, Hydrogen paints the panel with the workspace+theme background token and runs the Adom orbital loading animation (transparent,+so it reflects any theme) until the app URL has truly loaded, then fades it out.+Install steps stay behind the mark with their labels ticking in the launch toast+and the full command transcript streaming into the placeholder's console. Apps+bundle nothing and opt into nothing. Details and the embeddable single-file+animation: the adom/orbital-lab wiki page.++## Testing a draft (your own Hydrogen only)++Dock settings (gear) → **Manifest URL (draft)**: point the dock at any+`apps.json` URL — a draft branch blob of `adom/hydrogen-bootstrap`, or your own file —+and this Hydrogen renders that list instead of the production one. A **DRAFT** badge+stays on the rail while active. Clear the field to return to the production+list. This is how curators validate a change before it reaches the fleet, and+how publishers preview how their entry will look once listed.++Per-app, no draft needed: Dock settings → Add app → `owner/slug` reads your+page's live `dock.json` immediately.++## Publisher checklist (make your app dock-ready)++1. Public wiki page with a hero and a billboard-quality one-line brief; the+   dock shows exactly these.+2. Install step is `adom-wiki pkg install <owner>/<slug>` with a `skip_if`+   probe so relaunches skip it.+3. Server verb takes `--print-url`; that step gets `await: "url"`.+4. Sane `timeout_seconds` (180 install / 60 serve are good defaults).+5. Add `dock.json` to your page repo root and push the page.+6. Preview it in the workbench on the adom/hydrogen-desktop-dock wiki page —+   card, launch transcript, and AI-thread behavior, all simulated.+7. Test from your own dock: Dock settings, Add app, `owner/slug`, click it,+   watch the steps run behind the loading mark.++## Adding an app for a user (AI recipe)++When the user asks you to add a wiki app to their dock:++1. Confirm the page exists and is public (`adom-wiki discover search`).+2. Open Dock settings (gear at the top of the rail), Add app, enter+   `owner/slug`; or drive the equivalent Hydrogen API if you are automating.+3. If the page has no `dock.json`, it still loads as a plain webview of its+   wiki page; tell the user launch steps need the manifest and point the+   publisher at this skill.
skills/hydrogen-editor-state/SKILL.mdadded+52
@@ -0,0 +1,52 @@+---+name: hydrogen-editor-state+description: The Editor State Machine (ESM) — HOW to know the true state of the VS Code editor and the Claude Code conversations inside Hydrogen, and the HARD typing invariants. Use BEFORE typing into a Claude conversation, driving setup-step threads, diagnosing "state unknown", empty split panels, tab spam, prompts landing in the wrong conversation, or any Claude tab/webview confusion. Trigger words — editor state, /editor/state, claude tab state, conversation state, which tab is active, empty editor group, split panel, double-type, typed into two boxes, prompt in wrong conversation, census, safe to type, stray text, disk verified, msg_count wrong, workbench unknown.+---++# hydrogen-editor-state — the Editor State Machine++ONE authoritative read: `GET /editor/state` on Hydrogen's control API. Everything below is what it+returns and the rules for acting on it. Full design: docs/EDITOR-STATE-MACHINE.md (hd repo).++## The layered truth (never blend layers)++| Layer | Question | Source | Trust |+|---|---|---|---|+| L0 `lifecycle` | starting / editor-up / ready | port probes (7380, :8821, CDP) | absolute |+| L1 `layout`+`conversations` | groups, widths, empty panels, tab census, focused tab | workbench DOM + tab bar | high (VS Code chrome is stable) |+| L2 `extension` | what commands can I invoke | the extension's contributed-command registry + fingerprint | absolute (it IS the API) |+| L3 `conversations_disk` | turns, first prompt, generating, delivered | the jsonl transcripts | absolute — works for DISPOSED background tabs |+| L4 `census` | compose boxes + submit buttons visible NOW | DOM probes across ALL contexts | only under the invariants below |++## The HARD invariants (John 2026-07-17 — non-negotiable)++1. **Never type unless `census.safe_to_type == true`** (exactly 1 visible input across ALL+   webview contexts — not just the focused one).+2. **Freshness oracle is the TITLE**: `title == 'Untitled'` = never submitted. NEVER use+   `msg_count` for freshness — it counts welcome-screen DOM nodes (reads 5 on a pristine tab).+3. **After a submit, run the stray-text audit**: `POST /editor/audit-prompt {prefix}` must+   return `contexts_containing: 0`. 1+ = the prompt is ALSO sitting in another conversation's+   box (double-type) — clear it, fail loudly, never ignore.+4. **Delivery is proven on DISK, not in the DOM**: the prompt must be the first user turn of+   exactly ONE `~/.claude/projects/*/*.jsonl`. 0 = not delivered; 2+ = double-send.+5. **Background tabs are unreadable** (VS Code disposes their webviews): `loaded:false`,+   null ctx/compose. To read one, activate it first (`/claude/tabs/activate {index:<ordinal>}`+   — by ORDINAL, never by label: every Claude tab is labeled 'Claude Code').+6. **`normalize_needed: true` → `POST /editor/normalize`** before showing the user anything+   (an empty split group steals 50% of their editor; populated 2-group layouts are preserved).+7. **Prefer registered commands over DOM clicks**: `extension.commands` lists them+   (claude-vscode.newConversation, .focus, .editor.open, ...). Run via the in-distro+   `:8821/command` API. Commands survive extension updates; selectors don't.+8. **Extension drift**: `extension.fingerprint` changes when Claude Code updates — re-verify+   your assumptions (anchors, commands) before trusting flows after a fingerprint change.++## Quick recipes++- Am I safe to type? `GET /editor/state` → `census.safe_to_type`.+- Did my prompt double-type? `POST /editor/audit-prompt {"prefix":"<first words>"}` → want 0 after submit.+- Which conversation is generating? `conversations_disk[*].generating` (disk truth, any tab).+- Fix a half-width Claude tab / dead empty panel? `POST /editor/normalize`.+- Bars: `layout.bars` reports activity / primary sidebar / secondary (auxiliary) bar / bottom+  panel visibility. A Claude conversation living in the SECONDARY bar is a second compose box+  (census=2). Close bars deterministically: `POST /editor/bars {"primary":"close","secondary":"close"}`+  (read -> toggle-on-mismatch -> verify; never a blind toggle).
skills/hydrogen-embedded-ad/SKILL.mdadded+141
@@ -0,0 +1,141 @@+---+name: hydrogen-embedded-ad+description: How Hydrogen (ah) and Adom Bridge (ab) interact via "embedded mode". Use whenever the user asks about the system tray icon, why ab isn't visible, why two desktop apps are installed, the "Embedded · Hydrogen" footer pill, or how to make ab show up standalone. Trigger words — embedded mode, system tray, ab tray, why is adom desktop hidden, where's my tray icon, Hydrogen vs ab, both apps installed, why two installs, ad missing from tray, switch ab to standalone, ab won't show, run ab without Hydrogen, decouple ab from Hydrogen.+---++# Hydrogen ↔ ab Embedded Mode++There are two desktop apps in the Adom ecosystem: **Hydrogen (ah)** — the user-facing workspace app — and **Adom Bridge (ab)** — the bridge that talks to local apps (KiCad / Fusion 360 / Chrome / files). Hydrogen's installer silently installs ab (it fetches the latest ab from wiki.adom.inc at install time rather than carrying a copy) and runs it in **embedded mode**, where ab is invisible and Hydrogen's tray menu mirrors ab's controls.++This skill explains how the two interact so you can answer "why isn't ab in my tray?", "should I have two installs?", "why did the tray icon disappear?" and similar.++## Two modes++### Standalone ab (no Hydrogen anywhere)++How ab 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.adom.inc/api/v1/pages/adom-bridge-cli/files/version.json` every 4h+- Footer shows the user's email + a "Sign in" / "Sign out" link++This is what ab looked like before Hydrogen existed and what it reverts to when Hydrogen vanishes.++### Embedded ab (Hydrogen owns it)++Hydrogen's installer puts ab into embedded mode by:+1. Writing `%LOCALAPPDATA%\Adom Bridge\embedded.json` marker (`{"owner":"hydrogen-desktop", ...}`)+2. Deleting ab's Desktop / Start Menu / Startup `.lnk` shortcuts so the user can't double-launch standalone ab+3. Configuring Hydrogen's own systray to mirror ab's menu (Open ab, Connect All, Disconnect All, Settings, etc.)++When Hydrogen spawns ab with `--embedded --start-hidden --relay-url ws://127.0.0.1:8765 --relay-name hydrogen-desktop`, ab detects embedded mode and:+- **Hides** its system tray (Hydrogen owns the one tray)+- **Suppresses** its autostart shortcut creation+- **Disables** its auto-updater (Hydrogen owns ab's update channel — Hydrogen installs the latest published ab at install time)+- **Hides** its main window on boot (Hydrogen's tray "Open Adom Bridge" menu item opens it on demand)+- **Skips** HKLM Uninstall registration (Hydrogen's installer offers cascade-uninstall of ab)+- Footer shows a read-only email + an "Embedded · Hydrogen" pill++Result: the user only ever sees one app (ah) in the tray + Start Menu, even though two processes (Hydrogen + ab) are running.++## How ab detects "am I embedded?" — 3-signal cascade++ab 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 Hydrogen spawns ab with.**+2. **Env var**: `ADOM_EMBEDDED=1` (or `true`). Same behavior as the flag.+3. **Marker file**: `%LOCALAPPDATA%\Adom Bridge\embedded.json` exists. ab then probes Hydrogen's **liveness beacon** on `127.0.0.1:9001` (a real port ab listens for — NOT Hydrogen's control API; Hydrogen's actual control API is **47084**):+   - If Hydrogen answers the beacon → embedded mode+   - If Hydrogen doesn't answer → ab treats the marker as stale, deletes it, falls back to standalone++If none of the three signals fire → standalone.++## Lifecycle ownership in embedded mode++Hydrogen's runtime supervisor:+- Reads Hydrogen's session token from `%APPDATA%\hydrogen-desktop\hydrogen-session.txt`+- Spawns ab detached with the embedded flags above+- Polls `http://127.0.0.1:47200/health` every 5s+- If ab's health probe fails → Hydrogen respawns ab with the same args++Hydrogen's tray menu (mirrors ab's controls so the user gets all the ab verbs without seeing ab's tray):+```+Open Hydrogen+Reload Workspace+── Adom Bridge ──+Open Adom Bridge      → POST 127.0.0.1:47200 desktop/window_show+Connect All            → POST desktop/connect_all+Disconnect All         → POST desktop/disconnect_all+Settings (Adom Bridge)→ POST desktop/window_show+─────+Quit                   → POST desktop/shutdown, 200ms grace, then app.exit(0)+```++Hydrogen's uninstaller PREUNINSTALL hook prompts "Also remove Adom Bridge?" → cascade-uninstalls ab on Yes.++## Runtime transitions++**ab ≥ v1.8.47** re-checks Hydrogen'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 Hydrogen → Hydrogen spawns ab with `--embedded` → ab's tray hides, pill appears+- Evening: user quits Hydrogen → ab's next poll fails the Hydrogen probe → ab reverts to standalone → tray reappears+- Next morning: user launches Hydrogen again → ab's next poll succeeds → ab re-embeds → tray hides++The same ab process can flip mode back and forth indefinitely across an Hydrogen-quit / Hydrogen-relaunch cycle. Transitions:++| Current mode | Hydrogen probe says | What ab does |+|---|---|---|+| Standalone | dead | stay standalone (no-op) |+| Standalone | alive | → **embed**: hide tray, write marker, hide main window, show "Embedded · Hydrogen" 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.++**ab ≤ v1.8.46** only checks at boot. If Hydrogen dies while ab is running, ab stays invisible in embedded mode until restart.++## Common questions++### "I don't see ab in my system tray on a Windows box with Hydrogen installed"++Expected. Hydrogen's installer deleted ab's autostart shortcut, so ab doesn't launch on boot. ab only runs when Hydrogen spawns it. Hydrogen's tray includes ab's menu items.++To verify: `tasklist | findstr adom-bridge-cli`. If ab's running, it's just hidden (per embedded mode). If not running, Hydrogen isn't either.++### "I want ab standalone — how do I unembed?"++- Quit Hydrogen+- Delete `%LOCALAPPDATA%\Adom Bridge\embedded.json`+- Launch ab via `%LOCALAPPDATA%\Adom Bridge\adom-bridge.exe`+- ab will detect no marker + no CLI flag → standalone mode → tray reappears++Or uninstall Hydrogen with "Yes, also remove Adom Bridge" → No → Hydrogen is gone, ab's marker is cleared, ab reverts to standalone on next launch. (Note: ab's autostart shortcut was deleted by Hydrogen's installer, so ab still won't auto-launch on boot until you re-run ab's installer to recreate it.)++### "Both Hydrogen and ab are installed — is that normal?"++Yes. Hydrogen's NSIS POSTINSTALL hook resolves the latest ab from wiki.adom.inc, verifies its published sha256, and silently runs that installer with `/S /NOLAUNCH`. If that can't happen (offline at install time), Hydrogen retries it on first launch. ab lives at `%LOCALAPPDATA%\Adom Bridge\`, Hydrogen at `%LOCALAPPDATA%\Hydrogen\`. Two separate exes, two separate Add/Remove Programs entries (well, only Hydrogen's is in Add/Remove if Hydrogen's installer ran the embedded-mode ab which skips HKLM register — ab ≥ v1.8.46).++### "ab's tray suddenly appeared even though Hydrogen is installed"++Hydrogen probably crashed or was closed. ab ≥ v1.8.47's runtime poll noticed Hydrogen is dead and reverted to standalone tray. The fix: relaunch Hydrogen from the Start Menu → ab's next poll detects Hydrogen → tray hides again.++### "Direct API for ab verbs"++ab listens on `127.0.0.1:47200` for `POST /command` with `{"app":"desktop","command":"<verb>","args":{...}}`. The 7 user-visible verbs Hydrogen'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++- ab's mode detection: `adom-bridge-cli/src-tauri/src/embedded.rs:detect_embedded_mode()`+- Hydrogen's ab spawn: `hydrogen-desktop/src-tauri/crates/hydrogen-app/src/ad_supervisor.rs:spawn_ad_embedded()`+- Hydrogen's tray menu: `hydrogen-desktop/src-tauri/crates/hydrogen-app/src/tray.rs:install_tray()`+- Hydrogen's NSIS hook (embed-ab on install / offer-uninstall): `hydrogen-desktop/src-tauri/crates/hydrogen-app/installer/hydrogen-install.nsh.template`+- ab's release manifest (what Hydrogen reads at install time to find the latest ab): `wiki.adom.inc/api/v1/pages/adom-bridge-cli/files/version.json`
skills/hydrogen-file-transfer/SKILL.mdadded+92
@@ -0,0 +1,92 @@+---+name: hydrogen-file-transfer+description: >+  Move files BOTH WAYS between the Hydrogen WSL2 Ubuntu workspace (the Adom-Workspace+  distro, where your code lives at /home/adom/project) and the user's Windows PC+  — including their Desktop, Downloads, and any folder. Three methods: (1) the+  auto-mounted C: drive at /mnt/c inside the distro, (2) the \\wsl$ network path+  from Windows Explorer, and (3) the adom-bridge-cli relay (send_files / pull_file)+  relay. Use when the user asks "how do I+  get this file onto my desktop", "copy this out of the workspace", "get a file+  from my PC into the workspace", "move a file in/out", "where do my downloads+  go", or "drag a file into the container". Trigger words: copy file out, copy+  file in, move file to desktop, get file from desktop, file transfer, /mnt/c,+  wsl$, \\wsl.localhost, send_files, pull_file, export file, import file, drag+  into workspace, file to my pc, download to desktop, upload to workspace.+---++# Move files between the workspace and the Windows PC++Hydrogen's default workspace is the **WSL2 `Adom-Workspace` distro**. Your work lives+at `/home/adom/project` inside it. There are three ways to move files to/from the+user's real Windows machine. Methods 1–2 are WSL2-native (fast, no relay);+method 3 goes through the Adom Bridge relay.++## Method 1 — `/mnt/c` (from INSIDE the distro) — simplest++Windows drives are auto-mounted in the distro: `C:\` is `/mnt/c`. So you can `cp`+straight to/from the user's Desktop or Downloads. (Find the Windows username from+`/mnt/c/Users/` if you don't know it.)++```bash+WINUSER=$(ls /mnt/c/Users | grep -viE 'public|default|all users' | head -1)++# Workspace → Desktop+cp /home/adom/project/report.pdf "/mnt/c/Users/$WINUSER/Desktop/"++# Desktop → workspace+cp "/mnt/c/Users/$WINUSER/Desktop/data.csv" /home/adom/project/++# Downloads folder works the same: /mnt/c/Users/$WINUSER/Downloads/+```++## Method 2 — `\\wsl$` (from Windows Explorer) — drag & drop++From Windows, the distro is a network share. In Explorer's address bar:++```+\\wsl$\Adom-Workspace\home\adom\project+   (newer Windows also: \\wsl.localhost\Adom-Workspace\home\adom\project)+```++The user can **drag files in/out** of that folder to/from their Desktop. Tell+them this path when they want to grab their code or drop a file in by hand.++## Method 3 — the adom-bridge-cli relay (works on ANY runtime)++Hydrogen embeds Adom Bridge, which bridges the workspace to the real OS. Use this when+you want the AI to push/pull files programmatically, or when you need a specific+Save-As target directory on the PC. See **hydrogen-adom-bridge-cli**.++```bash+# Workspace → PC (lands in the user's Downloads by default; large files OK)+adom-bridge-cli send_files '{"filePaths":["/home/adom/project/out.zip"],"saveTo":"C:\\Users\\john\\Desktop"}'++# PC → workspace+adom-bridge-cli pull_file '{"filePaths":["C:\\Users\\john\\Desktop\\photo.png"],"saveTo":"/home/adom/project"}'+```++Notes:+- Confirm the bridge first: `adom-bridge-cli ping` → `pong`. If not connected, the+  user must have Hydrogen running/foregrounded.+- Files whose basename starts with `_` are silently dropped by the relay — use+  plain names.++## Which method to use++| Situation | Use |+|---|---|+| You're scripting inside the workspace (WSL2) | **Method 1** (`/mnt/c`) — fastest |+| User wants to drag files by hand | **Method 2** (`\\wsl$` path) |+| Programmatic, or you need a Save-As target dir | **Method 3** (relay) |++## Where things persist+`/home/adom/project` survives a workspace restart; it is wiped by a **virgin+reset** (`wsl --unregister Adom-Workspace`). Copy anything important to the PC+(any method above) before a virgin reset. See **hydrogen-volume** for the full+persistent-vs-ephemeral map.++## Related+- **hydrogen-volume** — what persists, where files live, backing up before virgin reset+- **hydrogen-adom-bridge-cli** — the relay (send_files / pull_file) and OS reach+- **hydrogen-workspace-lifecycle** — distro import/export/unregister
skills/hydrogen-golden-image/SKILL.mdadded+138
@@ -0,0 +1,138 @@+---+name: hydrogen-golden-image+description: >+  Your Hydrogen workspace is a PRE-BAKED golden WSL2 image, not a+  machine that installed itself step-by-step. The adom/core wiki package+  (gallia's replacement; gallia is deprecated), the 8 Adom CLIs, the+  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-hydrogen-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, hydrogen-wsl2-+  image, wsl --import, tarball version, adom-tarball-version, migrate tarball,+  update gallia, reinstall gallia, install-hydrogen-skills, install-gallia, install-+  claude-cli, install-claude-ext, verify-adom-bridge-cli, 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.+---++> NOTE (2026-07-21): gallia is deprecated. The base workspace install is now the+> adom/core wiki package (`adom-wiki pkg install adom/core`); the workspace+> self-updates via `adom-workspace-updater` (which re-pulls newer skills/CLIs+> periodically). References to gallia below are historical.++# Hydrogen 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:++- **the adom/core wiki package** (gallia's replacement; skills/CLIs come from+  signed adom-wiki packages, kept current by `adom-workspace-updater`)+- the **8 Adom CLIs** (`adom-cli`, `adom-bridge-cli`, etc. — at `/usr/local/bin`)+- the **Claude Code VS Code extension** (baked; auto-updates via VS Code's+  `extensions.autoUpdate`). The **`claude` CLI** is NOT baked — it's installed+  at runtime by the `install-claude-cli` setup step+- **code-server** + node, plus a baked **systemd** unit that owns code-server+  across distro boots and Hydrogen 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-ext`, and `verify-adom-bridge-cli` are gone. (`install-claude-cli`+is NOT in this list — it's a LIVE step that installs the `claude` CLI at runtime.)+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-hydrogen-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 `adom-wiki pkg` 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. Hydrogen downloads the release asset `adom-golden.tar.gz` **anonymously** (Hydrogen+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. Hydrogen's expected version is the+`TARBALL_VERSION` const in `runtime/wsl.rs` (~line 152). On each launch Hydrogen+compares them:++- `read_installed_version()` reads `/etc/adom-tarball-version` from inside the+  distro+- if it differs from `TARBALL_VERSION`, Hydrogen 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, Hydrogen's `TARBALL_VERSION` is bumped, and on next launch Hydrogen 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 **extension** is baked; the `claude` **CLI** is installed at runtime by `install-claude-cli`. Beyond that, 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 Hydrogen'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 is now **26+steps** that mostly *configure* the already-complete workspace: ~14 config/auth+steps (ensure/start code-server, activate the baked extension, set env vars,+inject the API key, configure VS Code, the relay/connectivity gates, install the+`claude` CLI, Claude sign-in), then Welcome, then 9 AI-thread bootstrap steps and+a final verify + open-welcome. See+[hydrogen-setup-steps](../../wsl2/hydrogen-setup-steps/SKILL.md) for the per-step detail.++## Related skills+- [hydrogen-workspace-lifecycle](../../wsl2/hydrogen-workspace-lifecycle/SKILL.md) — start/stop/reset/migrate the imported distro+- [hydrogen-setup-steps](../../wsl2/hydrogen-setup-steps/SKILL.md) — the 27 cascade steps (config/auth + AI-thread bootstrap) that run on top of the baked image
skills/hydrogen-lightbulb/SKILL.mdadded+68
@@ -0,0 +1,68 @@+---+name: hydrogen-lightbulb+description: >+  The LIGHTBULB menu (sample prompts) in Hydrogen's title bar — the 💡+  button just right of the Adom logo (top-left, 0.1.265+; it lived by the CPU/RAM+  badges before). Each item opens a FRESH Claude AI thread and+  types a curated demo prompt for the user (parts search, KiCad/Fusion, pup,+  native browser, adom-tsci, shotlog, webviews, hands-free, "Speak Adom"+  shorthand training). Covers: how items run, the samples-menu.open/close AI+  commands, GET /claude/sample-prompts, and sample-prompts.json — the overlay+  file that adds or rewords prompts WITHOUT a new Hydrogen release. Trigger words:+  lightbulb, light bulb menu, sample prompts, samples menu, demo prompts, sample+  threads, speak adom, add a sample prompt, sample-prompts.json.+---++# hydrogen-lightbulb — the sample-prompts (💡) menu++The **lightbulb button** just right of the Adom logo in Hydrogen's title bar (moved there+0.1.265 — the AI work happens on the left where VS Code lives) opens a menu of curated sample+prompts. Picking one opens a **new Claude conversation** in the editor and+human-types the prompt (open → ready → type → verify → submit, all verified) —+the user just watches it run. It exists to teach by DOING: each prompt+demonstrates a capability end to end.++## Current lineup (0.1.260+)++Full tour (everything at once) · Adom parts search · KiCad · Fusion 360 ·+Altium · pup (AI-driven browser) · Native Chrome/Edge · Google Workspace ·+chip-fetcher · adom-tsci (build a board) · shotlog · Webview panels ·+Hands-free (TTS) · **Speak Adom (wv, pup, nb...)** — teaches the popular Adom+shorthand with a cheat sheet in a wv and a quick quiz.++## Drive it as an AI++```bash+# open/close the menu (registered UI actions):+POST /ui/invoke {"id": "samples-menu.open"}     # and "samples-menu.close"++# list what the menu shows (id, label, blurb):+GET /claude/sample-prompts+```++## Where the list comes from (three layers, later wins)++1. **Compiled defaults** in the Hydrogen build (offline fallback).+2. **The wiki list** (0.1.263+): `lightbulb-prompts/prompts.json` on the Hydrogen wiki+   page — the fleet-wide lineup, updated INDEPENDENTLY of Hydrogen releases. Hydrogen checks+   it on every menu open (debounced 15 min) plus every 4 hours during business+   hours (Mon-Fri 7am-7pm local); a changed list pops into an open menu live.+   Cache: `%APPDATA%\hydrogen-desktop\sample-prompts-remote.json`. Docs + PR+   guide: the [Lightbulb Prompts sub-readme](https://wiki.adom.inc/adom/hydrogen-desktop/files/lightbulb-prompts/README.md).+3. **The user's local file** `%APPDATA%\hydrogen-desktop\sample-prompts.json` —+   personal overrides, always wins:++```json+[{"id":"thread-my-demo","label":"My demo","blurb":"one line","prompt":"the full prompt text"}]+```++- A **new id** appends a menu item; a **matching id** overrides a lower layer+  (label/blurb, and the prompt body if non-empty).+- Any `thread-*` id routes to the runner automatically — no code change needed.+- Bad JSON (local or wiki) is logged and ignored — the menu never breaks.+- To contribute a prompt for EVERYONE: PR `lightbulb-prompts/prompts.json` in the+  hydrogen-desktop repo (see the sub-readme above for the rules).++Prompts are written in the user's casual first-person voice, as ONE flat+paragraph (a typed newline would submit early). Never edit John's compiled+prompt bodies without permission; add new ids instead.
skills/hydrogen-pup/SKILL.mdadded+57
@@ -0,0 +1,57 @@+---+name: hydrogen-pup+description: >+  PUP — what "pup" means in Hydrogen and exactly how to open one. When+  the user says "show me in pup", "open it in pup", "pup window", "in pup", or+  asks to demo/drive ANY page "in pup", it means a real, AI-drivable Chrome/Edge+  window opened by Adom Bridge's Puppeteer bridge (auto-installed with Hydrogen) on+  THIS machine — never a webview (see hydrogen-webview), never the user's personal+  signed-in browser (that's nbrowser). CRITICAL Hydrogen DIFFERENCE: in Hydrogen, localhost+  URLs are the NORM for pup (everything binds localhost on the same machine) —+  the cloud-container rule "pup can't see localhost" is INVERTED here. Trigger+  words — pup, open in pup, show me in pup, in pup, pup window, puppeteer,+  pup browser, drive a browser, show the page in pup.+---++# hydrogen-pup — "pup" is a real drivable browser window on this machine++**pup** = Adom Bridge's Puppeteer bridge driving a fresh, isolated Chrome (or+Edge) process. Hydrogen embeds Adom Bridge and auto-installs the bridge, so pup is+always available — no setup questions, just check readiness and go.++## ⚠️ THE LOCALHOST RULE IS INVERTED IN Hydrogen (do not import cloud instincts)++- **Web Hydrogen (cloud container):** pup runs on the *user's desktop*, so the+  container's `localhost` is unreachable — the bridge sanity-checks localhost+  URLs because they're almost always a mistake, and pages get slug/proxy URLs+  (`vscode_proxy_uri` style) instead.+- **Hydrogen (here):** the workspace, Hydrogen, and pup all live on the SAME+  machine. **~95% of pup URLs are `http://localhost:<port>` and that is+  CORRECT.** Never rewrite a localhost URL to a slug/proxy URL, and never+  refuse it — Hydrogen binds its web servers to localhost by design.++## ✅ How to open a page in pup (from the workspace)++```bash+adom-bridge-cli browser_readiness '{}'          # is the bridge ready? which browser?+adom-bridge-cli browser_open_window '{"url":"http://localhost:<port>/...","sessionId":"<task-name>"}'+adom-bridge-cli browser_screenshot '{"sessionId":"<task-name>"}'   # verify what's on screen+```++- Reuse ONE window per task via a stable `sessionId` — open tabs in it rather+  than spawning window after window.+- pup opens in the background by default; pass `foreground:true` only when the+  user should see it appear right now.+- Verify by screenshot, not by assumption: if `browser_screenshot` doesn't show+  your page, it isn't open.++## ⛔ Never confuse the three browser surfaces++| Ask | Surface | Skill |+|---|---|---|+| "show me in **webview**" | native Hydrogen webview tab | `hydrogen-webview` |+| "show me in **pup**" / drive it | isolated Puppeteer Chrome (this skill) | `pup` for deep driving |+| use **my** logins / my real browser | user's signed-in Chrome/Edge | nbrowser / adom-browser-extension |++For deep pup driving (tabs, sessions, recording, eval), the full `pup` skill+applies — this skill exists so the word "pup" always routes correctly in Hydrogen.
skills/hydrogen-recording/SKILL.mdadded+408
@@ -0,0 +1,408 @@+---+name: hydrogen-recording+description: >+  How to record screen + window video from Hydrogen. In Hydrogen the recorder is+  NATIVE: Windows.Graphics.Capture (per-window, "record kicad") + DXGI full-screen,+  hardware H.264 (default, universally playable) or H.265 (smaller, not web-playable)+  → mp4 in ~/project/recordings/, with NO picker, NO "you're sharing" banner, real+  30-60fps, and NO display wake-lock. Driven by `adom-cli hydrogen recording+  start/stop` (which routes to the native control API) or the control endpoints+  directly (POST /recording/native/start|stop, GET /recording/native/status|sources).+  A server-side max-duration cap (default 600s) auto-stops + toasts. Read for codec+  choice, source selection, the cap+reason, and the "● Recording" indicator. Also+  covers AUDIO-ONLY / narration capture (`adom-cli hydrogen audio` → WebM/Opus in+  ~/project/audio/), ab host desktop recording (desktop_record_start, Windows-only),+  the tab-vs-desktop footgun, and what `--mic` does. TWO ENGINES: wgc (GPU, real+  displays) and gdigrab (ffmpeg GDI — the ONLY engine that records on VMs / virtual+  / indirect displays / VNC); engine:"auto" probes and falls forward; GET+  /recording/native/capabilities reports what THIS box supports. Multi-monitor:+  screen:1..N or screen:all. Trigger words — record,+  recording, screen recording, record my screen, record kicad, record a window,+  record whole screen, record the workspace, record a demo, record with mic, record+  no audio, mic on, mic off, voiceover, record audio, audio-only, record a narration,+  record my voice, narrate, narration track, record a voice track, h264, h265, hevc,+  mp4, webm, opus, codec, recordings folder, audio folder, ffmpeg, stitch clips,+  max duration, recording indicator, desktop_record_start, browser_record_start,+  start recording, stop and save, record on a vm, virtual display, indirect display,+  headless recording, vnc recording, 0 frames, zero frames, gdigrab, gdi capture,+  recording engine, capture engine, record all monitors, record second monitor,+  multi-monitor recording, recording capabilities, can this machine record.+---++# Hydrogen — Screen & Window Recording++In Hydrogen the primary recorder is **native** (Windows.Graphics.Capture + DXGI) — not+`getDisplayMedia`. It produces a real high-fps mp4, asks for no permission, shows no+"you're sharing" banner, and holds **no display wake-lock** (see+[hydrogen-screen-lock](../hydrogen-screen-lock/SKILL.md)). Pick the mechanism by *what* you're+recording — this is the single most important decision and a known footgun.++**DEFAULT — drive it with `adom-cli hydrogen recording start` / `stop`.** That IS the+Hydrogen native recorder (h264 mp4 → `~/project/recordings/`). Do **NOT** reach for+`adom-bridge-cli desktop_record_start` for an ordinary "record my screen / window / the+workspace" — that's the ab **host** fallback: it writes a `.webm` into ab's+bridges-cache (`…/Adom Bridge/bridges-cache/…`), **not** your `~/project/recordings/`,+and is only for when Hydrogen's native recorder is genuinely busy. "record my screen",+"record the workspace", "record a demo", "record kicad" all → `adom-cli hydrogen+recording`. (Same trap as screenshots: don't fall through to `adom-bridge-cli` for+anything inside Hydrogen.)++## Which recorder? — the priority ladder (read first)++Same shape as [hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md): **lead with `adom-cli`+(consistent with web Hydrogen), drop to the richer Hydrogen/ab surfaces only for what it can't+do.** Default to Hydrogen-native.++1. **Hydrogen content OR the whole screen (the 99% case): `adom-cli hydrogen recording start+   --share tab|screen` — THE DEFAULT.** The *same* command on web-Hydrogen and Hydrogen (web-H+   records via `getDisplayMedia`; Hydrogen records **natively** via WGC/DXGI — no picker, no "you're+   sharing" banner, no display wake-lock). `--share tab` = the Hydrogen workspace; `--share screen`+   = the entire display (KiCad/Fusion/desktop apps). Saves **h264 mp4 → `~/project/recordings/`**.+   Verified live 2026-06-14.+2. **A SPECIFIC WINDOW** (record just KiCad / Fusion, not the whole screen): **Hydrogen native+   control API `POST /recording/native/start {source:"window:<title-substring>"}`.** adom-cli's+   `--share` is **tab|screen only — it has no per-window flag**, so per-window recording is an+   **Hydrogen-native exclusive** (neither adom-cli nor ab can scope a recording to one window).+   Resolve the title via `GET /recording/native/sources`. Same endpoint does `{source:"screen"}`,+   `{source:"screen:2"}`, codec, custom cap.+3. **The whole DESKTOP when Hydrogen's native recorder is already busy:** `adom-bridge-cli+   desktop_record_start {confirmDesktopNotTabRecording:true}`. **Whole-desktop ONLY (no+   per-window), via Chrome `getDisplayMedia` → it DOES hold the display wake-lock** (unlike Hydrogen+   native) and writes a **`.webm` into ab's cache** (not `~/project/recordings/`). ab's own help+   says "prefer Hydrogen's `recording start --share screen`" — this is for the+   parallel-with-an-Hydrogen-capture case, not the default.+4. **A single Chrome tab** (AI-driven Pup browser — a clean per-tab clip): `adom-bridge-cli+   browser_record_start {sessionId}` / `browser_record_stop`. CDP screencast, ~50fps, no HUD,+   no wake-lock, coexists with everything.+5. **Just audio — a voice / narration track, no video** ("record a narration", "record my+   voice"): **`adom-cli hydrogen audio start/stop`** → `~/project/audio/<name>.webm` (Opus).+   See "Audio-only" below.++### What each recorder can actually scope (don't assume symmetry with screenshots)+Unlike screenshots — where ab grabs a window *or* the screen — **recording is NOT uniform**:++| Recorder | Window | Full desktop | Browser tab | Backend | Wake-lock | Output |+|---|---|---|---|---|---|---|+| `adom-cli hydrogen recording` (ah) | — (use control API) | ✅ `--share screen` | ✅ `--share tab` | native WGC/DXGI | **NO** | mp4 → `~/project/recordings/` |+| Hydrogen native control API | ✅ `source:"window:X"` | ✅ `source:"screen"` | — | native WGC/DXGI | **NO** | mp4 → `~/project/recordings/` |+| ab `desktop_record_start` | ❌ **none** | ✅ whole desktop | ❌ | Chrome `getDisplayMedia` | **YES** | `.webm` → ab cache |+| pup `browser_record_start` | — | ❌ | ✅ one tab | CDP screencast | **NO** | tab clip |++So: **per-window recording = Hydrogen-native only; whole-desktop = Hydrogen-native (no wake-lock) preferred+over ab (wake-lock); single tab = pup.** When someone says "record a window," that's #2 (Hydrogen+control API) — ab cannot do it.++---++## ⭐ Engines: wgc vs gdigrab — and why VMs are different++Hydrogen's native recorder has **two engines**, because the machine you're on decides what can+physically capture frames:++| | `wgc` (default on real displays) | `gdigrab` |+|---|---|---|+| Backend | Windows.Graphics.Capture + DXGI, Media Foundation MFT encoder | ffmpeg GDI capture (legacy, compositor-independent) |+| GPU accelerated | yes (~60fps) | no (~15-30fps, CPU) |+| Real display (laptop/desktop) | ✅ best choice | works, lower fps |+| **VM / virtual / indirect display / VNC / headless** | ❌ **captures 0 FRAMES, silently** | ✅ **the only engine that works** |+| Sources | screen, screen:N, window:X, self | screen, screen:N, **screen:all**, window:X |+| Containers | mp4 | mp4, mkv |+| Needs | nothing | ffmpeg (auto-fetched ~40MB on first use, cached) |++**The trap this exists for (ADOMGPU, 2026-07-19):** on a box whose desktop sits on an+indirect display driver (IddCx "Virtual Display Driver", Hyper-V Video, Parsec/Sunshine,+VNC-only), WGC *and* DXGI Desktop Duplication return success but deliver **zero frames**+— and `GraphicsCaptureSession.IsSupported()` still says True, so it can't warn you.+Legacy GDI keeps working there. Hydrogen now handles this for you:++- **`engine:"auto"` (the default)** probes the display adapters; on a virtual-display box+  it goes straight to gdigrab, otherwise wgc — and if wgc still yields 0 frames in ~2s it+  **falls forward to gdigrab automatically** (`fellForward:true` in the response).+- **Fail-fast:** `/start` verifies frames are actually flowing before returning success.+  A 0-frame take errors AT START with the fix in the message — never again a 90s take+  that turns out empty at stop.+- **`GET /recording/native/capabilities`** = the ground truth for THIS box: engines ++  ffmpeg readiness, every adapter (with `virtual:true` flags), every monitor, codecs,+  containers, and `recommended.engine` with the reason. **On any unfamiliar machine (a+  VM, a cloud box, anything over VNC), read this before recording.**++### Multi-monitor++`GET /recording/native/sources` (and `/capabilities`) list every monitor with its index,+geometry and `primary` flag. You can:+- record **one** monitor: `source:"screen:1"`, `"screen:2"`, … (each row's `select` value),+- record **all** monitors at once as a single spanning video: `source:"screen:all"`+  (gdigrab engine — WGC cannot span monitors),+- or just `source:"screen"` for the primary.++```bash+BASE="$(cat ~/.adom/hydrogen-control-url)"+# What can this box record? (do this first on a VM)+curl -s "$BASE/recording/native/capabilities"+# Record monitor 2 explicitly via gdigrab (e.g. on a VM):+curl -s -X POST -H 'Content-Type: application/json' \+  -d '{"source":"screen:2","engine":"gdigrab"}' "$BASE/recording/native/start"+# Record ALL monitors as one spanning video:+curl -s -X POST -H 'Content-Type: application/json' \+  -d '{"source":"screen:all"}' "$BASE/recording/native/start"+```++`status` and `stop` now report `engine`, `frames`, and **measured `fps`** — check them:+frames climbing + fps > 0 is a live take; the start already fail-fasts a dead one.+Note: `mic` audio is wgc-only (gdigrab records video-only for now).++---++## (a) Hydrogen native recording (WGC + DXGI) — the default++**Mechanism:** Rust `crates/hydrogen-control/src/recorder.rs` — Windows.Graphics.Capture+per-window + DXGI Desktop Duplication full-screen → Media Foundation hardware+encoder → mp4 in `~/project/recordings/` (born `adom`-owned). Real 30–60fps at full+source resolution. **No picker, no banner, no display wake-lock.**++### Drive it via adom-cli (the usual way)++```bash+adom-cli hydrogen recording start --share screen --mic false --countdown 0 --reason "demo"+# ... do the thing (wait / drive the app you're recording) ...+adom-cli hydrogen recording stop      # → filePath: recordings/recording-<ts>.mp4 (relative to ~/project)+```++**All four `start` flags are REQUIRED** (`--share`, `--mic`, `--countdown`, `--reason`)+— the CLI rejects the call otherwise (it forces you to think through scope, audio,+pacing, intent). `--share screen` records the whole (primary) screen with no approval+dialog (native capture); re-running `start` with the same `--share`/`--mic` skips+straight to countdown. In Hydrogen the result JSON carries fields web-Hydrogen doesn't:+`native:true`, `codec` (h264), `maxSeconds`, `reason`, `source` (e.g. `monitor:primary`),+and `pendingFile` / `filePath` (relative `recordings/<name>.mp4`).++⚠️ **The `stop` `_hint` text currently LIES about the format** — it says *"WebM format+(VP9 + Opus)"*, but Hydrogen native recordings are **h264 mp4** (the file is `.mp4`, and the+`codec` field says `h264`). Trust the `.mp4` `filePath` + `codec` field, not the hint.+(Known adom-cli bug, on Colby's list.)++### Drive it via the control API (full control — window by name, codec, cap)++The AI can call the control endpoints directly for capabilities adom-cli flags don't+yet expose (a specific window, codec, a custom cap):++```bash+# Discover sources first (resolve a fuzzy name like "kicad")+GET  /recording/native/sources         # → { windows:[...titles...], monitors:[{index,width,height}] }++# Start — source is the key arg:+POST /recording/native/start  { "source": "window:KiCad" }      # a window by title substring+POST /recording/native/start  { "source": "screen" }           # primary monitor ("record whole screen")+POST /recording/native/start  { "source": "screen:2" }         # monitor by 1-based index++POST /recording/native/stop            # → { file: "recordings/<name>.mp4", fileAbsolute, seconds, source }+GET  /recording/native/status          # → { recording, source, seconds, maxSeconds, reason, remainingSeconds }+```++So **"record kicad"** = `POST /recording/native/start {source:"window:kicad"}` (after+confirming the window title via `/sources`); **"record whole screen"** =+`{source:"screen"}`. No picker, no asking the user which window.++### Codec — H.264 (default) vs H.265++`POST /recording/native/start {"codec": "h264" | "h265"}` (default **h264**):++- **h264** — universally playable: browsers (Chrome/Edge `<video>`), the wiki, every+  player. **Use this for anything shared, embedded, or streamed.** (Default.)+- **h265 / hevc** — ~40% smaller at equal quality, BUT not playable in Chrome/Edge+  `<video>` (needs hardware/codec support). **Only for archival / local use** where+  you control the player.++When unsure, keep h264. The start response repeats this tradeoff in `_hints.codec`.+(adom-cli's `recording start` has no `--codec` flag yet — it always sends h264; use+the control API to choose h265. A `--codec` flag is on the adom-cli wishlist.)++### Audio (microphone)++`--mic true` (adom-cli) / `{"mic": true}` (control API) mixes the **default input+device** (the user's mic, via cpal/WASAPI) into the mp4's audio track (48 kHz AAC) —+for narration/voiceover. WGC is video-only, so Hydrogen opens the mic itself and feeds PCM+to the encoder. If no mic is available the recording proceeds **video-only** (graceful).+`--mic false` omits the voice. Notes:+- **`--mic false` is NOT "no audio track."** Either way the native mp4 carries an aac+  stream; with mic off it's a harmless EMPTY stream (0 frames, no audio, plays fine) —+  a windows-capture limitation, intentionally not stripped to avoid an ffmpeg+  dependency on clean VMs. There is **no recorder flag for a fully audio-free file**; if+  a user truly needs zero audio, strip it post-hoc (`ffmpeg -i in.mp4 -c copy -an out.mp4`).+- **~1s startup gap:** the audio stream begins once the encoder is created on the first+  video frame, so the mic track can be ~1s shorter than the video and slightly lead.+  Fine for casual narration; flag if you need frame-accurate sync.+- **adom-cli `_hint` bugs (Colby) — don't be misled:** the `start` hint always says+  *"No mic in this recording — pass --mic true"* even when you DID pass `--mic true`;+  and the `stop` hint says *"WebM (VP9 + Opus)"* though the file is h264 mp4. Trust the+  `.mp4` `filePath` + the `codec` field, not the hints.++### Max-duration cap + "● Recording" indicator (safety nets)++- A **`maxSeconds`** cap (default **600s / 10 min**) is enforced server-side: a+  watchdog auto-stops + finalizes the mp4 and fires a UI toast. To override, pass+  `maxSeconds` AND a **required `reason`** (e.g. `{"maxSeconds":1800,"reason":"30-min+  walkthrough the user asked for"}`) — the reason is shown in the toast when the cap+  hits. Overriding without a reason is rejected (400).+- While recording, Hydrogen shows a persistent **"● Recording · <source>"** badge so it's+  never silently on.+- Hydrogen does **not** stop on blur/minimize — recording while Hydrogen is backgrounded (to+  capture KiCad/Fusion) is the normal case. See [hydrogen-screen-lock](../hydrogen-screen-lock/SKILL.md).++### Taking screenshots WHILE recording (multi-AI-thread capture contention)++Adom users run several AI threads against one Hydrogen at once, so a recording started by one+thread and a screenshot requested by another routinely overlap. **Almost every capture+method coexists with a live recording — only the `adom-cli` SSE screenshot path does not.**++- ✅ **Coexist with a recording** (use these while recording): CDP `POST /screenshot`+  (Hydrogen-native, the default), `adom-bridge-cli desktop_screenshot_window` (ab per-window),+  `adom-bridge-cli desktop_screenshot_screen` (ab host GDI), pup `browser_screenshot`.+- ⚠️ **The ONE exception** — `adom-cli hydrogen screenshot panel|workspace|screen` (the SSE+  path, `PATCH /current/screenshot`). The moment a native recording is active this returns+  **`{ok:false, error:"capture_busy", retry_after_ms, alternatives:[...], _hint}`** instead of+  silently failing. **Don't blind-retry it — read `alternatives` and switch to a coexisting+  method** (CDP `POST /screenshot` is the drop-in).+- **Pre-flight when unsure:** `GET /capture/availability` reports `recording:{active,source}`,+  per-method reliability + `coexists_with_recording`, and a `recommended` method. Check it+  before a screenshot if you might be mid-recording.++Full screenshot routing + the coexistence matrix live in+[hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md) ("During a recording"). The takeaway for+*this* skill: **starting a recording does NOT lock out screenshots** — it only redirects the+SSE path to `capture_busy`; every other method keeps working.++## Audio-only — record a narration / voice track (`adom-cli hydrogen audio`)++When the user wants **just their voice, no video** — "record a narration track for+the video", "record my voice", "let me record some audio to narrate this" — use the+dedicated **`audio`** verb, NOT the video recorder:++```bash+adom-cli hydrogen audio enable                                  # activate mic (silent if cached)+adom-cli hydrogen audio start --countdown 0 --reason "narration for the demo video"+# ... user speaks ...+adom-cli hydrogen audio stop        # → filePath: audio/<name>.webm  (relative to ~/project)+```++- Output is a **WebM / Opus** audio file in **`~/project/audio/`** (the `audio` verb's+  WebM hint is **correct**, unlike the video recorder's). Reason is required.+- Other subcommands: `audio status` (mic active? device label), `audio level`+  (rms/peak/clipping/gain — warn if clipping), `audio gain <0.0-4.0>` (persists),+  `audio disable`.+- Audio-only files land in `audio/`; screen videos land in `recordings/`.++### Stitching / post-processing (ffmpeg) — install it on demand++A common follow-on is "stitch these clips together" or "lay this narration over the+video." **ffmpeg/ffprobe are NOT pre-installed** — the Hydrogen workspace image is+intentionally **lightweight**, and the AI adds tools as the user needs them. That's+not a blocker: when you need to concatenate clips, mux a narration `.webm` onto an+`.mp4`, transcode, or probe a file, **just install ffmpeg and proceed** (passwordless+sudo + apt are available; ffmpeg ~6.1 is in the repos):++```bash+sudo apt-get update -qq && sudo apt-get install -y ffmpeg   # pulls ffprobe too+```++Don't tell the user "ffmpeg isn't available here" — install it (a few seconds) and do+the job. Same pattern for any other missing tool (imagemagick, pandoc, …): install on+demand rather than declaring it impossible.++---++## (b) Hydrogen's in-app combined recorder (web-Hydrogen / legacy getDisplayMedia)++> **In Hydrogen this path is superseded by (a) native recording.** The+> getDisplayMedia-based combined recorder below is the **web-Hydrogen** mechanism+> (and the mic/level-meter detail still applies there). In Hydrogen, `adom-cli hydrogen+> recording` and the Recording dropdown route to the native recorder in (a).++Source: `src/lib/components/editor/EditorNav.svelte` (~L2418–2519).+Stores: `src/lib/stores/recordingStore.ts` + `src/lib/stores/audioCaptureStore.ts`.++The **Recording dropdown** in the editor toolbar records the workspace (and/or mic)+into one file.++**Sources** — you choose **Screen** and/or **Mic** (a recording needs at least one+enabled; the **Start recording** button is disabled with "Enable screen sharing or+microphone to start recording" until one is active):+- **Screen** row — shows the current share state; click to set up sharing (this+  routes through the Screen Capture share flow — see+  [hydrogen-capture-share](../hydrogen-capture-share/SKILL.md)).+- **Microphone** row — shows the selected device label or "Not enabled."++**While recording** the dropdown shows a **live MM:SS timer** (`recordingSeconds`),+a "Recording" status dot, and tags for the active sources (Screen / Mic), with:+- **Stop & save** — finalizes the recording.+- **Discard** — throws it away (`discardCombinedRecording()`).++**Output:** a **single `.webm`** (VP9+Opus preferred, then VP8+Opus / WebM, MP4 as+last resort). `startCombinedRecording()` muxes the screen + audio streams; **Stop &+save** finalizes and **uploads to the container's `recordings/` folder.** If the+upload fails, Hydrogen **falls back to a local browser download** named+`recording-<timestamp>.webm`.++**AI-requested auto-open:** `promptRecording(message?)` sets a prompt and the+Recording dropdown can **auto-open** so the user sees what you're asking to record+(default message: "Enable screen sharing and microphone to allow AI recording").+Store state: `{ isRecording, recordingSeconds, hasVideo, hasAudio, promptMessage }`.+API-side finalizers exist too: `stopCombinedRecordingForAPI()` (base64 data URL)+and `stopCombinedRecordingAsBlob()` (raw blob) for programmatic capture.++### Mic device picker + level/gain meter++`audioCaptureStore.ts` drives the microphone:+- **Device picker:** `enumerateAudioDevices()` lists `audioinput` devices;+  `startAudioCapture(deviceId?)` selects one; the last device persists as+  `adom:lastMicDeviceId`.+- **Level + gain meter:** the store exposes `audioLevel` (0–1 RMS), `rms`, `peak`,+  and `clipping` (peak ≥ 0.99) from an analyser node, updated ~every 50 ms — this+  is the live mic meter. **Gain** is adjustable via `setAudioGain(v)` (clamped+  **0–4**, persisted as `adom:micGain`). Watch `clipping` to warn the user their+  mic is too hot.++---++## (c) ab host desktop recording (Windows-only)++Served by the embedded **Adom Bridge** process (see+[hydrogen-adom-bridge-cli](../hydrogen-adom-bridge-cli/SKILL.md)), not by the Hydrogen frontend. These+record the **real OS desktop**, not Hydrogen's webview:++```bash+adom-bridge-cli desktop_record_start '{"reason":"Demo recording","confirmDesktopNotTabRecording":true}'+adom-bridge-cli desktop_record_stop+adom-bridge-cli desktop_record_status+adom-bridge-cli desktop_recorder_open      # open the recorder UI on the host+```++**Verify the capability first** — desktop recording is **Windows-only** and only+present when ab advertises it:++```bash+adom-bridge-cli status   # → .capabilities must include "record"+```++If `record` isn't listed, the host/runtime doesn't support it — fall back to the+in-app recorder (for the workspace) or `browser_record_start` (for a tab).++---++## Quick reference++| Intent | Command / surface | Output |+|---|---|---|+| **Record whole screen (ah)** | `adom-cli hydrogen recording start --share screen …` or `POST /recording/native/start {source:"screen"}` | **h264 mp4** → `~/project/recordings/` |+| **Record a window (ah), e.g. "record kicad"** | `POST /recording/native/start {source:"window:KiCad"}` | **h264 mp4** → `~/project/recordings/` |+| **Smaller archival recording (ah)** | `POST /recording/native/start {source:…, codec:"h265"}` | **h265 mp4** (not web-playable) |+| Record one AI browser tab | `adom-bridge-cli browser_record_start` (pup) | tab screencast |+| Record host desktop via ab (Hydrogen busy) | `adom-bridge-cli desktop_record_start '{"confirmDesktopNotTabRecording":true}'` | host recording |+| Record the workspace (web-Hydrogen) | In-app Recording dropdown (or `promptRecording()`) | `.webm` → container `recordings/` |++## Related skills+- [hydrogen-screen-lock](../hydrogen-screen-lock/SKILL.md) — native capture holds NO display wake-lock; the max-duration cap + "● Recording" indicator+- [hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md) — native CDP screenshots (the 90% "ralph test the webview" case); the capture-contention matrix — which screenshot methods coexist with a recording and which (`adom-cli` SSE) return `capture_busy`+- [hydrogen-capture-share](../hydrogen-capture-share/SKILL.md) — the (web-Hydrogen) screen-share + AI-consent flow incl. the cancelable CountdownToast+- [hydrogen-bridges](../hydrogen-bridges/SKILL.md) — `browser_record_start` (Pup tab recording) and the ab capability list (`record`, `screenshot`, …)+- `pup` — driving the Pup browser windows you'd record per-tab
skills/hydrogen-screen-lock/SKILL.mdadded+89
@@ -0,0 +1,89 @@+---+name: hydrogen-screen-lock+description: >+  Hydrogen screen capture (screenshots + recording) and the display wake-lock. A browser+  getDisplayMedia capture asserts ES_DISPLAY_REQUIRED, which blocks the user's+  screensaver and stops the display turning off → burn-in. Hydrogen is NATIVE: its+  screenshots use CDP and its recording uses Windows.Graphics.Capture (WGC) /+  DXGI — neither holds the display wake-lock — so Hydrogen does not have the web leak.+  Hydrogen still must start→capture→release per op, cap recordings, and show a "●+  recording" indicator so a forgotten recording is never silent. Read before+  adding or using Hydrogen screenshot / screen-recording features. Trigger words: 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.+---++# Hydrogen screen capture & the display wake-lock++A browser/`getDisplayMedia` screen capture asserts **`ES_DISPLAY_REQUIRED`** →+Windows won't run the screensaver or turn the display off while it's held →+**burn-in**. Web Hydrogen is forced to keep a capture stream alive all session+(the browser re-prompts otherwise), so it **leaks this lock constantly** — that+has cost real nights of burn-in. `powercfg /requests` shows the holder, e.g.+`DISPLAY: msedge.exe — screen capture`.++## Hydrogen is native — it does NOT have this leak++Hydrogen never uses `getDisplayMedia`:++- **Screenshots** use **CDP** (`Page.captureScreenshot` via `/capture/viewport` and+  `/shot`) — a one-shot grab, no `MediaStream`, no wake-lock. See+  [hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md).+- **Recording** uses **Windows.Graphics.Capture (WGC)** per-window + **DXGI Desktop+  Duplication** full-screen (Rust, `crates/hydrogen-control/src/recorder.rs`). **WGC/DXGI+  do NOT call `SetThreadExecutionState` / hold `ES_DISPLAY_REQUIRED`** — so a native+  recording does **not** block the screensaver or keep the display awake. See+  [hydrogen-recording](../hydrogen-recording/SKILL.md).++So the burn-in failure mode that bites web Hydrogen **cannot happen** from Hydrogen's own+capture. The remaining risk is only a *forgotten* recording wasting CPU/disk — which+Hydrogen guards with the rules below.++## Rules Hydrogen must follow (and already does)++- **Screenshots:** grab the frame → done. Never hold a capture between shots.+  (CDP capture is inherently one-shot; there is nothing to release.)+- **Recording:** has a server-side **max-duration auto-stop** (`maxSeconds`, default+  **600s / 10 min**; a watchdog finalizes the mp4 and emits `recording-auto-stopped`+  → a UI toast). Overriding the cap requires a `reason` so the toast can explain it.+- **Loud indicator:** while a native recording runs, Hydrogen shows a persistent **"●+  Recording · <source>"** badge (bottom-right) so it's never silently on.+- **No blur-stop for screen recording:** Hydrogen being backgrounded is the *normal* case+  for native screen recording (you're recording KiCad / Fusion / the desktop), so Hydrogen+  does **not** stop on blur/minimize — that would break the core use. The+  max-duration cap + indicator are the right nets here. (Blur-stop only makes sense+  for a `getDisplayMedia` *tab* capture, which Hydrogen doesn't use.)+- **Treat a held wake-lock as a bug.** Hydrogen shouldn't ever hold one; if you find one,+  diagnose with `powercfg /requests` (elevated) — the holder is listed under+  `DISPLAY:`.++## If you DO find the display pinned on++1. `powercfg /requests` (elevated) — read the `DISPLAY:` section for the holder.+2. If it's a **browser** (`msedge.exe`/`chrome.exe — screen capture`), that's a+   `getDisplayMedia` leak from web Hydrogen or a pup tab capture — stop that capture+   (close the tab / `browser_record_stop`), not an Hydrogen problem.+3. If it's **Hydrogen**, that's unexpected (native capture holds no lock) — capture the+   `powercfg` output and treat it as a bug to file against the recorder.++A screensaver can't clear another process's power request, so "release at the+source" is the fix. Hydrogen's native capture is release-by-construction.++## Separate concern: capture CONTENTION while recording (multi-thread)++Distinct from the wake-lock: when one AI thread is recording, another thread's+**screenshot** must not silently fail. Hydrogen handles this without holding any lock —+CDP `POST /screenshot`, ab `desktop_screenshot_window`/`_screen`, and pup+`browser_screenshot` all **coexist** with a live recording; only the `adom-cli` SSE+screenshot path returns **`capture_busy`** (with `alternatives`) so the caller switches+methods instead of getting an empty frame. Pre-flight with `GET /capture/availability`.+This is a *contention* guard, not a power-state one — full detail in+[hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md) ("During a recording") and+[hydrogen-recording](../hydrogen-recording/SKILL.md).++## Related skills+- [hydrogen-recording](../hydrogen-recording/SKILL.md) — native WGC/DXGI recording, the max-duration cap + reason, the "● Recording" indicator+- [hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md) — native CDP screenshots (no wake-lock)+- [hydrogen-notifications](../hydrogen-notifications/SKILL.md) — OS toasts + the in-app `/ui/toast`+- the `screen-capture-wake-lock` skill (formerly a gallia skill; skills now ship via adom-wiki packages) — the **web-Hydrogen** side of this issue (where the leak actually happens)
skills/hydrogen-skill-loop/SKILL.mdadded+121
@@ -0,0 +1,121 @@+---+name: hydrogen-skill-loop+description: >+  The maintainer loop for making Hydrogen's own hd-* skills perfect: when a skill is+  wrong, stale, or confusing, verify the truth against the RUNNING Hydrogen, 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 Hydrogen — a wrong one confuses every+  Adom user. For Adom devs improving Hydrogen. 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.+---++# Hydrogen Skill Loop — verify → fix live → write back to the repo → bake++Hydrogen's `hd-*` skills are baked into the WSL2 golden image and are the **only** way+the in-workspace AI knows anything about Hydrogen. 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}/<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 bootstrap bundles / the golden image bakes). 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. Hydrogen's `screenshot` returns a `dataUrl` inline when `uploadFailed:true`,+  not a saved path).+- **Read the live env**, never hardcode: `cat ~/.adom/hydrogen-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 Hydrogen source** for the endpoint/behavior (the repo checkout).+- **Not inside Hydrogen?** `adom-cli` is the SAME binary in web Hydrogen — test there.+  Or reach the user's running Hydrogen through Adom Bridge:+  ```bash+  adom-bridge-cli ping && adom-bridge-cli status        # confirm ab is on the right host+  # run anything inside the Hydrogen distro:+  adom-bridge-cli 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+**`hydrogen-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 `hydrogen-skill-test` skill.)++### 4. Pick the bucket+Workspace-specific (the `Adom-Workspace` distro's filesystem, networking, ports,+lifecycle, setup) → `wsl2/`; everything else → `shared/`. `name:` must equal the+dir; skills install flat. (See the dev skill `hydrogen-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+`hydrogen-overview` and `hydrogen-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-bridge-cli run_script '{"interpreter":"powershell","scriptB64":"<base64 of:+  cd C:\Github\hydrogen-desktop; git add skills; git commit -m \"skills: fix <name> (verified vs live Hydrogen)\"; 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 Hydrogen.++## 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 — Hydrogen reaches the host at `127.0.0.1` /+      `$(cat ~/.adom/hydrogen-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 Hydrogen'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+- `hydrogen-skill-test` (dev-internal) — the automated GRADE step (step 3b): headless-`claude` battery that asserts tool choices / forbidden patterns+- `hydrogen-skills-structure` (dev-internal) — bucket rules, naming, the deploy/bake mechanism+- [hydrogen-golden-image](../hydrogen-golden-image/SKILL.md) — the baked-image model these skills ship in+- `golden-image-bake` — the tooling that rebuilds the image from the repo+- [hydrogen-self-screenshot](../hydrogen-self-screenshot/SKILL.md) — a skill fixed via this loop (the `adom-cli hydrogen screenshot` / `dataUrl` truth)+- [hydrogen-desktop-sse](../hydrogen-desktop-sse/SKILL.md) — the adom-cli → Hydrogen-proxy → SSE interception model you test against
skills/hydrogen-staying-current/SKILL.mdadded+42
@@ -0,0 +1,42 @@+---+name: hydrogen-staying-current+description: How a Hydrogen workspace stays current — its Adom tooling (CLIs, skills, apps) updates registry-native via `adom-wiki pkg update` + the `adom/hook` prompt hook, plus the code-server reload banner — and how an Adom employee ships a skill / CLI / app to every workspace. Use when asked "how do I get the latest skills or tooling", "how do updates work here", "what's the reload banner", "how do I distribute a tool to all Adom users", or about the retired workspace-updater daemon (history). Trigger words: staying current, how do updates work, latest skills, get updates, auto update, keep tooling current, distribute a skill, publish to all users, reload banner, adom-wiki pkg update, workspace updater, adom-workspace-updater.+---++# How this workspace stays up to date++## TL;DR (anyone)+You don't do anything. Your workspace's Adom tooling is **registry-native**: everything is an+`adom-wiki` package (CLIs, skills, apps), and the **auto-update hook** (`adom/hook`, installed by+`adom/core`) runs `adom-wiki pkg update` in the background (throttled ~30 min, on your prompts, for+both Claude Code and Codex) and re-resolves `adom/core` so newly added tools arrive automatically.+- Force a check now: `adom-wiki pkg update`+- See what's installed / behind: `adom-wiki pkg list` / `adom-wiki pkg outdated`++Skills are **bundled into the Hydrogen bootstraps** — `adom/hydrogen-bootstrap` ships the platform-generic+hd-* skills, `adom/hydrogen-windows-bootstrap` ships the WSL2-specific ones (Kyle's `adom/hydrogen-mac-bootstrap`+ships the mac ones), each deployed flat into `~/.claude/skills/`. (There is no separate skills+package; the short-lived `adom/hydrogen-skillpack` was deleted 2026-07-16.) Org member apps (e.g. the+employee build of adom-google) arrive via your org's **`org-bootstrap`** meta-package. All are just+dependencies — kept current like everything.++## The reload banner+code-server self-updates on disk but only takes effect after a restart. Hydrogen detects+that natively and shows **"code-server has an update — click to reload"** at the top of the editor+(only after setup is complete, never mid-cascade). Clicking restarts code-server + reloads the+editor. Extensions never banner — VS Code's own auto-update activates them.++## "How do I ship a tool/skill/app to every Adom workspace?" (Adom employees)+Publish it as an `adom-wiki` package, then add it as a **dependency** of the right bootstrap:+- every container: `adom/core` · every Hydrogen workspace: `adom/hydrogen-bootstrap` ·+  org members only: your org's `org-bootstrap` · Hydrogen skills: add the skill to+  `skills/public-facing/` in the hydrogen-desktop repo (`shared/` for platform-generic, `wsl2/` for+  Windows-only) and republish the matching bootstrap (`adom/hydrogen-bootstrap` or+  `adom/hydrogen-windows-bootstrap`).+The hook's re-resolve delivers it fleet-wide within ~30 minutes of users prompting.++## The retired daemon (history)+`adom-workspace-updater` (a systemd timer converging a wiki manifest) previously delivered+tooling in-place. It was retired 2026-07-16 in favor of the registry updater; legacy distros are+migrated off it automatically by its final self-update. If you still see it:+`systemctl status adom-workspace-updater.timer` should be disabled/absent on a migrated workspace.
skills/hydrogen-webview/SKILL.mdadded+123
@@ -0,0 +1,123 @@+---+name: hydrogen-webview+description: >+  WEBVIEW — what the word "webview" means in Hydrogen and exactly how to+  open one. When the user says "show me in webview", "open it in webview", "in a+  webview", "webview panel", "wv", or asks to show ANY app/page/tool "in webview",+  it ALWAYS means a native Hydrogen workspace webview tab opened via+  `adom-cli hydrogen webview open-or-refresh` — NEVER VS Code's built-in Simple+  Browser, never an editor-internal browser, never an iframe hack. ALSO covers the+  External Webview (extwv): the opt-in real-browser panel for sites that refuse to+  be embedded (Google, Amazon), and the "This site can't be embedded" handoff card.+  Read this the moment the word webview (or wv, or extwv) appears. Trigger words —+  webview, wv, extwv, external webview, show me in webview, open in webview, in a+  webview, webview tab, webview panel, native webview, show in webview, launch in+  webview, webview app, site can't be embedded, refuses to embed, x-frame-options.+---++# hydrogen-webview — "webview" means a native Hydrogen webview tab. Period.++When a user (or a setup prompt) says **"show me X in webview"**, they mean a+**native Hydrogen webview tab** — a first-class panel in the Hydrogen+workspace, next to their editor. Nothing else qualifies.++## ⛔ NEVER (the 2026-07-16 incident — do not repeat)++- **NEVER open VS Code's built-in "Simple Browser"** (or any editor-internal+  browser page) to satisfy a webview request. A setup-thread AI did this once —+  the user called it "an awful outcome that can never happen again." The Simple+  Browser is not the Hydrogen webview: it's cramped, it isn't a workspace tab, and it+  breaks every downstream flow (screenshots, captions, panel routing).+- Don't fall back silently to pup or a native browser either — those are+  *different, deliberate* destinations (see `hydrogen-open-url` for the decision+  guide). "webview" is not ambiguous; it names exactly one thing.++## ✅ The exact command (idempotent — safe to re-run)++```bash+# open (or refresh, if a tab with this name already exists) a webview tab:+adom-cli hydrogen webview open-or-refresh --name "<Tab Name>" --url "<url>" --panel-id <id>++# ⚠️ --panel-id is REQUIRED to CREATE a new tab (without it: "Tab 'X' not found+#    and --panel-id not provided"). Get a panel id first:+adom-cli hydrogen workspace get        # or: adom-cli hydrogen workspace tabs+# Re-running with the same --name once the tab exists just navigates/refreshes+# (no --panel-id needed). The --name is your stable handle.+```++Local Adom apps (parts search, chip-fetcher, adom-tsci previews, wiki pages)+are all shown this way: start/locate the app's local URL, then+`open-or-refresh` it into a named tab.++## Fullscreen: the 90% dialog++Every webview toolbar has a **fullscreen button**: it grows the page to a 90%+dialog over the Hydrogen window (never real OS fullscreen — the content stays+sandboxed inside Hydrogen). Exit via the close button, clicking outside, or Esc. Pages+that call the browser fullscreen API get the same 90% treatment.++## Verify you actually did it++The tab must appear in `adom-cli hydrogen workspace tabs` with your `--name`.+If it isn't there, you did NOT open a webview — go again.++For the broader "which surface should this URL open in?" decision (webview vs+pup vs native browser vs headless fetch), see **hydrogen-open-url**.++## extwv — External Webview: the real-browser panel for sites that refuse embedding++The default webview tab is an **iframe**, so sites that send+`X-Frame-Options: deny/sameorigin` or a restrictive CSP `frame-ancestors`+(Google, Amazon, most login walls) will not load in it. Hydrogen 0.1.257+ handles this+with a second panel type: **External Webview (extwv)** — the same tab/pane+ergonomics, but backed by a REAL native browser surface (WebView2) that loads+anything.++**How users hit it:** navigating a webview to a blocked site shows a handoff+card — "This site can't be embedded" — with two buttons: **Open in External+Webview** (a new extwv tab in the same pane) and **Open in Hydrogen Browser Window**+(hbw). Users can also add one directly from the **+ panel menu → "External+Webview"**.++**Check embeddability up front** (before choosing a surface for a known URL):++```bash+# Hydrogen control API — returns {"embeddable": bool, "reason": "...", "title": "<page title>"}+GET http://127.0.0.1:{control}/webview-can-embed?url=https://google.com+# → embeddable=false, reason "x-frame-options: sameorigin" → use extwv (or hbw)+```++**Open an extwv tab programmatically** (Hydrogen control API; the `adom-cli hydrogen+webview` verbs target the DEFAULT webview panel type, not extwv):++```bash+POST http://127.0.0.1:{control}/workspace/tabs+{"panelType": "adom/a1b2c3d4-0032-4000-a000-000000000032",+ "displayName": "Google", "initialState": {"url": "https://google.com"}}+```++**Know the trade-offs** (why extwv is opt-in, not the default):+- It is a native OS surface: **Hydrogen's own HTML can never draw on top of it**+  (menus and overlays are designed around it, but don't try to float custom UI+  over an extwv pane).+- Heavier than an iframe tab; prefer the default webview whenever the site+  embeds fine.+- Reload lives in the tab's right-click menu (0.1.258+), and the tab's hover+  tooltip identifies the panel as "External Webview (extwv)" (0.1.261+), so you+  can always tell the two apart.++**Vocabulary:** "open it in extwv" == this panel. See the shared Adom glossary+(`definitions` skill / wiki.adom.inc/adom/definitions) for the full surface+vocabulary.++## hbw — Hydrogen's standalone browser window (the fourth surface)++Under Hydrogen there is one more surface: **hbw**, Hydrogen's standalone built-in+Tauri browser window (picker label "Hydrogen Browser Window", destination id+`webview-window`, window label `hbw-*`). It is a separate OS window Hydrogen manages+directly — bigger than a pane tab, still not a real browser. Open one first-class via+`POST /hbw/open {"url": ...}` on Hydrogen's control API (0.1.113+), or pick "Hydrogen Browser+Window" in the Browser Picker. The surface shortlist: **wv** (this skill's webview+panel; Hydrogen's pane tab inside Hydrogen), **extwv** (the real-browser pane tab for embed-blocked+sites, above), **pup**, **nb** (the user's native browser via abe), **hbw**. VS+Code's Simple Browser is NEVER one of them.
skills/hydrogen-wiki-review/SKILL.mdadded+48
@@ -0,0 +1,48 @@+---+name: hydrogen-wiki-review+description: >+  After editing ANY wiki.adom.inc page (an adom-wiki push/publish, a README/doc/hero change),+  immediately LOAD that page in pup so the user can see and double-check the rendered result —+  don't just report the commit hash and move on. Covers how to open the page in pup on the+  user's machine, refresh it, and post a screenshot to the shotlog for review.+  Trigger words: wiki update, adom-wiki push, adom-wiki publish, wiki page, show wiki in pup,+  review wiki, verify wiki page, load wiki page, wiki hero, wiki readme, double-check wiki.+---++# Show every wiki edit in pup for review++Whenever you change a wiki.adom.inc page — `adom-wiki repo push`, `adom-wiki pkg publish`, or+any edit to a page's README / docs / hero — **open that page in pup right away** so the user+can eyeball the rendered result. A successful push is not proof the page looks right; the user+wants to verify it. Never stop at "pushed, commit abc123."++## Do this after every wiki change++```bash+# 1. Push / publish the page (the change itself).+adom-wiki repo push <slug> -m "…" --files README.md docs/foo.md+#    (or: adom-wiki pkg publish --org adom)++# 2. Immediately load the page in pup on the user's machine so they can review it.+adom-bridge-cli browser_open_window '{"sessionId":"wiki-review","profile":"wiki-review","url":"https://wiki.adom.inc/<owner>/<slug>"}'+#    Already open from a prior review? Refresh + cache-bust instead of opening a duplicate:+#    adom-bridge-cli browser_navigate '{"sessionId":"wiki-review","url":"https://wiki.adom.inc/<owner>/<slug>?cb=2"}'++# 3. Screenshot it and post to the shotlog with a one-line description, then hand the user the URL.+```++## Rules++- **Always the real `https://wiki.adom.inc/...` URL** — never a `localhost:<port>` (pup/webviews+  can't reach those).+- **Proactive, not offered** — load it without being asked; the whole point is the user reviews+  every wiki change as it happens.+- **Wiki ops go through the `adom-wiki` CLI**, not raw API calls.+- Keep related content on ONE page (e.g. all Hydrogen docs live on the `hydrogen-desktop`+  page) — don't spray a new wiki page per small doc.++## Why++The user runs the same setup as every Adom user and validates the shared experience by eye.+Loading each wiki edit in pup closes the loop: they confirm the page renders correctly (hero,+screenshots, links, formatting) instead of trusting a silent push.