Adom Hydrogen (Windows / WSL2)
Public Made by Adomby adom
The Windows (WSL2) platform layer for Hydrogen.
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
- The slot mounts with a compact placeholder (a ring and a status line, sized by the slot).
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.- Any previous instance is stopped first (
launch.stop, else apkillon 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. launch.commandruns 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, withHYDROGEN_WIDGET_ID,HYDROGEN_WIDGET_PORT,HYDROGEN_WIDGET_SLOTandHYDROGEN_PROXY_PORTin its environment plus the workspace env every login shell has (ADOM_HD_CONTROL_URL,ADOM_HYDROGEN_URL).- Hydrogen asks for
http://127.0.0.1:<port>/<health.url>from inside the workspace until it answers 2xx or 3xx, up tohealth.timeout_seconds, then navigates the slot tohttp://<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. - 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
resizeif 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
widththe compact title-bar width; never send the expanded width aswidth; - 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: nullto withdraw, and omit it to keep the plain card (yourbriefas text); - the request is dropped whenever the slot frame loads, so post it again on your
loadevent; - only the slot frame's
widthandexpandcount; 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 ahydrogen-widget.jsonappears 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" }towidgets/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 sharefor 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 ownwidgets.jsonif 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.
---
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:
```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.