$ termy docs
Native UI
Build safe JSX tools that Termy validates and renders with GPUI.
Plugins can open small native tools from .tsx entrypoints. JSX is compiled into a
strictly validated Termy document and rendered with GPUI; it is not HTML or React,
and plugins never receive native renderer access.
Open a view
Point the manifest at a .tsx entrypoint:
{
"apiVersion": 1,
"id": "todos",
"name": "Todos",
"main": "plugin.tsx"
}Add the Termy JSX pragmas, declare the view, and return view.open from a command:
/** @jsxRuntime classic */
/** @jsx TermyUI.createElement */
/** @jsxFrag TermyUI.Fragment */
export default definePlugin({
commands: [{
id: "open",
title: "Todos: Open",
run() {
return { type: "view.open", view: "todos" };
},
}],
views: {
todos: {
title: "Todos",
async render({ context }) {
const todos = await context.storage.get<Array<{
id: string;
title: string;
done: boolean;
}>>("todos") ?? [];
return (
<TermyUI.Column gap="medium">
<TermyUI.Row gap="small" align="center">
<TermyUI.TextInput id="title" placeholder="Add a task…" submit="add" />
<TermyUI.Button id="add-button" action="add" variant="primary">
Add
</TermyUI.Button>
</TermyUI.Row>
<TermyUI.Divider />
{todos.slice(0, 24).map((todo) => (
<TermyUI.Checkbox
id={`todo-${todo.id}`}
action="toggle"
payload={todo.id}
checked={todo.done}
>
{todo.title}
</TermyUI.Checkbox>
))}
</TermyUI.Column>
);
},
async onAction({ action, values, context }) {
// Persist changes using action.id, action.payload, and the form values.
// Termy rerenders the view after this handler finishes.
},
},
},
} satisfies TermyPlugin);The view key (todos) must match the view in the view.open action. The managed
termy.d.ts file supplies TermyUI, JSX, view, and action types globally.
Handle interactions
Interactive controls use named actions instead of JavaScript callbacks. Any view
that renders an interactive control must define onAction; passive views may omit
it.
ButtonandCheckboxrequire anidandaction;payloadcarries optional item data.TextInputrequires anid;submitnames the action emitted when the user submits it.- Every control ID in one rendered document must be unique.
onAction receives action.id, action.controlId, optional action.payload and
action.value, plus a values snapshot keyed by control ID. It can update storage,
emit toasts, or return any normal plugin action.
After it finishes, Termy calls render again and replaces the document.
type Todo = { id: string; title: string; done: boolean };
async onAction({ action, values, context }) {
if (action.id === "add") {
const title = String(values.title ?? "").trim();
if (!title) {
context.toasts.info("Give the todo a title first");
return;
}
const todos = await context.storage.get<Todo[]>("todos") ?? [];
await context.storage.set("todos", [
...todos,
{ id: crypto.randomUUID(), title, done: false },
]);
}
}Component allowlist
| Component | Supported props |
|---|---|
Column, Row | gap, align, and children |
Text | variant, tone, and text children |
TextInput | id, label, placeholder, value, maxLength, submit, disabled |
Button | id, action, payload, variant, disabled, and text children |
Checkbox | id, action, payload, checked, disabled, and text children |
Divider | No props |
Spacer | size |
Gaps and spacer sizes use none, small, medium, or large. Alignment uses
start, center, end, or stretch. Text variants are heading, body,
caption, and code; tones are default, muted, success, and danger.
Button variants are secondary, primary, and danger.
Styling is semantic and theme-aware. Arbitrary CSS, GPUI properties, assets, callbacks, and colors are rejected. Read Security and limits for document and runtime boundaries. Paginate dynamic lists so large saved collections stay inside the node, child, and value-control limits.