Skip to content
Back to skills

Tui Widgets

ASecurity

Author live widget apps for the Hermes TUI dock.

  • 36 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 10, 2026
ai-agentsgoreactrailsapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 10, 2026

npx -y skills add theheavenlyd3mon/hermes-profiles --skill tui-widgets --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Tui Widgets?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Tui Widgets
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/theheavenlyd3mon-tui-widgets/badge)](https://www.skillsdirectory.com/skills/theheavenlyd3mon-tui-widgets)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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
---

# TUI Widgets Skill

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.

## When to Use

- The user asks for a live panel in the TUI (ticker, clock, countdown,
  status card, API-backed readout).
- The user wants a custom modal tool (picker, calculator, viewer) bound to
  a slash command.

## Prerequisites

- The TUI must be in use (`hermes --tui`). Widgets do not render in the
  classic CLI or messaging platforms.
- Network-backed widgets need whatever credentials their API needs; fetch
  failures must land as an error phase, never a crash.

## How to Run

1. Use `write_file` to create `~/.hermes/tui-widgets/<name>.mjs` (see
   `templates/clock.mjs` for a complete working widget).
2. If the TUI is running it hot-loads the file within ~a second (the
   widgets directory is watched); `/widgets-reload` forces a rescan.
3. The widget's id becomes its slash command automatically (`/<id>`), with
   its `help` in the `/` completion popover. No other registration exists.
4. Auto-open (no command needed): end `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`.

## Quick Reference

A widget file default-exports `register(sdk)`:

```js
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):

- Give `Dialog` an explicit `width`; charts already return exactly the
  `width` you ask for (short series pad-left while history warms up).
- Pad dynamic numbers: `String(v).padStart(6)` — `51 ms` → `112 ms` must
  not change the line length.
- Keep row counts constant per phase; swap content, not structure.

Charts (pure string builders — color the result with theme tones):

- `sdk.sparkline(series, width?)` → `▂▃▅▇█▆` one-row trend
- `sdk.sparkRows(series, width, rows)` → multi-row column chart (top line
  first) — the mission-control panel look; taller cells gain resolution
- `sdk.gauge(ratio, width)` → `█████░░░` fill bar for a 0..1 value
- `sdk.hbars(values, width)` → horizontal bar chart, one bar per value,
  eighth-block tips, scaled to the max

Keep 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):
  - Docks (chrome rows): `dock-top` (under the top status bar),
    `dock-bottom` (default — above the bottom one).
  - Rails (side columns beside the transcript; text reflows around them):
    `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.
- Async data: fire the fetch from `init`, land results with
  `sdk.updateWidget(app, fn)` — it no-ops if the widget was closed, so a
  late reply can never resurrect it.
- Animation: own a timer inside a component via `React.useState` +
  `React.useEffect` (see the template); keep intervals ≥ 250ms.
- Colors: ALWAYS theme tones (`t.color.primary/label/muted/ok/error/…`),
  never hardcoded hexes — widgets must survive `/skin` and light/dark.

## Procedure

1. Pick `id`, `mode`, and the state shape; keep state serializable.
2. Write the file from the template; wire data via `init` + `updateWidget`.
3. `/<id>` to launch (hot-loaded on write); relaunch `/<id>` to dismiss an
   ambient widget.
4. Iterate: edit the file — it hot-reloads on save (last-writer-wins, the
   fresh definition shadows the old one). Relaunch `/<id>` to remount.

## Pitfalls

- No JSX and no bare imports in `.mjs` — everything comes from the `sdk`
  parameter; `h(...)` builds elements.
- Don't ship a modal without a close path (`Esc`/`q` returning `null`).
- Ambient widgets must stay small (≤ ~6 rows) — the dock sits between the
  transcript and the status bar.
- A thrown `register()` is logged and skipped; check
  `~/.hermes/logs/tui_gateway_crash.log` if a widget never appears.

## Verification

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.

Files in this skill

  • SKILL.md6.5 KB
  • templates/clock.mjs1.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.