Skip to content

feat(ui-tui): widget-app SDK — apps as state+reducer+render, with three reference apps - #68306

Merged
OutThisLife merged 8 commits into
mainfrom
bb/tui-widget-sdk
Jul 22, 2026
Merged

feat(ui-tui): widget-app SDK — apps as state+reducer+render, with three reference apps#68306
OutThisLife merged 8 commits into
mainfrom
bb/tui-widget-sdk

Conversation

@OutThisLife

@OutThisLife OutThisLife commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #20379. Turns the layout/theme engine into an authorable surface — the TUI counterpart of the desktop component model. A widget app is state + init(arg) + reduce(input) + render(ctx), registered by id, launched from a slash command. Three reference apps ship with it: the ported /grid-test and /dialog-test demos, plus a new /weather app that proves the async-data contract.

The SDK (ui-tui/src/sdk/)

  • types.ts — the WidgetApp<S> contract: id + help (surfaced in / completions), mode (modal owns input and blocks the composer; ambient captures nothing, never blocks, toggles closed on relaunch) + zone placement — dock rows (dock-top/dock-bottom, right-aligned card rows in the chrome) and side RAILS (top-right/bottom-right/top-left/bottom-left: reserved columns beside the transcript that text REFLOWS around; apps declare width, same-corner cards stack, crash-isolated by a per-widget error boundary), init (slash tail → state; null ⇒ launcher prints usage), reduce (next state / same reference to swallow / null to close), render (composes SDK primitives at viewport level).
  • registry.tsthe registry IS the catalog: defineWidgetApp registers at module load (last-writer-wins so user apps can shadow built-ins); slash commands are GENERATED from listWidgetApps() and / completions merge each app's help client-side — adding an app is one defineWidgetApp call, no command/completion lists to touch on either side.
  • host.tsx — the ONLY core coupling, five functions: launchWidget(id, arg) (slash), openWidget(app, state) (typed programmatic launch — apps stack each other), updateWidget(app, fn) (async delivery: patches state only while the app is still active, so a late fetch can never resurrect a closed app), dispatchWidgetInput (input pipeline), ActiveWidgetSlot (modal render slot) + AmbientDock (right-aligned flow dock above the bottom status rule — never floats over the transcript). The active app lives in the overlay store's generic widget slot and blocks the composer like any overlay.
  • index.ts — the one import an author needs: app contract + host, layout engine (resolveGridTracks/layoutWidgetGrid/layoutGridAreas + WidgetGrid/GridAreas), Overlay/Dialog, chrome primitives (chipRowProps, listRowStyle, scrollbarColors, useMenu, MenuRow, OverlayHint, …), color math (mix, liftForContrast, …).

Reference apps (sdk/apps/)

App Demonstrates
grid-test stateful reducer (~140 lines of keybindings moved out of useInputHandlers), app stacking via openWidget (d opens the dialog app)
dialog-test zoned Overlay + Dialog + backdrop, minimal close-only reducer
weather async data + ambient mode: init fires the fetch, results land via updateWidget; docks as a glanceable card (composer stays live, /weather toggles it away); table-driven WWO-code → ASCII-art buckets; every tint is a theme family tone (sun = primary, rain = shell blue, thunder = warn)
ticker animated ambient: fake 1-pip chart, random-walk price + live block sparkline at 250ms (streams-demo style — the component owns the animation, app state is just the symbol), direction-colored via theme ok/error

Core shrinkage

interfaces.ts drops DialogState/GridTestState; overlayStore's two demo slices collapse into widget; appOverlays/appLayout lose bespoke demo branches; the slash commands are three-line launchers through one widgetCommand helper.

Self-authoring (Hermes makes its own widgets)

The Python-plugin contract, mirrored: drop <name>.mjs into $HERMES_HOME/tui-widgets/ default-exporting register(sdk) — the SDK object is passed IN (user files have no import path to the bundle: defineWidgetApp, updateWidget, React, h, Box/Text/Dialog/Overlay/WidgetGrid/GridAreas). Loaded at boot + /widgets-reload (cache-busted, last-writer-wins, errors log-and-skip). A registry-first dispatch fallback means late-registered widgets get their slash command with zero static wiring. The tui-widgets skill (skills/productivity/tui-widgets/) teaches the agent the contract, with a working /clock template.

Reviewer guide

Read sdk/types.ts (contract) → sdk/host.tsx (five integration points) → sdk/apps/weather.tsx (canonical data-backed consumer). The core diff is almost entirely deletions.

Demo commands

