bootstrap
Adom Hydrogen (platform base)
Public Made by Adomby adom
Layer Hydrogen's workspace onto a standard Adom workstation.
← Commit history
0.5.30: hydrogen-widget: curated widgets list, the clock widget, the hydrogen-widget skill
9 files changed
+1070−4
bin/hydrogen-widget-clockadded+12@@ -0,0 +1,12 @@+#!/usr/bin/env python3+"""The smallest possible Hydrogen widget: a clock. Shows the minimum a widget binary needs:+serve one page on $HYDROGEN_WIDGET_PORT."""+import os, sys+from importlib.machinery import SourceFileLoader+lib = SourceFileLoader('hwlib', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hydrogen-widget-lib.py')).load_module()+import datetime+def data():+ now = datetime.datetime.now()+ return {'time': now.strftime('%H:%M'), 'date': now.strftime('%a %d %b')}+RENDER = r"""(`<span class="big">${d.time}</span><span class="lbl">${d.date}</span>`)"""+lib.serve('Clock', data, RENDER, 15000)
bin/hydrogen-widget-lib.pyadded+65@@ -0,0 +1,65 @@+#!/usr/bin/env python3+"""Shared shape for the sample Hydrogen widgets: a tiny HTTP server that serves one page for+the title-bar slot and a JSON feed the page polls. A widget binary only needs: serve on+$HYDROGEN_WIDGET_PORT, answer GET / with HTML sized for a ~30 px strip, and keep the JSON fresh.+Hydrogen hands the slot over as soon as GET / answers (or the binary can POST+$ADOM_HD_CONTROL_URL/widgets/navigate {id, url} itself)."""+import json, os, sys, time, threading, urllib.request+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer++PAGE = """<!doctype html><meta charset="utf-8"><title>%(title)s</title>+<style>+html,body{margin:0;height:100%%;background:transparent;color:#e6ebef;font:11px/1.2 "Familjen Grotesk","Satoshi",system-ui,sans-serif;overflow:hidden}+.w{display:flex;align-items:center;gap:6px;height:30px;padding:0 6px;white-space:nowrap}+.lbl{opacity:.75;font-size:9px;letter-spacing:.04em;text-transform:uppercase}+.bar{position:relative;width:38px;height:6px;border-radius:3px;background:rgba(255,255,255,.14);overflow:hidden}+.bar i{position:absolute;left:0;top:0;bottom:0;background:#19b3a6;border-radius:3px}+.bar.warn i{background:#f0a060}.bar.hot i{background:#e5484d}+.v{font-variant-numeric:tabular-nums;min-width:26px;text-align:right}+.big{font-size:14px;font-weight:600;font-variant-numeric:tabular-nums}+.err{color:#f0a060;font-size:10px}+</style><body><div class="w" id="w">%(title)s…</div>+<script>+const R=%(refresh_ms)d;+function bar(l,p){const c=p>=90?'hot':p>=70?'warn':'';return `<span class="lbl">${l}</span><span class="bar ${c}"><i style="width:${Math.max(0,Math.min(100,p))}%%"></i></span><span class="v">${Math.round(p)}%%</span>`;}+async function tick(){try{const r=await fetch('/data.json',{cache:'no-store'});const d=await r.json();document.getElementById('w').innerHTML=%(render_js)s;}catch(e){document.getElementById('w').innerHTML='<span class="err">no data</span>';}}+tick();setInterval(tick,R);+</script></body>"""++def serve(title, data_fn, render_js, refresh_ms=60000):+ port = int(os.environ.get('HYDROGEN_WIDGET_PORT') or (sys.argv[sys.argv.index('--port')+1] if '--port' in sys.argv else 8930))+ cache = {'t': 0, 'v': {'error': 'starting'}}+ lock = threading.Lock()+ def data():+ with lock:+ if time.time() - cache['t'] > refresh_ms/1000 - 1:+ try: cache['v'] = data_fn()+ except Exception as e: cache['v'] = {'error': str(e)[:120]}+ cache['t'] = time.time()+ return cache['v']+ class H(BaseHTTPRequestHandler):+ def log_message(self, *a): pass+ def _send(self, code, ctype, body):+ self.send_response(code); self.send_header('content-type', ctype); self.send_header('cache-control', 'no-store')+ self.send_header('access-control-allow-origin', '*'); self.end_headers(); self.wfile.write(body)+ def do_GET(self):+ if self.path.startswith('/data.json'): self._send(200, 'application/json', json.dumps(data()).encode())+ elif self.path.startswith('/health'): self._send(200, 'application/json', b'{"ok":true}')+ else: self._send(200, 'text/html; charset=utf-8', (PAGE % {'title': title, 'refresh_ms': refresh_ms, 'render_js': render_js}).encode())+ srv = ThreadingHTTPServer(('127.0.0.1', port), H)+ # Tell Hydrogen we are up (the health poll would find us anyway; this is the explicit handshake).+ ctl = os.environ.get('ADOM_HD_CONTROL_URL'); wid = os.environ.get('HYDROGEN_WIDGET_ID')+ if ctl and wid:+ def announce():+ try:+ proxy = os.environ.get('HYDROGEN_PROXY_PORT', '')+ url = f"http://{port}.localhost:{proxy}/" if proxy else f"http://127.0.0.1:{port}/"+ req = urllib.request.Request(f"{ctl}/widgets/navigate", data=json.dumps({'id': wid, 'url': url}).encode(), headers={'content-type': 'application/json'})+ urllib.request.urlopen(req, timeout=5)+ except Exception: pass+ threading.Timer(0.5, announce).start()+ srv.serve_forever()++def http_json(url, headers):+ req = urllib.request.Request(url, headers=headers)+ with urllib.request.urlopen(req, timeout=15) as r: return json.loads(r.read().decode())
hydrogen-widget.jsonadded+11@@ -0,0 +1,11 @@+{+ "hydrogen_widget": "1.0.0",+ "id": "clock",+ "name": "Clock",+ "icon": "mdi:clock-outline",+ "brief": "The time and date. The smallest possible widget, the one to copy when you make your own.",+ "width": 110,+ "install": "adom/hydrogen-bootstrap",+ "launch": { "command": "hydrogen-widget-clock", "port": 8933 },+ "health": { "url": "/", "timeout_seconds": 60 }+}
install.sh+5@@ -472,3 +472,8 @@ UNIT fi } cs_gate++# hydrogen-widget (adom/hydrogen#17): the clock widget and the shared widget helper for Hydrogen's title bar.+HERE_W="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"+mkdir -p "$HOME/.local/bin"+for w in "$HERE_W"/bin/hydrogen-widget-*; do [ -f "$w" ] && install -m 0755 "$w" "$HOME/.local/bin/$(basename "$w")"; done
package.json+2−2@@ -4,7 +4,7 @@ "type": "bootstrap", "title": "Adom Hydrogen (platform base)", "brief": "Layer Hydrogen's workspace onto a standard Adom workstation.",- "version": "0.5.29",+ "version": "0.5.30", "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": [@@ -33,4 +33,4 @@ "hydrogen editor configuration", "where do hydrogen skills come from" ]-}+}
page.json+2−2@@ -4,7 +4,7 @@ "type": "bootstrap", "title": "Adom Hydrogen (platform base)", "brief": "Layer Hydrogen's workspace onto a standard Adom workstation.",- "version": "0.5.29",+ "version": "0.5.30", "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": [@@ -49,4 +49,4 @@ "hydrogen editor configuration", "where do hydrogen skills come from" ]-}+}
skills/hydrogen-api/SKILL.mdadded+837@@ -0,0 +1,837 @@+---+name: hydrogen-api+description: >+ Complete reference for Hydrogen control API endpoints. The control+ API runs on a dynamic port (find it in the Hydrogen log or via port discovery). Use+ these endpoints to drive Hydrogen programmatically: manage the workspace (the WSL2 `Adom-Workspace` distro), control+ Claude Code, manipulate VS Code, manage workspace tabs, run setup steps, trigger+ virgin reset, and more. The COMPLETE, always-current catalog is+ GET /_manifest (every route with a category + description; curated entries are+ richer). The API Explorer window (Adom menu > API Explorer, or POST /api-explorer/open)+ renders it with a runnable Try per endpoint. GET /test/help lists the gated /test/*+ diagnostics. NEVER blind-call destructive endpoints (see the Safety section).+ Trigger words: Hydrogen API, control API, Hydrogen endpoint, container exec, claude api,+ iframe eval, workspace tabs, setup run-all, workspace status, reload vscode,+ browser profiles, open url, virgin reset api, reboot api, diagnostics,+ ad health, test window-tree, ai cursor, setup panel, screenshot mode.+---++# Hydrogen Control API Reference++The control API is large and grows. This file quoted "~159 endpoints" in its body and+"~200" in its own frontmatter while the router carried **274**: two stale numbers that did+not even agree with each other, in the one document whose job is to be accurate about the+API. So it no longer quotes a count at all. Don't trust a static count or list in+this file to stay exhaustive — the API is **self-documenting**, with two live+sources of truth you should hit first:++| Source | What it gives you | Where |+|---|---|---|+| `GET /_manifest` | **THE complete catalog** — every registered route (`path`, `method`, `args`, `category`, `description`, `visibility`). Curated entries carry richer descriptions; the rest are auto-generated from the router (`scripts/gen-manifest.py`), so it never drifts. **Discover here first — don't guess paths.** | `hydrogen-control/src/lib.rs` `/_manifest` arm |+| **API Explorer window** | The human face of `/_manifest`: every endpoint grouped by category tab, with a runnable **Try** + Copy URL. Open from the Adom menu, or `POST /api-explorer/open`. | Adom menu ▸ API Explorer |+| **Workspace control** (`:47083`) | A SEPARATE server (`hydrogen-proxy`), and its ~30 routes are **NOT in `/_manifest`**: `/tabs`, `/tabs/find`, `/active-tab`, `/splits`, `/swaps`, `/moves`, `/nav`, `/window-mode`, `/caption`, `/recording`, `/screenshot`, `/audio`, `/sharing`, `/notification`. This is the surface the in-container AI reaches through `adom-cli`. Enumerating `/_manifest` alone will NOT show you these. | `hydrogen-proxy/src/lib.rs` |+| `GET /test/help` | The `/test/*` diagnostics (window enumeration, UIA click, dialog classification, cascade reset). **Gated behind `HD_TEST_API_ENABLED=1`** — 404 by default (expected, not a bug). | `hydrogen-control/src/lib.rs` `/test/help` arm |++## ⛔ Safety — the API enforces this, and tells you in-band++The API itself now labels, warns about, and gates dangerous endpoints (single source of+truth: `safety.rs` in hydrogen-control). Three tiers:++- **`catastrophic`** — irreversibly destroys real user state: `/setup/virgin-reset`,+ `/setup/panel/(run-virgin-reset|wipe-keep-auth)`, `/wsl/unregister` (deletes the whole+ workspace distro), `/system/reboot` (reboots the+ user's COMPUTER), `/config-reset`, `/claude/wipe-creds`, `*-delete`.+ **Headless calls (no browser Origin header — i.e. YOU) get HTTP 409 `confirmation_required`+ and NOTHING happens** until you re-call with `?confirm=1`. Only add `?confirm=1` when the+ user explicitly asked for exactly that action — never to make an error go away.+- **`destructive`** — kills/restarts/re-runs real state (recoverable but real):+ `/app/(restart|relaunch-frontend)`, `/reload-vscode`,+ `/container(-stop|-restart)`, `/workspace/apply-updates`, `/setup/(run-all|run-step|state|+ force-close|trigger|countdown)`, `/claude/tabs/close-all`, `/updater/install`.+ Never call these speculatively or in a sweep.+- **`disruptive`** — the user SEES it (window opens, focus moves, typing lands in their+ real chat, screen recording starts): `/hbw/open` (and `/hbw/close` to close one by label), `/api-explorer/open`, `/open-url`,+ `/open-in-profile`, `/window/foreground`, `/ai-cursor/*`, `/recording/native/start`,+ `/ui/toast`, `/claude/(trigger-auth|authorize-in-browser|open-with-message|inject*|send|+ submit|uia-*|new-chat-and-send|type-and-submit)` (the send/submit ones SPEND a real user+ turn). Run only when the user actually wants that action, and clean up after yourself.+ (Calling `/claude/authorize-in-browser` blindly is how you spray OAuth windows into Edge.)++How the API tells you:+- **Before calling:** every `GET /_manifest` entry carries `safety` + `safety_note`. Check+ them before you POST anything unfamiliar. Unlabeled = safe.+- **When you call:** responses from these endpoints include a `_safety` block. READ IT —+ it says exactly what you just did to the user's machine.+- **The 409 gate** is your last chance to stop; it is not an error to route around.++A safe GET/POST returns within a couple seconds with a 2xx or a clean 4xx `{ok:false,...}`.+If you must probe an endpoint you're unsure about, POST an empty `{}` body and treat a+graceful validation error as "it works" — a 500 or a hang is the bug.++**Base URL:** `http://127.0.0.1:<dynamic>` — the same loopback address from inside the+workspace (WSL2 mirrored networking shares loopback with the Windows host) and on the+Windows host. The port is dynamic per launch; read the live URL from+`~/.adom/hydrogen-control-url` — see "Finding the Control Port" below.++## Finding the Control Port++The control API port is dynamic (default 47084, auto-resolves conflicts).++```bash+# From Hydrogen log+adom-bridge hd_log '{"tail":100}' | grep "control="++# From ports.json+type %APPDATA%\hydrogen-desktop\ports.json++# From inside the WSL2 workspace — USE THIS. Hydrogen writes its live control URL to a discovery+# file every launch (mirrored loopback), so you never hunt for the dynamic port:+CTRL="$(cat ~/.adom/hydrogen-control-url)"; curl -sf "$CTRL/health"++# No file access (remote tool, browser)? Probe the DISCOVERY responder: fixed candidate+# ports tried in order (a single fixed port is not bindable on every Windows box; WSL2+# mirrored networking claims ports invisibly). Take the FIRST that answers with+# service == "hydrogen-desktop-discovery"; its control_url is the live control API:+for p in 47080 48080 49180 50280; do+ R=$(curl -sf --max-time 2 "http://127.0.0.1:$p/") && echo "$R" | grep -q hydrogen-desktop-discovery && break+done+```++⭐ **From the workspace, read `~/.adom/hydrogen-control-url`** (e.g. `http://127.0.0.1:<control>`) — a+file, NOT an env var (your non-interactive Bash shells don't source `.bashrc`/`profile.d`). Hydrogen+rewrites it with the live dynamic port every launch. This is the channel for the UI command bus,+the live ports map, Claude control, etc. — distinct from `adom-cli` (`ADOM_HYDROGEN_URL` → SSE),+which is unchanged for web-hydrogen parity.++## Calling gotchas (learned the hard way)++- **Never hardcode the port.** It's dynamic (47084 is only the default and it auto-resolves+ conflicts). Read `~/.adom/hydrogen-control-url` from the workspace, or `GET /ports`. Hardcoding+ 47084 is what makes tools show Hydrogen as "down" when it's actually up on another port.+- **Calling from a REMOTE container (hw/cloud)? Use `adom-bridge hd_api`,** the relay's+ pass-through verb: `hd_api {"method":"POST","path":"/...","body":{...}}`. It auto-discovers+ the port; never shell curl through the relay (quoting gets mangled). Long-blocking calls+ (typed injection) can outlive the relay timeout: the work still completes on Hydrogen's side, so+ verify state instead of re-firing.+- **Getting files OUT (recordings, screenshots): streamed binary, never base64.** Endpoints+ return paths inside the WSL distro; a remote container pulls them with `adom-bridge+ --target <desktop> pull_file {filePaths:["\\\\wsl$\\Adom-Workspace\\<path>"], saveTo:...}`+ (1 MiB binary chunks, sha256-verified, no practical size limit). `/recording/native/stop`+ returns the ready-made `fileWindows` UNC path. Full demo-video pipeline: the+ `hydrogen-demo-recording` skill.+- **Confirm it's really the control API, not the SPA.** In dev, the frontend file server+ answers 200 with HTML for *any* path — so a bare 200 doesn't prove you hit Hydrogen's API. Check+ `GET /health` returns JSON with `service:"hydrogen-desktop-control"`.+- **Driving Hydrogen from a page Hydrogen serves? Use relative fetches, not Tauri IPC.** Tauri `invoke`+ is unavailable on remote http origins; a same-origin `fetch('/ports')` / `fetch('/_manifest')`+ works and sidesteps port discovery entirely.+- **Driving from the CLOUD over the relay:** if two Adom Bridges are connected (laptop ++ a VM), `desktop_*` verbs need `--target <name>` (e.g. `AdomLapper`) or they no-route and+ look empty; `nbrowser_*`/`hd_*` auto-route. PowerShell emits **CRLF** — strip `\r` before+ string-comparing its output. Long calls can exceed the relay's per-call timeout — run+ detached + poll a marker (see `hydrogen-cloud-relay-patterns`).++## Health & Status++| Method | Path | Description |+|---|---|---|+| GET | `/health` | Hydrogen alive check — returns `{ok, port, service}` |+| GET | `/buildinfo` | Running build git SHA + build metadata (the only trustworthy "did my build ship?" check) |+| GET | `/ports` | The 4 editable PortConfig ports (proxy, control, cdp, code_server) |+| GET | `/ports/all` | LIVE full port map — every host + WSL2 port Hydrogen has bound, with status + mirrored-mode info |++## ab-health diagnostics (`/ab/*`)++(Canonical since 1.0.47; the old `/ad/*` spellings still answer as aliases.)++These verify the **adom-bridge / relay / embedded-ab** half of Hydrogen is alive (the+bridge that connects this cloud container to Hydrogen). All GET, all return `{ok, ...}`.++| Method | Path | Description |+|---|---|---|+| GET | `/ab/health` | Overall embedded-ab health roll-up |+| GET | `/ab/process` | Is the adom-bridge process running |+| GET | `/ab/embedded` | Embedded-ab module status |+| GET | `/ab/workspace-direct` | ⭐ The **converged** check — direct path to the workspace relay (use this one) |+| GET | `/ab/relay` | Relay (WS/HTTP) reachability |+| GET | `/ab/liveness-beacon` | Liveness beacon freshness |+| GET | `/ab/direct-api` | Direct-API reachability |+| GET | `/ab/discovery-file` | Discovery file present + parseable |++## Driving Hydrogen's UI (command bus)++Open/close/toggle any Hydrogen menu, dialog, or panel — the first-class alternative to CDP clicking. See the `hydrogen-ui` skill.++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/ui/actions` | — | List every drivable UI action `{id,label,description,group}` (+ `_hints`) |+| POST | `/ui/invoke` | `{"id":"ports.open"}` | Run a UI action; returns `{ok,value}` only after the frontend ran it |+| GET | `/auth-token` | Hydrogen session token |+| GET | `/setup-status` | Overall setup completion state |+| GET | `/diagnostics/all` | Run all diagnostic checks |+| GET | `/diagnostics/{name}` | Run a single diagnostic check |++## Workspace Management++These endpoints manage the workspace: the WSL2 distro `Adom-Workspace`. The paths+keep their historical `container` spelling; there is no container.++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/container-status` | — | `runtime_available` (WSL2 usable), `workspace_installed` (distro present), workspace exists/running |+| GET | `/container-stats` | — | CPU, memory, disk usage |+| POST | `/container-restart` | — | Restart the workspace (5s timeout) |+| POST | `/container-stop` | — | Stop the workspace |+| POST | `/container-exec` | `{"command":"..."}` | Run command in the workspace as user `adom` |+| POST | `/container-delete` | — | Remove the workspace |+| GET | `/container/logs` | — | Workspace stdout/stderr |+| GET | `/container/env` | — | Workspace environment variables |+| GET | `/container/ports` | — | Forwarded port mappings |+| GET | `/container/disk` | — | Disk usage inside the workspace |+| POST | `/container/restart` | — | Alias for `/container-restart` |+| POST | `/direct/command` | `{...}` | Direct-connect command channel into the workspace |++> `GET /container-status` renamed two fields: `docker_available` → **`runtime_available`**+> and `image_pulled` → **`workspace_installed`**. Old field names are gone.++### Workspace / WSL endpoints++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/wsl/status` | — | WSL distro state (registered, running, code-server reachable) |+| GET | `/workspace/health` | — | Deep workspace health (distro + code-server + host reachability) |+| GET | `/virgin-reset-progress` | — | Live progress of an in-flight virgin reset |+| POST | `/workspace/terminal-profile` | — | Ensure the stable `Adom-Workspace` Windows Terminal profile (idempotent) |+| POST | `/workspace/terminal-profile/remove` | — | Remove the `Adom-Workspace` Windows Terminal profile(s) |+| POST | `/workspace/check-updates` | — | Check upstream for newer code-server + Claude extension; stage them |+| POST | `/workspace/apply-updates` | — | Apply staged code-server/extension updates + reload editor |++> ⛔ `POST /wsl/unregister` is **REFUSED as deprecated** — it was a headless+> destructive reset (deletes `/home/adom/project`). Use the Setup panel's Virgin+> Reset or `POST /setup/panel/run-virgin-reset` instead.++## WebView & VS Code++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/eval` | `{"js":"..."}` | JS eval in main Hydrogen webview |+| POST | `/iframe-eval` | `{"js":"...","contextIndex":N}` | JS eval in a specific iframe context (0-14) |+| POST | `/cdp-eval` | `{"expression":"..."}` | Direct Chrome DevTools Protocol eval |+| POST | `/click` | `{"x":N,"y":N}` | Click at coordinates in webview |+| POST | `/mouse-click` | `{"x":N,"y":N,"button":"left"}` | Mouse click with options |+| POST | `/query` | `{"selector":"..."}` | Query DOM elements |+| POST | `/reload-vscode` | — | Reload the VS Code iframe |+| POST | `/vscode/hide-activity-items` | — | Hide activity-bar items (persisted, overflow-proof) |+| GET | `/targets` | — | List CDP targets (all webview contexts) |+| GET | `/cdp-info` | — | CDP connection info (port, URL) |+| GET | `/devtools` | — | DevTools status |++### AI cursor (visible cursor for demos / clicking)++Distinct from `/click` and `/mouse-click` — this renders a **visible** AI cursor,+glides it, and can click. Use for demos or when the user should see where the AI clicks.++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/ai-cursor/move` | `{"x":N,"y":N}` | Move (glide) the visible AI cursor to coordinates |+| POST | `/ai-cursor/click` | `{"x":N,"y":N}` | Move the visible AI cursor there + click |+| POST | `/ai-cursor/hide` | — | Hide the visible AI cursor |++### Screenshot primitives++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/capture/viewport` | — | Capture the Hydrogen webview viewport (PNG) |+| POST | `/shot` | `{...}` | Screenshot primitive (see **hydrogen-self-screenshot** for the full workflow) |++### Port hints++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/adom-port-hints` | — | Read the workspace port-hints map |+| POST | `/adom-port-hints` | `{...}` | Write/merge the workspace port-hints map |++## Claude Code++### Status / auth state (GET)++| Method | Path | Description |+|---|---|---|+| GET | `/claude/status` | Install state, auth, active session, version |+| GET | `/claude/auth-check` | Deep auth check — file exists, token, expiry |+| GET | `/claude/auth-status` | Auth-status detail |+| GET | `/claude/auth-page-state` | Which auth screen the panel shows: `continue-in-browser` \| `subscription-button` \| `chat`; returns `oauth_url` + `at_browser_auth_gate` |+| GET | `/claude/detect-auth-failure` | Scan webview contexts for auth button/errors |+| GET | `/claude/sessions` | List active Claude sessions |++### Auth actions (POST)++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/claude/find-auth-button` | — | Find + highlight the sign-in button WITHOUT clicking (validation) |+| POST | `/claude/start-auth` | — | Find + highlight + CLICK the "Claude.ai Subscription" button to begin OAuth |+| POST | `/claude/trigger-auth` | — | Alias for start-auth |+| POST | `/claude/detect-state` | — | Detect current auth/panel state |+| POST | `/claude/authorize-in-browser` | `{"timeout_ms":N,"dry_run":B,"fresh":B,"oauth_url":"..."}` | Auto-click the "Authorize" button on the claude.ai OAuth page (unattended sign-in) |+| POST | `/claude/authorize-tab-enter` | — | Approve the Adom OAuth tab via Enter (branded picker flow) |+| POST | `/claude/inject-creds` | — | Push host backup credentials into the workspace |+| POST | `/claude/wipe-creds` | `{"backup":B}` | Wipe Claude credentials (destructive); `backup:false` keeps the host backup so reinject can restore |++### Panel / conversation control (POST unless noted)++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/claude/open` | — | Open Claude Code panel (`claude-vscode.editor.open`) |+| POST | `/claude/ensure-open` | — | Ensure the panel is open (strict readiness; recovery if not rendered) |+| POST | `/claude/open-with-message` | `{"message":"..."}` | Open + pre-fill message |+| POST | `/claude/focus` | — | Focus Claude panel |+| POST | `/claude/new-conversation` | — | Start a fresh conversation (shows sign-in panel when unauthenticated) |+| POST | `/claude/new-chat-and-send` | `{"text":"...","typed":B,"submit":B,"delay_ms":N}` | Open fresh conversation, wait for compose box, type + (default) submit |+| POST | `/claude/submit` | — | Find + click send button |+| POST | `/claude/send` | `{"message":"..."}` | Send message to Claude |+| POST | `/claude/inject` | `{"message":"..."}` | Inject text into the compose box |+| POST | `/claude/inject-message` | `{"message":"..."}` | Inject into conversation |+| POST | `/claude/type-and-submit` | `{"message":"...","delay_ms":N}` | Type char-by-char (human-paced) + submit |+| POST | `/claude/remote-control` | `{"name":"..."}` | Turn on Claude Code Remote Control (submits `/remote-control` slash command) |+| POST | `/claude/highlight-open-button` | `{"duration":N}` | Glow animation on Claude button |+| POST | `/claude/clear-open-highlight` | — | Clear the open-button highlight |++## OAuth & Auth++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/claude-auth` | — | Full PKCE OAuth flow — opens browser, sets up callback |+| GET | `/oauth/callback` | — | OAuth callback handler (receives auth code) |+| POST | `/auth/proxy` | varies | Proxy auth requests to carbon.adom.inc |+| POST | `/open-url` | `{"url":"..."}` | Open URL (triggers browser picker) |+| POST | `/start-oauth-proxy` | `{"port":N}` | Start OAuth callback proxy on a port |+| GET | `/browser-profiles` | — | List browsers + profiles with avatars |+| POST | `/open-in-profile` | `{"url":"...","browser":"...","profileDir":"..."}` | Open URL in specific browser profile |+| POST | `/browser-picker/opened` | `{...}` | Frontend reports the browser picker was opened |+| GET | `/browser-picker/last-open` | — | Last browser-picker open event |++## Setup Steps & Setup Panel++### State / trigger / step control++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/setup/state` | — | Get current setup state (`never-run` \| `in-progress` \| `complete`) |+| POST | `/setup/state` | `{"state":"..."}` | Set setup state |+| POST | `/setup/step/<id>` | `{"continue_after":B}` | ⭐ Run one step by id (forces panel visible first); `continue_after:true` runs this step + every step after it |+| POST | `/setup/force-close` | — | Force-close setup panel |+| POST | `/setup/countdown` | `{"seconds":N,"message":"..."}` | Show countdown overlay |+| GET | `/setup/trigger` | — | Read trigger file |+| DELETE | `/setup/trigger` | — | Delete trigger file |+| POST | `/config-reset` | — | Reset Hydrogen config |++> ⛔ **REFUSED as deprecated** (headless runs with no visible UI):+> `POST /setup/run-all`, `POST /setup/run-step`, `POST /setup/virgin-reset`,+> `POST /wsl/unregister`. They return a refusal pointing at the UI-visible path.+> To run steps: use `POST /setup/step/<id>` or relaunch Hydrogen to auto-run the cascade.++### Setup panel control++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/setup/panel/show` | — | Force the setup panel visible |+| POST | `/setup/panel/hide` | — | Hide the setup panel |+| POST | `/setup/panel/show-virgin-reset` | — | Show panel + expand the Virgin Reset section |+| POST | `/setup/panel/virgin/show` | — | Show the Virgin Reset section only |+| POST | `/setup/panel/virgin/hide` | — | Hide the Virgin Reset section |+| POST | `/setup/panel/set-virgin-options` | `{"options":{...}}` | Set the virgin-reset checkboxes |+| POST | `/setup/panel/run-virgin-reset` | — | ⭐ The **ONLY** allowed virgin-reset trigger — UI-visible by construction (shows panel + checkboxes + progress before deleting anything) |+| POST | `/setup/panel/wipe-keep-auth` | — | Synchronous wipe that KEEPS Adom + Claude tokens |+| POST | `/setup/screenshot-mode` | `{"enabled":B}` | Toggle per-step screenshot capture (default OFF) |+| GET | `/setup/screenshot-mode` | — | Whether per-step screenshot mode is on |++## Workspace Tabs++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/workspace/tabs` | — | List all workspace tabs |+| GET | `/workspace/tabs/find?name=X` | — | Find tab by name (404 if not found) |+| POST | `/workspace/tabs` | `{tab spec}` | Add a new tab |++## System++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/system/nonce` | — | Get nonce for authenticated reboot |+| GET | `/system/reboot-state` | — | Check reboot pending state |+| POST | `/system/reboot` | `{"nonce":"..."}` | Reboot Windows (nonce-authenticated) |+| POST | `/system/cancel-reboot` | — | Cancel pending reboot |++## App lifecycle & window++| Method | Path | Body | Description |+|---|---|---|---|+| POST | `/app/restart` | — | Restart the Hydrogen app (kills + respawns the binary) |+| POST | `/app/relaunch-frontend` | — | Relaunch the Hydrogen GUI (kill+respawn) while it is responsive |+| POST | `/window/foreground` | — | Bring the Hydrogen window to the foreground (unminimize + focus) |++## Diagnostics (`/test/*`)++Window enumeration, UIA clicking, native-dialog classification, and cascade+diagnostics. **Gated behind `HD_TEST_API_ENABLED=1`** (return 404 / "test API+disabled" otherwise). `GET /test/help` self-lists the whole cluster.++| Method | Path | Body | Description |+|---|---|---|---|+| GET | `/test/help` | — | Self-lists every `/test/*` endpoint |+| GET | `/test/window-tree` | — | Enumerate all visible top-level windows |+| POST | `/test/focus` | `{"window":"..."}` | Focus Hydrogen (`self`) or a window by title substring |+| POST | `/test/click-uia` | `{...}` | Poll UIAutomation for a Button by Name; Invoke when found |+| GET | `/test/setup-steps` | — | Raw setup-steps.json + derived summary (alias: `/test/install-steps`) |+| POST | `/test/dump-state` | `{...}` | Write a diagnostic bundle (logs, install-steps, workspace state, tasklist, window-tree) |+| POST | `/test/reset-cascade` | — | Delete install-steps.json + emit setup-reset event |+| POST | `/test/screenshot-hydrogen-window` | `{...}` | Capture Hydrogen's own window to a PNG file |+| GET | `/test/probe-dialogs` | — | Enumerate visible windows, classify against the cascade-blocker catalog (UAC, WSL2 update, MSI installer, …); returns `kind` + `is_blocking` per window |++## Key Patterns++### iframe-eval context discovery+Contexts 0-14 cover all webview frames. Context numbers shift when panels open/close.+Always scan, never hardcode. The Claude Code extension webview IS reachable through+these contexts.++```+POST /iframe-eval {"js":"document.body.innerText.substring(0,100)","contextIndex":3}+```++### container-exec+Runs as user `adom` inside the workspace. Use full paths (`/home/adom/`+not `~/`). Tilde doesn't expand through the exec chain.++```+POST /container-exec {"command":"ls /home/adom/project"}+```++### Re-run a setup step after completion+The trigger file poller STOPS when all steps are done. `POST /setup/run-all` is+**REFUSED as deprecated** — don't use it. Re-run one step by id (or relaunch Hydrogen to+auto-run the cascade):+```+POST /setup/step/<id>+```++## Complete route index++<!-- BEGIN GENERATED ROUTE INDEX -->++Every route the router exposes, 318 across 22 categories, grouped the+way the API Explorer groups them. Generated from the same table `/_manifest` serves, so it+cannot drift from the code; `scripts/check-manifest-fresh.sh` fails the build if it does.++`/_manifest` remains the richer source at runtime: it carries each route's args, its+description, and a `safety` flag (`destructive` / `disruptive`) that this flat index does not.+**Read a route's safety from `/_manifest` before calling it, not from this list.**++**AI Threads** (38)++- `GET /ai-threads`+- `POST /ai-threads/activate`+- `POST /ai-threads/close`+- `GET /ai-threads/dom-debug`+- `POST /ai-threads/dom-event`+- `GET /ai-threads/dom-events`+- `POST /ai-threads/focus`+- `GET /ai-threads/history`+- `POST /ai-threads/hook-event`+- `POST /ai-threads/inject`+- `POST /ai-threads/inspect`+- `POST /ai-threads/install`+- `GET /ai-threads/install/log`+- `POST /ai-threads/issue-inject`+- `GET /ai-threads/issue-queue`+- `GET /ai-threads/names`+- `POST /ai-threads/new`+- `POST /ai-threads/new-tui`+- `POST /ai-threads/pick`+- `GET /ai-threads/prompts`+- `POST /ai-threads/rename`+- `POST /ai-threads/reorder`+- `POST /ai-threads/repo-link`+- `GET /ai-threads/repo-links`+- `POST /ai-threads/resume`+- `POST /ai-threads/run-prompt`+- `GET /ai-threads/run-prompt/status`+- `GET /ai-threads/settings`+- `POST /ai-threads/settings`+- `GET /ai-threads/tabs`+- `POST /ai-threads/tag`+- `GET /ai-threads/tags`+- `GET /ai-threads/tools`+- `POST /ai-threads/uninstall`+- `GET /ai-threads/wait`+- `POST /ai-threads/walk`+- `GET /ai-threads/walk-report`+- `GET /ai-threads/wiki-repos`++**Adom Bridge** (19)++- `GET /ab/direct-api`+- `GET /ab/discovery-file`+- `GET /ab/embedded`+- `GET /ab/health`+- `GET /ab/liveness-beacon`+- `GET /ab/permissions`+- `POST /ab/permissions/request`+- `POST /ab/permissions/revoke`+- `GET /ab/process`+- `GET /ab/relay`+- `GET /ab/settings`+- `POST /ab/settings`+- `POST /ab/show`+- `POST /ab/toast-ack`+- `POST /ab/verb`+- `GET /ab/workspace-direct`+- `POST /direct/command`+- `GET /direct/health`+- `GET /direct/status`++**Auth & User** (7)++- `GET /auth-token`+- `GET /auth/browser-status`+- `GET /auth/proxy`+- `POST /auth/proxy`+- `DELETE /user/avatar`+- `POST /user/avatar`+- `POST /user/refresh`++**Browser & Open** (11)++- `GET /browser-picker/last-open`+- `POST /browser-picker/opened`+- `DELETE /browser-picker/prefs`+- `GET /browser-picker/prefs`+- `PUT /browser-picker/prefs`+- `GET /browser-profiles`+- `POST /open-in-profile`+- `POST /open-url`+- `GET /webview-can-embed`+- `GET /webview-proxy`+- `GET /wiki-proxy`++**Claude** (49)++- `POST /claude-auth`+- `GET /claude/auth-check`+- `GET /claude/auth-page-state`+- `GET /claude/auth-status`+- `POST /claude/authorize-in-browser`+- `POST /claude/authorize-tab-enter`+- `POST /claude/clear-open-highlight`+- `POST /claude/cli-auth/code`+- `POST /claude/cli-auth/start`+- `GET /claude/contexts`+- `GET /claude/conversation-state`+- `GET /claude/conversations`+- `POST /claude/cross-eval`+- `GET /claude/detect-auth-failure`+- `POST /claude/detect-state`+- `POST /claude/ensure-open`+- `POST /claude/find-auth-button`+- `POST /claude/focus`+- `POST /claude/highlight-open-button`+- `POST /claude/inject`+- `POST /claude/inject-creds`+- `POST /claude/inject-message`+- `POST /claude/new-chat-and-send`+- `POST /claude/new-conversation`+- `POST /claude/open`+- `POST /claude/open-with-message`+- `POST /claude/remote-control`+- `POST /claude/sample-prompt`+- `GET /claude/sample-prompts`+- `POST /claude/sample-prompts/refresh`+- `POST /claude/send`+- `GET /claude/sessions`+- `POST /claude/start-auth`+- `GET /claude/status`+- `POST /claude/submit`+- `GET /claude/tabs`+- `POST /claude/tabs/activate`+- `POST /claude/tabs/close-all`+- `POST /claude/tabs/close-empty-groups`+- `POST /claude/tabs/close-untitled`+- `POST /claude/tabs/merge-groups`+- `POST /claude/trigger-auth`+- `POST /claude/type-and-submit`+- `POST /claude/uia-authorize`+- `POST /claude/uia-scan`+- `POST /claude/wipe-creds`+- `POST /config-reset`+- `GET /oauth/callback`+- `POST /start-oauth-proxy`++**Container / Docker (legacy)** (15)++- `POST /container-delete`+- `POST /container-exec`+- `POST /container-pull-image`+- `POST /container-restart`+- `POST /container-start`+- `GET /container-stats`+- `GET /container-status`+- `POST /container-stop`+- `GET /container/disk`+- `GET /container/env`+- `GET /container/logs`+- `GET /container/ports`+- `POST /container/restart`+- `POST /image-delete`+- `POST /volume-delete`++**Dock** (6)++- `GET /dock/apps`+- `POST /dock/close`+- `POST /dock/launch`+- `POST /dock/refresh`+- `POST /dock/reset`+- `POST /dock/result`++**Editor State Machine (ESM)** (4)++- `POST /editor/audit-prompt`+- `POST /editor/bars`+- `POST /editor/normalize`+- `GET /editor/state`++**Eval & Automation** (17)++- `POST /ai-cursor/click`+- `POST /ai-cursor/hide`+- `POST /ai-cursor/move`+- `POST /cdp-eval`+- `POST /click`+- `GET /devtools`+- `POST /devtools`+- `POST /eval`+- `POST /eval-in`+- `POST /files/inject`+- `POST /iframe-eval`+- `POST /mouse-click`+- `POST /query`+- `POST /reload-vscode`+- `GET /targets`+- `POST /vscode/command`+- `POST /vscode/hide-activity-items`++**Health & Build** (17)++- `GET /_manifest`+- `POST /app/relaunch-frontend`+- `POST /app/restart`+- `GET /buildinfo`+- `GET /cdp-info`+- `GET /console`+- `GET /diagnostics`+- `GET /diagnostics/all`+- `GET /doctor`+- `POST /doctor`+- `GET /health`+- `GET /logs/tail`+- `GET /ports`+- `GET /ports/all`+- `POST /ports/check`+- `POST /ports/set`+- `POST /report/problem`++**Hydrogen Browser Window** (9)++- `GET /hbw/chrome`+- `POST /hbw/chrome-toggle`+- `POST /hbw/close`+- `POST /hbw/nav`+- `POST /hbw/open`+- `GET /hbw/state`+- `GET /hbw/tip`+- `GET /hbw/tip-state`+- `POST /hbw/tooltip`++**Other** (5)++- `POST /annotate`+- `GET /favicon.ico`+- `GET /webview/frames`+- `POST /webview/link-intercepted`+- `GET /welcome.html`++**Permissions** (3)++- `POST /permission/dismiss`+- `POST /permission/request`+- `POST /permission/resolve`++**Screenshots & Recording** (9)++- `GET /capture/availability`+- `GET /capture/viewport`+- `GET /recording/native/capabilities`+- `GET /recording/native/sources`+- `POST /recording/native/start`+- `GET /recording/native/status`+- `POST /recording/native/stop`+- `POST /screenshot`+- `POST /shot`++**Settings** (2)++- `GET /settings/external-changes`+- `POST /settings/rebroadcast`++**Setup Cascade** (25)++- `GET /setup-status`+- `POST /setup/countdown`+- `GET /setup/flags`+- `POST /setup/flags`+- `POST /setup/force-close`+- `POST /setup/panel/hide`+- `POST /setup/panel/run-virgin-reset`+- `POST /setup/panel/set-virgin-options`+- `POST /setup/panel/show`+- `POST /setup/panel/show-virgin-reset`+- `POST /setup/panel/virgin/hide`+- `POST /setup/panel/virgin/show`+- `POST /setup/panel/wipe-keep-auth`+- `POST /setup/run-all`+- `POST /setup/run-step`+- `GET /setup/screenshot-mode`+- `POST /setup/screenshot-mode`+- `GET /setup/state`+- `POST /setup/state`+- `POST /setup/step`+- `DELETE /setup/trigger`+- `GET /setup/trigger`+- `POST /setup/virgin-reset`+- `POST /virgin-reset`+- `GET /virgin-reset-progress`++**System** (6)++- `GET /adom-port-hints`+- `POST /adom-port-hints`+- `POST /system/cancel-reboot`+- `GET /system/nonce`+- `POST /system/reboot`+- `GET /system/reboot-state`++**Test & Dev** (13)++- `POST /debug/show-auth-caption`+- `POST /debug/zorder-probe`+- `POST /dev/emit-event`+- `POST /test/click-uia`+- `POST /test/dump-state`+- `POST /test/focus`+- `GET /test/help`+- `GET /test/install-steps`+- `GET /test/probe-dialogs`+- `POST /test/reset-cascade`+- `POST /test/screenshot-hd-window`+- `GET /test/setup-steps`+- `GET /test/window-tree`++**UI & Windows** (14)++- `POST /api-explorer/open`+- `POST /demo/run`+- `POST /panel-webview/fullscreen`+- `GET /panes/memory`+- `POST /panes/reload-errors`+- `POST /ui/_bus-pong`+- `POST /ui/_result`+- `POST /ui/_sync`+- `GET /ui/actions`+- `POST /ui/invoke`+- `POST /ui/toast`+- `POST /ui/toast-ack`+- `POST /window/close`+- `POST /window/foreground`++**Wake & Repair** (5)++- `POST /wake/config`+- `POST /wake/preview`+- `POST /wake/preview/hide`+- `POST /wake/simulate`+- `GET /wake/status`++**Widgets** (7)++- `GET /widgets`+- `POST /widgets/close`+- `POST /widgets/menu`+- `POST /widgets/navigate`+- `POST /widgets/open`+- `POST /widgets/refresh`+- `GET /widgets/status`++**Workspace & Updates** (37)++- `POST /migration/approved`+- `POST /migration/dismissed`+- `POST /updater/check`+- `POST /updater/install`+- `POST /updater/show-banner`+- `GET /updater/status`+- `POST /workspace/apply-updates`+- `POST /workspace/check-updates`+- `POST /workspace/exec`+- `GET /workspace/health`+- `POST /workspace/migration/approve`+- `POST /workspace/migration/decline`+- `GET /workspace/stats`+- `GET /workspace/tabs`+- `POST /workspace/tabs`+- `GET /workspace/tabs/`+- `POST /workspace/tabs/`+- `GET /workspace/tabs/find`+- `POST /workspace/terminal-profile`+- `POST /workspace/terminal-profile/remove`+- `POST /workspace/vscode-settings`+- `POST /workspaces/apply`+- `GET /workspaces/benches`+- `POST /workspaces/demo`+- `POST /workspaces/menu`+- `POST /workspaces/offer`+- `POST /workspaces/refresh`+- `POST /workspaces/reset`+- `POST /workspaces/save`+- `GET /workspaces/status`+- `POST /workspaces/unload`+- `GET /wsl/gui`+- `POST /wsl/gui`+- `GET /wsl/status`+- `POST /wsl/unregister`+- `GET /wsl/wedge`+- `POST /wsl/wedge-repair`++**Workspace control** (~30 routes, a SEPARATE server on the proxy port, absent from `/_manifest`)++Reached at `/api/workspaces/editor/<owner>/<repo>/current/...`, which the handler strips+before matching. A bare `GET /tabs` on the proxy 404s; that is correct, not a fault.++- `/tabs`+- `/tabs/find`+- `/tabs/alert`+- `/active-tab`+- `/splits`+- `/swaps`+- `/moves`+- `/nav`+- `/reload`+- `/window-mode`+- `/caption`+- `/recording`+- `/screenshot`+- `/audio`+- `/sharing`+- `/notification`+- `/probe`++<!-- END GENERATED ROUTE INDEX -->
skills/hydrogen-widget/SKILL.mdadded+122@@ -0,0 +1,122 @@+---+name: hydrogen-widget+description: "Build, list, open and drive Hydrogen widgets: small live panels in Hydrogen Desktop's title bar (Claude usage, Codex usage, a clock, or your own), each served by a binary inside the workspace and mounted in a sandboxed slot. Covers the hydrogen-widget.json facet a wiki repo carries (alone, or next to its dock manifest and project workspace), the launch contract (HYDROGEN_WIDGET_ID / _PORT / _SLOT, the health URL, the optional explicit takeover), the curated list on adom/hydrogen-bootstrap, the Widgets pulldown, and the control API (GET /widgets, POST /widgets/open|close|navigate|refresh|menu, GET /widgets/status) so an AI can do all of it with no mouse. Trigger words: hydrogen widget, hydrogen-widget, title bar widget, widgets pulldown, widget slot, claude usage widget, codex usage widget, tokens left widget, usage meter in the title bar, hydrogen-widget.json, /widgets/open, make a widget, add a widget to hydrogen, widget SDK."+version: 1.0.0+---++# hydrogen-widget: small live panels in Hydrogen Desktop's title bar++A **hydrogen-widget** is a page about 30 px tall that lives in the title bar, left of the+bell. It is served by a binary that runs **inside the workspace** (the WSL2 distro on the+desktop), the same way a dock app serves its dashboard, and Hydrogen mounts it in a+sandboxed iframe slot. The user opens and closes widgets from the **Widgets** pulldown (the+grid icon in the title bar); an AI does the same through the control API. Open widgets are+remembered per user and come back on the next start.++The name has a prefix on purpose: wiki-widget is the component-page widget, and future+surfaces (kicad-widget, fusion-widget) get their own. This skill is only about Hydrogen's+title bar.++## The three that ship++| id | what it shows | source of the numbers |+|---|---|---|+| `claude-usage` | 5-hour session, weekly, and the weekly model bucket that applies, as percent used | the Claude Code sign-in in `~/.claude/.credentials.json`, asked at `api.anthropic.com/api/oauth/usage` (what `/usage` in Claude Code shows) |+| `codex-usage` | the plan's rate-limit window as percent used, plus a secondary window when the plan has one | the Codex sign-in in `~/.codex/auth.json`, asked at `chatgpt.com/backend-api/wham/usage` (what `/status` in Codex shows) |+| `clock` | the time and date | the workspace clock; the smallest possible widget, the one to copy |++`claude-usage` and `codex-usage` are a facet of the adom/adom-vscode repo (their binaries+ship with that package); `clock` is a facet of adom/hydrogen-bootstrap.++## Drive it from an AI (no mouse)++All on Hydrogen's control API (`$ADOM_HD_CONTROL_URL` inside the workspace, `ports.json` on+the host). Nothing in the pulldown needs a click.++```+GET /widgets # the list: id, name, wiki_repo, source, open, status, url+POST /widgets/refresh # re-read the curated list and the checkout+POST /widgets/open {"id":"claude-usage"} # mount, install, launch, hand the slot over+POST /widgets/close {"id":"claude-usage"} # unmount and stop it+POST /widgets/navigate {"id":"...","url":"..."} # point an open slot at a URL yourself+POST /widgets/menu {"open":true} # show or hide the pulldown+GET /widgets/status # open ids, each status (closed|installing|starting|live|failed), menu state+```++UI bus: `widgets.menu.open` and `widgets.menu.close` (POST /ui/invoke {id}).++`POST /widgets/open` returns when the widget's page answers (`ok:true, url`) or when the+install, launch or health wait failed (`ok:false, error`); the slot stays mounted with the+reason so the user can see it, and `/widgets/close` removes it. The launch log is+`~/.adom/widgets/<id>.log` in the workspace.++## Make one: the facet++Put a `hydrogen-widget.json` at the root of a wiki repo. It can sit next to a dock manifest+and a `hydrogen/workspace.json`, or be the whole repo. One widget:++```json+{+ "hydrogen_widget": "1.0.0",+ "id": "clock",+ "name": "Clock",+ "icon": "mdi:clock-outline",+ "brief": "The time and date, the smallest widget there is.",+ "width": 96,+ "install": "adom/hydrogen-bootstrap",+ "launch": { "command": "hydrogen-widget-clock", "port": 8933 },+ "health": { "url": "/", "timeout_seconds": 60 }+}+```++Several in one repo: `{"widgets": [ {...}, {...} ]}`.++| field | meaning |+|---|---|+| `id` | unique, lowercase, stable; the curated list and the control API address it |+| `width` | slot width in px, clamped to 48..320 (the strip is about 30 px tall) |+| `install` | the adom-wiki package that provides the command; defaults to the repo itself. Hydrogen runs `adom-wiki pkg install <install>` before launching, every open, so publishing a new version is enough |+| `launch.command` | run in the workspace as the workspace user, backgrounded, from the repo folder when the widget came from the checkout; `launch.stop` optional (default: pkill on the command's first word) |+| `launch.port` | the port the page listens on, 127.0.0.1 inside the workspace; Hydrogen reaches it as `http://<port>.localhost:<proxy>/` (its subdomain port proxy) |+| `health.url` | path Hydrogen polls until it answers (default `/`); `timeout_seconds` default 60 |++### The launch contract++Hydrogen sets, in the command's environment: `HYDROGEN_WIDGET_ID`, `HYDROGEN_WIDGET_PORT`,+`HYDROGEN_WIDGET_SLOT` (the slot's identity), plus the workspace env every login shell has+(`ADOM_HD_CONTROL_URL`, `ADOM_HYDROGEN_URL`). Serve one page on the port, sized for the strip+(transparent background, no scrollbars, 30 px tall content). As soon as `health.url` answers,+Hydrogen navigates the slot to your page and your page owns it: poll your own JSON, redraw,+push whatever you want.++Explicit takeover, when you want the slot before health is up or want to point it at another+page: `POST $ADOM_HD_CONTROL_URL/widgets/navigate {"id": $HYDROGEN_WIDGET_ID, "url": "..."}`.++### Listing it++- **Your checkout**: any repo under `~/project` (one clone folder down too) with a+ `hydrogen-widget.json` appears in the pulldown as "(checkout)", no publish needed. Good for+ developing one.+- **Everyone**: publish the repo (with the binary in its package) and ask the adom team to add+ `{ "id", "wiki_repo", "order" }` to `widgets/widgets.json` on adom/hydrogen-bootstrap, the+ curated list Hydrogen reads. The list only orders and names repos; every other fact comes+ from the repo's own manifest.++### A binary in fifty lines++The shipped widgets are Python 3 scripts (already in every workspace), one file each plus a+shared helper: an `http.server` that answers `GET /` with a 30 px page, `GET /data.json` with+fresh numbers (cached, refreshed every minute), and `GET /health`. Copy+`hydrogen-widget-clock` from adom/hydrogen-bootstrap `bin/` and change `data()` and the+render expression; that is the whole SDK.++## Ralph test++```+curl -s -X POST $ADOM_HD_CONTROL_URL/widgets/open -H 'content-type: application/json' -d '{"id":"clock"}'+curl -s $ADOM_HD_CONTROL_URL/widgets/status+curl -s http://8933.localhost:$(python3 -c "import json;print(json.load(open('/home/adom/.adom/ports.json'))['proxy'])")/data.json+```++Then look: a screenshot of the Hydrogen window shows the slot beside the bell, and+`/widgets/close` makes it go away.
widgets/widgets.jsonadded+14@@ -0,0 +1,14 @@+{+ "manifest_version": "1.0.0",+ "_comment": [+ "The Hydrogen title-bar widget list (hydrogen-widget, adom/hydrogen#17). World-readable; Hydrogen reads it anonymously.",+ "THIS FILE IS ONLY A LIST: which widgets appear in the Widgets pulldown, and in what order.",+ "Everything else (name, width, launch, health) resolves from each wiki_repo's own hydrogen-widget.json.",+ "A repo may ship several widgets; `id` picks one. See the hydrogen-widget skill."+ ],+ "widgets": [+ { "id": "claude-usage", "wiki_repo": "adom/adom-vscode", "order": 10 },+ { "id": "codex-usage", "wiki_repo": "adom/adom-vscode", "order": 20 },+ { "id": "clock", "wiki_repo": "adom/hydrogen-bootstrap", "order": 90 }+ ]+}