master
Colby Knox Publish 0.3.30 ed6a0ea

name: hydrogen-widget description: >- Build, list, open and drive Hydrogen Widgets, small live panels in the Title Bar (Claude usage, Codex usage, your own): the hydrogen-widget.json facet, launch contract, and /widgets API. Use when asked to make a widget, add a title bar widget, or about the usage meter widget. version: 1.5.7

hydrogen-widget: small live panels in Hydrogen'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:

{
  "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.