Command Shows
/grid-test (4x3, 6 4, streams) arrows/hjkl move, a areas, s streams, g/p gap/padding, n nest, + - ] [ resize, z/Enter zoom, d stacks the dialog app, r reset, Esc/q close
/dialog-test [zone] zoned viewport overlay with backdrop scrim
/weather [location] docked ambient card: live wttr.in conditions + themed ASCII art, composer stays usable; /weather again dismisses (blank location = IP geolocation)
/ticker [symbol] docked animated sparkline ticker; stack it next to /weather to see the multi-widget dock
/widgets-reload rescan $HERMES_HOME/tui-widgets/ — try it with the skill's clock.mjs template, then /clock Asia/Tokyo

Test plan

  • 6 host-contract tests: lifecycle, refusal→usage, unknown id, app stacking, typed openWidget, $isBlocked
  • Loader e2e: temp-dir .mjs → load → registered + launchable, broken files reported not fatal
  • 4 async-contract tests: loading→ready via updateWidget, late-resolution guard (toggle-close mid-flight), error phase, keymap (fetch mocked — no live network)
  • Slash + grid component tests updated to the widget slot
  • 1253 vitest, typecheck, lint, build green

Follow-up (not this PR)

External authorship: data-driven widget manifests over the gateway for Python plugins, and/or publishing @hermes/ink + the SDK as a package. The SDK stays internal until the contract has soaked.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels Jul 21, 2026
@OutThisLife OutThisLife changed the title feat(ui-tui): widget-app SDK — apps as state+reducer+render, demos ported feat(ui-tui): widget-app SDK — apps as state+reducer+render, with three reference apps Jul 21, 2026
@OutThisLife
OutThisLife force-pushed the bb/widget-grid-slots branch from cbd9142 to 0ada783 Compare July 21, 2026 21:43
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

running on 2ed61d4

CI timings

CI timings · View job

Wall time 7m48s vs 10m10s (-23.3%). 8 job(s) slower, 10 faster, 3 unchanged.

  • Build&Test Docker image / build (arm64, ubuntu-24.04-arm, linux/arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope...: -120.0s
  • Build&Test Docker image / build (amd64, ubuntu-latest, linux/amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=do...: -108.0s
  • Docs Site / docs-site-checks: -78.0s
  • Python tests / Run tests slice 5/8: -76.0s
  • Python tests / Run tests slice 4/8: -33.0s

Base automatically changed from bb/widget-grid-slots to main July 21, 2026 22:52
… apps

The SDK the desktop app already has, ported to the TUI: a WidgetApp contract
(id/help/mode/init/reduce/render/usage), a registry, and a host that owns the
active widget, routes input to its reducer, and renders it. The grid-test and
dialog-test debug surfaces are reimplemented as widget apps instead of bespoke
overlay state, and slash commands are generated from the registry. Input for an
open widget is owned by the active app (supersedes the demo-only stacked-modal
routing) — the single active widget enforces topmost-owns-input structurally.
… ASCII art

/weather [location]: wttr.in current conditions behind a Dialog, art bucket
table-driven off WWO weather codes, every tint a theme family tone (sun =
primary, rain = shell blue, thunder = warn). Proves the async story the
demos don't: init returns a loading phase and fires the fetch; results land
through the new host.updateWidget, which patches state ONLY while the app
is still active — a late resolution can never resurrect a closed app or
clobber a different one. `r` refetches; Esc/q/Enter close.

Four async-contract tests (loading→ready via updateWidget, late-resolution
guard, error phase, keymap). 1253 TS tests green.
…n-flow dock

Widgets can render as ambient (glanceable, non-blocking) instead of modal,
docked in the normal layout flow above/below the status bar rather than taking
over the screen. The slash catalog is generated from the widget registry so new
apps surface automatically, and /ticker lands as the first live-animation
ambient demo.
…kill

Hermes can write its own widgets: a loader discovers $HERMES_HOME/tui-widgets/*.mjs,
fs.watch hot-loads them the moment they land (no restart), and a tui-widgets skill
teaches the agent the contract and the openWidget-at-register auto-open recipe.
Load/error/remove events announce themselves in the transcript; a lazy intro
skeleton covers the first paint.
…streams

Reusable render primitives the SDK exposes to widget authors: sparkline/gauge/
hbars chart helpers (dimension-stable so live updates never resize the card),
an Accordion for expand/collapse sections, animated shimmer loaders, and a
streams demo that no longer reserves a phantom icon column on unfocused titles.
A full placement grid so the agent can put a widget where it asks — dock-top/
bottom and corner zones, with corners as reserved rails that take real space
instead of floating over content. A per-widget error boundary plus lenient
ShimmerRows means generated widget code can't crash the TUI.
host.tsx collapses to one placement router over a shared render context, and the
grid-test app drops its width floor too (carrying the #20379 review rule). Final
formatting pass folded in.
@OutThisLife
OutThisLife merged commit 7dc535a into main Jul 22, 2026
79 of 81 checks passed
@OutThisLife
OutThisLife deleted the bb/tui-widget-sdk branch July 22, 2026 02:17
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…-sdk

feat(ui-tui): widget-app SDK — apps as state+reducer+render, with three reference apps
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
…-sdk

feat(ui-tui): widget-app SDK — apps as state+reducer+render, with three reference apps
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…-sdk

feat(ui-tui): widget-app SDK — apps as state+reducer+render, with three reference apps
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants