Termy
Using TermyPlugins

$ 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:

plugin.json
{
  "apiVersion": 1,
  "id": "todos",
  "name": "Todos",
  "main": "plugin.tsx"
}

Add the Termy JSX pragmas, declare the view, and return view.open from a command:

plugin.tsx
/** @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.

  • Button and Checkbox require an id and action; payload carries optional item data.
  • TextInput requires an id; submit names 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

ComponentSupported props
Column, Rowgap, align, and children
Textvariant, tone, and text children
TextInputid, label, placeholder, value, maxLength, submit, disabled
Buttonid, action, payload, variant, disabled, and text children
Checkboxid, action, payload, checked, disabled, and text children
DividerNo props
Spacersize

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.

On this page