bootstrap
Adom Hydrogen (platform base)
Public Made by Adomby adom
Layer Hydrogen's workspace onto a standard Adom workstation.
← Commit history
skills/hydrogen-widget/SKILL.mddeleted−265@@ -1,265 +0,0 @@-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 per-widget page the pulldown links to, the launch contract (HYDROGEN_WIDGET_ID / _PORT / _SLOT / HYDROGEN_PROXY_PORT, the health URL, the explicit takeover, stop-first relaunch), the tall and compact layouts, vendor marks, private repos (read with the user's session), the curated list on adom/hydrogen-bootstrap, the Hydrogen 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, private widget, widget SDK."-version: 1.5.7--# hydrogen-widget: small live panels in Hydrogen Desktop's Title Bar--The bar across the top of the Hydrogen window is the **Title Bar**: the Adom menu, the-widgets, the CPU/RAM/GPU meter, the bell, your avatar and the window controls. Its short form-(the chevron at the top edge) is the **compact Title Bar**. A **Hydrogen Widget** is a page-that lives in a slot of that bar, left of the meter, served by a binary that runs **inside the-workspace** (the WSL2 distro on the desktop), the way a dock app serves its dashboard. Hydrogen-mounts it in a sandboxed iframe. The user opens and closes widgets from the **Hydrogen-Widgets** pulldown (the puzzle-piece icon); 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 the Title Bar.--## The three that ship--| id | what it shows | source of the numbers | repo |-|---|---|---|---|-| `claude-usage` | 5-hour session, weekly, and every weekly limit scoped to a model on your plan (Fable, Opus, ...), as percent used, in the provider's severity colour | the Claude Code sign-in in `~/.claude/.credentials.json`, asked at `api.anthropic.com/api/oauth/usage`; the `limits` list in that answer is what Claude Code's `/usage` panel draws | a facet of adom/adom-vscode |-| `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` shows) | a facet of adom/adom-vscode |-| `clock` | the time and date, stacked when tall, inline when compact; the one to copy | the workspace clock | a facet of adom/hydrogen-bootstrap |--Neither usage call runs a model, so neither costs quota. They ask at most once every three-minutes per provider, pause while the window is hidden, back off on errors (doubling to 15-minutes), and re-read the sign-in file only when it changes.--## 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, stop any old instance, 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.--## What happens on open, exactly--1. The slot mounts with a compact placeholder (a ring and a status line, sized by the slot).-2. `adom-wiki pkg install <install>` runs in the workspace, every open, so publishing a new- version of your package is enough to update the widget.- When Hydrogen restores widgets at start it installs only what is missing (the launch- command's binary is looked up first) and runs installs one at a time; an open from the- pulldown or the API always installs, so a new package version lands then.-3. Any previous instance is stopped first (`launch.stop`, else a `pkill` on the command's first- word), so a new version actually gets the port. The slot's frame is remounted on the- placeholder for the same reason: a stale page never stays put.-4. `launch.command` runs in its own session (`setsid -f nohup ...`, so an editor reload or an- extension host restart cannot take it down) as the workspace user, from the repo folder when the widget- came from the checkout, with `HYDROGEN_WIDGET_ID`, `HYDROGEN_WIDGET_PORT`,- `HYDROGEN_WIDGET_SLOT` and `HYDROGEN_PROXY_PORT` in its environment plus the workspace env- every login shell has (`ADOM_HD_CONTROL_URL`, `ADOM_HYDROGEN_URL`).-5. Hydrogen asks for `http://127.0.0.1:<port>/<health.url>` from inside the workspace until it- answers 2xx or 3xx, up to `health.timeout_seconds`, then navigates the slot to- `http://<port>.localhost:<proxy>/` (its subdomain port proxy). The probe runs in the- workspace on purpose: the proxy's own error page while the distro boots is not an answer.- Before any of this, if the workspace is not running yet (a full quit and relaunch boots the- distro for a minute or more), the slot says "Waiting for the workspace" and waits, up to- ten minutes; nothing is installed or launched until it is up.-6. Your page owns the slot from then on: poll your own JSON, redraw, push whatever you want.- Explicit takeover, when you want the slot before health is up or want another page:- `POST $ADOM_HD_CONTROL_URL/widgets/navigate {"id": $HYDROGEN_WIDGET_ID, "url": "..."}`.--## 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": 110,- "width_tall": 84,- "install": "adom/hydrogen-bootstrap",- "launch": { "command": "hydrogen-widget-clock", "port": 8933 },- "health": { "url": "/", "timeout_seconds": 60 },- "docs": "widgets/README.md"-}-```--Several in one repo: `{"widgets": [ {...}, {...} ]}`. Each entry has its own `id`, name, mark,-launch command and port; they share the repo's package (`install`) and page (`docs`). The curated-list names a Hydrogen Widget by repo plus id, so each is its own row in the pulldown and opens and-closes on its own. adom/adom-vscode ships Claude usage and Codex usage this way because both read-an AI sign-in that lives next to the editor extension; a repo with one thing to show ships one.--| field | meaning |-|---|---|-| `id` | unique, lowercase, stable; the curated list and the control API address it |-| `name`, `brief` | the pulldown row and its hover text; `brief` is also the body of the hover card when the widget shows no live view there, so write it for a person |-| `icon` | an iconify name for the row (`mdi:clock-outline`), or `vendor:claude` / `vendor:codex` for the vendor marks the AI launch bar paints, so a person can tell whose numbers a widget shows |-| `launch.port` | the port your page serves on inside the workspace. Pick one far from where apps land: not the 8000s (dashboards, dev servers, the KiCad dashboard's 8931), not the ephemeral range (32768 and up). The shipped widgets sit at 27011 to 27013; take 27020 and up for yours. A widget is a side panel and must never cost an app its port (John 2026-09-15). |-| `width` | slot width in px in the compact Title Bar (one horizontal row); clamped to 48..320 |-| `width_tall` | slot width in the full Title Bar, where your page stacks its rows; defaults to 60 percent of `width` |-| `install` | the adom-wiki package that provides the command; defaults to the repo itself |-| `launch.command` | run in the workspace as the workspace user, backgrounded; `launch.stop` optional |-| `launch.port` | the port the page listens on, 127.0.0.1 inside the workspace |-| `health.url` | path Hydrogen polls until it answers (default `/`); `timeout_seconds` default 60 |-| `about` | one or two sentences on how it works, shown under Details on the hover card ("Reads the Claude Code sign-in ... asks the usage endpoint ... no quota is spent") |-| `refresh` | how often it asks anything, shown under Details on the hover card ("the provider at most once every 3 minutes; pauses while hidden; backs off on errors") |-| `docs` | the widget's own page: a repo file path (`widgets/README.md`) or a full URL. The pulldown row links to it. Defaults to `<repo>/files/widgets/README.md` |--### The widget's own page (what the pulldown row opens)--Every widget should ship a page a person can read before opening it. The convention is-`widgets/README.md` in the repo (the default `docs`), with: a picture of the widget in the full-and the compact Title Bar, what it shows and where the numbers come from, how often it asks-anything, the list of files in the repo that make it a widget, and a link back to this skill. The-two shipped examples: adom/adom-vscode `widgets/README.md` and adom/hydrogen-bootstrap-`widgets/README.md`.--### The page in the slot: two layouts, chosen by height--The slot is 38 px tall in the full Title Bar (like the meter) and 20 px in the compact one.-Size your page by its height, never in fixed px: `@media (min-height: 34px)` stack rows-(label, bar, value) to use the room; `@media (max-height: 33px)` one horizontal row with-`clamp()` font sizes. Transparent background, no scrollbars. The shipped helper does this;-copy it.--### The hover card (a tooltip that lives outside the iframe)--A tooltip inside your page cannot reach past the slot's edge, so Hydrogen draws the card:-hover the slot and, after a moment, a card appears under it in the host, in the style of the-tab cards. It is for the person looking at the widget, so it leads with what the widget shows:-the name and mark, then the widget's own live view when it asks for one (see "A bigger view on-hover" below), otherwise its `brief`. A status line under the name appears only while the-widget is not live (Installing, Starting, Waiting for the workspace, Could not start: reason).--Everything about how it runs sits under **Details** at the foot of the card, folded by default:-`brief` (when a live view took its place), `about`, `refresh`, the port and the log path, the-page URL, the repo, and four links: the widget's page (`docs`), the wiki repo, this SDK, and-Close widget. Opening Details pins the card, so those links are live as soon as they show.-Moving onto the card keeps it; a click on the card (or its pin) pins it; Escape or a click-outside closes a pinned card. Your page needs nothing for this beyond the manifest fields, so-write `brief` for a person ("how much of your limits you have used; hover for when they-reset"), and put the mechanics in `about` and `refresh`. An AI shows the same card with-`POST /widgets/card {"id": "...", "pinned": true}` and hides it with `{"id": "...", "open": false}`.--### Resizing: who tells the page--Nobody has to. The slot is an iframe; when the Title Bar goes compact or tall, Hydrogen resizes-the slot and the iframe's own `window` gets a normal `resize` event, and any height media-queries re-evaluate by themselves. Hydrogen posts no message on resize. So a page must:--- size itself by its own `innerHeight`/`innerWidth` (or the height media queries above), never- by fixed pixel widths;-- give every column a natural width and let one flexible column absorb the rest- (`grid-template-columns: auto minmax(14px, 1fr) auto`), so a value is never clipped when the- slot is narrow; trim long labels with an ellipsis instead;-- re-render on `resize` if its render depends on the size (the shipped helper keeps the last- data and re-renders after 80 ms of quiet, no refetch).--The page decides its own width per layout: after every render and resize, measure your content-and post `window.parent.postMessage({hdWidget: {width: <px>}}, '*')`; Hydrogen sizes the slot to-it (clamped 48..320) and knows which slot from the frame that sent it. The manifest's `width`-(compact) and `width_tall` (full) are only the first guess before your page has measured. So a-tall layout can be narrow (rows stacked) and the compact one wide (one row), which is what the-shipped helper does. Measure on a hidden copy of your content laid out at `width: max-content`-(clone the root, remove ids, `position:absolute; left:-10000px; height:100vh; width:max-content;-visibility:hidden`, read its width, remove it): the live boxes stretch to the slot, so measuring-them only ever grows the slot (the clock once asked for 361 px that way). The helper's-`postWidth()` is that code; keep it when you copy the helper. For pictures of your widget the-helper serves `/preview?h=38` and `/preview?h=20`: its page in a frame of that height that follows-the width the page asks for, so a browser screenshot of `#w` at a high device scale is a crisp-docs image (`?w=` pins the frame instead). The shipped widgets are-the reference: adom/adom-vscode `widgets/bin/hydrogen-widget-lib.py` is the page and layout,-`hydrogen-widget-claude-usage` and `hydrogen-widget-codex-usage` the data feeds, and-adom/hydrogen-bootstrap `bin/hydrogen-widget-clock` the smallest one.--### Who draws what: your two canvases--Your widget has two canvases and draws everything inside them: the **slot** in the Title Bar and,-if you ask for one, the **card** view on hover. Hydrogen draws only what surrounds them: the slot's-edge, its height (38 or 20 px) and its close button, and the card's frame, header (your `name` and-`icon`), pin and Details (your `about`, `refresh`, `docs`, plus the port and log). Anything inside a-canvas changes when you publish your package, with no Hydrogen release. The table and the diagram:-the Hydrogen Widgets page, `widgets/README.md` on adom/hydrogen, section "Who draws what".--### A bigger view on hover (optional; Hydrogen 1.0.609+ gives the card its own frame)--Ask for a live view in the hover card by adding `expand` to the same message:-`window.parent.postMessage({hdWidget: {width: 265, expand: {width: 560, height: 400}}}, '*')`.-When the card opens, Hydrogen loads a **second copy of your page** in the card at that size (width-clamped 280..720, height 120..600, at most 45% of the window height), or the page at `expand.path`-(`{width, height, path: "/card"}`, a path on your own origin) if you want a separate one. Your slot-keeps showing your page the whole time. Rules:--- keep `width` the compact title-bar width; never send the expanded width as `width`;-- switch layouts on height (`@media (min-height: 100px)` shows the big view), not on a message:- the card copy is simply a taller frame of the same page;-- the card copy is a fresh page each time the card opens: keep state you need on your server;-- send `expand: null` to withdraw, and omit it to keep the plain card (your `brief` as text);-- the request is dropped whenever the slot frame loads, so post it again on your `load` event;-- only the slot frame's `width` and `expand` count; the card frame may only send- `{hdWidget: {collapse: true}}` (forward Escape with it, since keys do not leave an iframe).--Before 1.0.609 Hydrogen moved the slot's own frame into the card, leaving the slot empty with a-"Expanded below" stand-in; pages written for that work unchanged, because they already pick the big-layout by height. Adom AI Flow and the usage widgets are the references.-Details: `docs/hydrogen-widget-expansion.md` in the Hydrogen source.--### 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.-- **Everyone**: publish the repo (with the binary in its package), then file an issue on the- adom/hydrogen-bootstrap page titled `List widget: <owner>/<repo>` with a one-line description- and a picture of the slot (its `/preview?h=38`). That page's maintainers add- `{ "id", "wiki_repo", "order" }` to `widgets/widgets.json`, the curated list Hydrogen reads,- and the widget appears in every user's pulldown at their next refresh. From a workspace:- `adom-wiki issue create adom/hydrogen-bootstrap --title "List widget: <owner>/<repo>" --body "..."`.- The list only orders and names repos; every other fact comes from the repo's own manifest,- which the curated list Hydrogen reads. The list only orders and names repos; every other fact comes- from the repo's own manifest.-- **Just you, or your team (private)**: keep the repo private on the wiki (`adom-wiki page- create --private`, `adom-wiki page share` for teammates). Hydrogen reads the curated list and- every manifest **with the signed-in user's session**, so a private repo's widget lists and- installs for the people the page is shared with and nobody else. Private widgets are not in- the curated list; they come from your checkout, or from your own `widgets.json` if you fork- the bootstrap for your org.--### 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 the page, `GET /data.json` with fresh-numbers (cached per interval, refreshed only when asked after the interval, exponential backoff-on errors), and `GET /health`. Copy `hydrogen-widget-clock` and `hydrogen-widget-lib.py` from-adom/hydrogen-bootstrap `bin/`, change `data()` and the render expression, add a-`hydrogen-widget.json`, a `widgets/README.md` and an `install.sh` line that symlinks your script-into `~/.local/bin`. 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://27013.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 left of the meter, the chevron-makes the Title Bar compact and the slot follows, and `/widgets/close` makes it go away.--