- TypeScript 100%
Two inputs in one card shared the host's first field, so a hidden tab's input swallowed the visible one's keys. Mark which fields are present in the current tree, route keys to the focused (else first active) field, and keep hidden tab fields cached without letting them take input. Cursor-only motion now re-describes the input so the terminal sees the new caret, and terminal selection edits and undo are applied through Input.handleNativeEvent instead of being forwarded to the card's onEvent. The focus event selects the field when the host receives it. |
||
|---|---|---|
| .omp-plugin | ||
| src | ||
| test | ||
| .bun-tag | ||
| .gitignore | ||
| bun.lock | ||
| index.ts | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
omp-tsp-widgets
Interactive answer cards for omp (oh-my-pi), drawn natively by Tern through the Tern Surface Protocol (TSP).
Instead of answering only in prose, the model can write a small TypeScript program. The plugin runs it in omp's retained JS kernel and hosts the node tree it returns as TSP nodes in the transcript. The card streams in while the code is still arriving, Tern draws it natively, and clicks or keys on its controls flow back into the kernel.
What it does
- The model picks the form. A
uitool and a short system-prompt note let the model decide per answer: plain text when that reads best, a card when seeing or operating the answer helps (calculators, comparisons, plans, labelled drawings, quizzes, small games). - Streams as it is written. The tool's call preview shows the code as it arrives; the card mounts as soon as the program has run.
- Operable in place. Controls are ordinary TSP nodes: click them, or focus them with the terminal's usual keys. Every interaction re-runs
describe()and repaints the card. - State lives in the card.
statepersists across interactions and re-describes;ui.post(text)sends the user's next message to the model;ui.css(text)installs a surface-scoped stylesheet. - One block per card. The call and its result share a native node, so node ids stay stable and controls keep their target while the card updates.
Requirements
- omp 18.14.1 or newer, running in Tern (a TSP terminal). Cards are disabled in print and RPC modes and in subagents.
- Tested with omp 18.14.1 and Tern 0.9.8.
Install
From a clone of this repository:
omp plugin install /path/to/omp-tsp-widgets
Or link it while developing (edits are picked up on the next session):
omp plugin link /path/to/omp-tsp-widgets
This repository also ships a marketplace (.omp-plugin/marketplace.json):
omp plugin marketplace add ./.
omp plugin install omp-tsp-widgets@rakutek
Run the first command from the repository root (marketplace sources are either ./. or a GitHub owner/repo).
Start a new omp session afterwards. To try it without installing:
omp -e /path/to/omp-tsp-widgets/index.ts
Use
Ask for something that benefits from a visual answer — "make me a savings calculator", "break down the design of a 7-speed bike", "plan a day trip to Hakone". The card appears in the transcript as the model writes it.
Operate it directly: click a button, tab or checkbox, or type in a field. tab/shift+tab move focus, enter/space activate. A button's handler can call ui.post(...) to send a message for you.
The card API
The model writes the card's whole program; the tool contract is in src/guide.ts.
ui.mount({ describe, onEvent? })—describe()returns the card's node tree;onEvent(ev)reacts to user actions. Both may be async.- Layout:
n(kind, props?, children?, key?),col,row,card,section,rule,spacer,el. - Content:
text,span,md,code,diff,ansi,math,table,kv,meter,progress,badge,kbd,icon,list,item,checklist,tree,json,figure,media. - Charts:
chart({ kind: "bars" | "spark" | "heatmap", … }), plusbars,sparkandheatmapshortcuts. Series entries accepttone;marksare reference lines. - Controls:
tabs,input(editable; Enter sends{ type: "send", key, text }),editor(display-only),checkbox,button.ev.keyis the node's own key; buttons sendaction, tabsselect, inputssend. - Motion:
effort,rate;spinner,shimmerandelapsedthroughn(...). - Kinds are Tern's TSP vocabulary, so every native component above is drawn by the terminal itself.
figure(svg, caption?)is converted by the host into a cached SVGmedianode;media(path)shows a file the terminal can draw;el(tag, …)is Tern's plain HTML element. stateis a plain object that persists for the life of the card.ui.post(text)delivers a user message to the model;ui.css(text)replaces the card's own stylesheet (empty removes it).
The base sheet installs button chrome and numeric text transitions. A card's own sheet cascades after it.
Development
bun install
bun test # unit tests for the host, kernel shim, sanitizer and media cache
bun run check # type check
@oh-my-pi/* packages are pinned as devDependencies for type checking and tests only; at runtime omp resolves them from its own installation.
Limitations
- The extension relies on omp internals that are not stable API:
EvalTooland its retained kernel, streaming tool arguments, nativedescribehooks, and TSP stylesheet writes. A future omp release may need an update. - Cards are off outside the interactive TUI and in subagents.
- The tool description adds roughly 1.5k tokens to every request while the extension is loaded.
- A card that fails to mount reports the error to the model. After five consecutive failed event cells it is disabled rather than retried forever.
- Tern's own verdicts on rejected frames are relayed to the model as a hidden next-turn note.
- Like every omp extension, it runs inside omp with your permissions.