feat(ui-tui): widget-app SDK — apps as state+reducer+render, with three reference apps - #68306
Merged
Conversation
OutThisLife
force-pushed
the
bb/tui-widget-sdk
branch
from
July 21, 2026 18:37
31b7c94 to
c8a1687
Compare
5 tasks
OutThisLife
force-pushed
the
bb/widget-grid-slots
branch
from
July 21, 2026 21:43
cbd9142 to
0ada783
Compare
OutThisLife
force-pushed
the
bb/tui-widget-sdk
branch
from
July 21, 2026 22:34
c8a1687 to
222d0c9
Compare
Contributor
૮ >ﻌ< ა ci reviewrunning on 2ed61d4 CI timingsCI timings · View jobWall time 7m48s vs 10m10s (-23.3%). 8 job(s) slower, 10 faster, 3 unchanged.
|
OutThisLife
force-pushed
the
bb/tui-widget-sdk
branch
from
July 22, 2026 01:03
222d0c9 to
e5b891d
Compare
… 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
force-pushed
the
bb/tui-widget-sdk
branch
from
July 22, 2026 02:00
e5b891d to
2ed61d4
Compare
austinpickett
approved these changes
Jul 22, 2026
1 task
1 task
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-testand/dialog-testdemos, plus a new/weatherapp that proves the async-data contract.The SDK (
ui-tui/src/sdk/)types.ts— theWidgetApp<S>contract:id+help(surfaced in/completions),mode(modalowns input and blocks the composer;ambientcaptures nothing, never blocks, toggles closed on relaunch) +zoneplacement — 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 declarewidth, same-corner cards stack, crash-isolated by a per-widget error boundary),init(slash tail → state; null ⇒ launcher printsusage),reduce(next state / same reference to swallow / null to close),render(composes SDK primitives at viewport level).registry.ts— the registry IS the catalog:defineWidgetAppregisters at module load (last-writer-wins so user apps can shadow built-ins); slash commands are GENERATED fromlistWidgetApps()and/completions merge each app'shelpclient-side — adding an app is onedefineWidgetAppcall, 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 genericwidgetslot 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/)grid-testuseInputHandlers), app stacking viaopenWidget(dopens the dialog app)dialog-testOverlay+Dialog+ backdrop, minimal close-only reducerweatherinitfires the fetch, results land viaupdateWidget; docks as a glanceable card (composer stays live,/weathertoggles it away); table-driven WWO-code → ASCII-art buckets; every tint is a theme family tone (sun = primary, rain = shell blue, thunder = warn)tickerCore shrinkage
interfaces.tsdropsDialogState/GridTestState;overlayStore's two demo slices collapse intowidget;appOverlays/appLayoutlose bespoke demo branches; the slash commands are three-line launchers through onewidgetCommandhelper.Self-authoring (Hermes makes its own widgets)
The Python-plugin contract, mirrored: drop
<name>.mjsinto$HERMES_HOME/tui-widgets/default-exportingregister(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. Thetui-widgetsskill (skills/productivity/tui-widgets/) teaches the agent the contract, with a working/clocktemplate.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
/grid-test(4x3,6 4,streams)aareas,sstreams,g/pgap/padding,nnest,+ - ] [resize,z/Enter zoom,dstacks the dialog app,rreset, Esc/q close/dialog-test [zone]/weather [location]/weatheragain dismisses (blank location = IP geolocation)/ticker [symbol]/weatherto see the multi-widget dock/widgets-reload$HERMES_HOME/tui-widgets/— try it with the skill'sclock.mjstemplate, then/clock Asia/TokyoTest plan
openWidget,$isBlocked.mjs→ load → registered + launchable, broken files reported not fatalupdateWidget, late-resolution guard (toggle-close mid-flight), error phase, keymap (fetch mocked — no live network)widgetslotFollow-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.