with one click
tui-widgets
Author live widget apps for the Hermes TUI dock.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Author live widget apps for the Hermes TUI dock.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Author a Hermes color theme that skins every surface.
Build auditable Excel workbooks headless with openpyxl — blue/black/green cell conventions, formulas over hardcodes, named ranges, balance checks, sensitivity tables. Use for financial models, audit outputs, reconciliations.
Create, read, edit Word .docx documents and templates.
Edit PDF text/typos/titles via nano-pdf CLI (NL prompts).
Extract text from PDFs/scans (pymupdf, marker-pdf).
Create, merge, split, fill, and secure PDF files.
| name | tui-widgets |
| description | Author live widget apps for the Hermes TUI dock. |
| version | 1.0.0 |
| author | Hermes Agent |
| license | MIT |
| metadata | {"hermes":{"tags":["tui","widgets","sdk","ui"],"category":"productivity"}} |
Author widget apps for the Hermes TUI (hermes --tui): glanceable ambient
panels docked above the status bar, or modal overlays that own the keyboard.
Widgets are plain ESM files the TUI loads at startup — no build step, no
repo changes. This skill does not cover desktop-app or web-dashboard
widgets.
hermes --tui). Widgets do not render in the
classic CLI or messaging platforms.write_file to create ~/.hermes/tui-widgets/<name>.mjs (see
templates/clock.mjs for a complete working widget)./widgets-reload forces a rescan./<id>), with
its help in the / completion popover. No other registration exists.register(sdk) with
sdk.openWidget(app, app.init('')) — the widget docks itself the moment
the file loads. Only do this when the user asked for it; note it re-docks
on every /widgets-reload.A widget file default-exports register(sdk):
export default function register(sdk) {
const { Box, Text, defineWidgetApp, h } = sdk
defineWidgetApp({
id: 'clock', // slash command name
help: 'live clock in the dock', // `/` completion metadata
mode: 'ambient', // 'ambient' docks; 'modal' takes input
init: arg => ({ label: arg.trim() || 'UTC' }), // null = print usage
reduce: (state, { ch, key }) => (key.escape || ch === 'q' ? null : state),
render: ({ state, t }) => h(sdk.Dialog, { width: 24 }, h(Text, { color: t.color.label }, state.label))
})
}
sdk contents: defineWidgetApp, openWidget, updateWidget, isCtrl,
React, h (createElement — no JSX in .mjs), components Box, Text,
Dialog, Overlay, WidgetGrid, GridAreas, and loaders Shimmer,
ShimmerRows, useShimmerPhase — use ShimmerRows for loading phases
instead of a bare "loading…" line.
Expand/collapse: sdk.Accordion — the same primitive the session panel's
tool/skill sections use. h(Accordion, { t, title: 'details', count: 3, defaultOpen: false }, body) toggles on CLICK (works in ambient widgets,
which receive no keys); modal apps may pass open + onToggle to drive it
from reducer state instead.
Stable sizing (cards must NEVER resize while ticking):
Dialog an explicit width; charts already return exactly the
width you ask for (short series pad-left while history warms up).String(v).padStart(6) — 51 ms → 112 ms must
not change the line length.Charts (pure string builders — color the result with theme tones):
sdk.sparkline(series, width?) → ▂▃▅▇█▆ one-row trendsdk.sparkRows(series, width, rows) → multi-row column chart (top line
first) — the mission-control panel look; taller cells gain resolutionsdk.gauge(ratio, width) → █████░░░ fill bar for a 0..1 valuesdk.hbars(values, width) → horizontal bar chart, one bar per value,
eighth-block tips, scaled to the maxKeep a rolling series in component state (push per tick, cap ~120 samples)
and render sparkRows for dashboard panels, sparkline for one-liners.
Contract essentials:
mode: 'ambient' — captures no input, the command toggles it; render
returns a CARD (usually Dialog), never Overlay. Placement via zone — every zone RESERVES real space (nothing ever
paints over the transcript):
dock-top (under the top status bar),
dock-bottom (default — above the bottom one).top-left, top-right, bottom-left, bottom-right — corner names
pick the rail side and its top/bottom anchor. Set width on the app
to the card's width (match your Dialog width; default 44) — the rail
reserves exactly that many columns.
Map the user's words to the nearest zone: "top right" → top-right,
"above/next to the status bar" → a dock. Rails suit narrow cards
(~30-46 cols); full-width or short-and-wide content belongs in a dock.mode: 'modal' (default) — owns every keypress; reduce returns next
state, the same reference to swallow a key, or null to close; render
wraps content in Overlay for placement.init, land results with
sdk.updateWidget(app, fn) — it no-ops if the widget was closed, so a
late reply can never resurrect it.React.useState +
React.useEffect (see the template); keep intervals ≥ 250ms.t.color.primary/label/muted/ok/error/…),
never hardcoded hexes — widgets must survive /skin and light/dark.id, mode, and the state shape; keep state serializable.init + updateWidget./<id> to launch (hot-loaded on write); relaunch /<id> to dismiss an
ambient widget./<id> to remount..mjs — everything comes from the sdk
parameter; h(...) builds elements.Esc/q returning null).register() is logged and skipped; check
~/.hermes/logs/tui_gateway_crash.log if a widget never appears.Run /widgets-reload — the transcript line must list the file under
loaded:. Then /<id>: an ambient widget appears docked right, above the
status bar, while the composer keeps accepting input; /<id> again removes
it.