---
name: rect-view-author
description: Author a "Rect" view — a single self-contained HTML file that renders a JSON view model, syncs edits through the Rect.js state store, and can declare named actions the host runs server-side, for uploading to rect.sh. Use whenever the user wants to build, create, or edit an interactive view, dashboard, form, board, or review tool to run on rect.sh.
version: 0.2.0
homepage: https://rect.sh
---

# Authoring a Rect view (SKILL.md)

This guide is for an agent (or a person) writing the **HTML for a Rect view** —
a small, self-contained page a human opens to do one piece of structured work
(review items, make decisions, organize a board, fill a form), after which the
agent reads the result back.

If you only read one thing: **a Rect view is plain HTML that renders a JSON
"view model" and edits it through a tiny state store called `Rect.js`. You
write the rendering; Rect.js handles state and syncing it back to the host.**

---

## 1. The mental model

```
 your HTML  ──renders──>  the human sees a UI
     │                          │ clicks / types
     │  reads state             ▼
  Rect.js  <── store ──  set() / update() / patch()
     │                          │
     │  postMessage             ▼
   Host (Rect web page)  ── JSON Merge Patch ──>  server (dumb JSON store)
```

- **The server is view-agnostic.** It does not understand your data. It stores
  your view model as opaque JSON, and its free-form write op is a
  [JSON Merge Patch (RFC 7386)](https://www.rfc-editor.org/rfc/rfc7386). The
  one way to teach it your rules is **named actions** (section 5): handlers
  *your view declares* that the host runs in a sandbox on dispatch. Logic you
  don't put in an action lives only in your HTML.
- **Rect.js is a state store, nothing more.** It connects to the host, holds the
  current view model, lets you read/subscribe/write it, and syncs writes
  optimistically (your UI updates instantly; the host confirms a moment later).
- **You own rendering.** Vanilla DOM is great. You may load your own framework
  (Vue, Alpine, lit, Preact…) from a CDN inside your HTML if you want — Rect.js
  does not impose one and does not render anything itself.
- **The view model is just JSON you design.** Pick whatever shape best fits your
  UI. Keep it small and serializable.

---

## 2. Required structure of a view

Every Rect view HTML must have three things:

### a) An embedded spec block (so agents know how to create your view)

```html
<script type="application/rect-view+json">
{
  "name": "Sprint Board",
  "description": "A board of work items a human moves between lanes. A card belongs to a lane via card.laneId.",
  "example": {
    "lanes": [{ "id": "todo", "title": "To do" }, { "id": "done", "title": "Done" }],
    "cards": [{ "id": "c1", "laneId": "todo", "title": "First task" }]
  }
}
</script>
```

- The browser ignores this block (its `type` is not executable JS). The Rect
  server extracts it to document your view to other agents.
- **`name`** and **`example`** are **required**. `example` is a concrete initial
  view model — it doubles as the default when a view is created with no data,
  and as the shape agents copy.
- **`description`** (recommended) and **`stateSchema`** (optional JSON Schema)
  help agents but are not required.
- If the view declares [named actions](#5-named-actions-optional), the spec
  also carries an **`actions`** catalog (per-action `description` +
  `inputSchema`) and optionally **`patchPolicy`** (`"open"` default, or
  `"actions-only"` to refuse free-form agent patches).

### b) Load Rect.js

```html
<script src="/rect-js/Rect.js"></script>
```

Use that **exact root-absolute path**. Do NOT rewrite it to `file:///…`, a full
`https://…` origin, or a relative path — the view is served from inside
rect.sh, where `/rect-js/Rect.js` resolves against the host. (Opening the `.html`
file directly won't work: a view only runs when served by rect.sh, because
Rect.js talks to the host over postMessage. Test by uploading it, not by opening
the file.)

### c) Connect, render, and wire events

```html
<script>
  (async function () {
    const rect = await Rect.connect();
    rect.subscribe((state) => render(state)); // called now + on every change
    // wire DOM events to rect.set / rect.update / rect.patch
  })();
</script>
```

---

## 3. The store API

`await Rect.connect()` resolves to a store:

| Call | What it does |
| --- | --- |
| `rect.get()` | Returns the whole view model (a **clone**). |
| `rect.get('a.b.0.c')` | Returns the value at a dot path (`0` indexes arrays). A clone. |
| `rect.subscribe(fn)` | `fn(state)` runs immediately and on every change. Returns an unsubscribe function. |
| `rect.subscribe('a.b', fn)` | `fn(value)` runs when that slice changes. Returns unsubscribe. |
| `rect.set('a.b', value)` | Set an **object** path. Optimistic + synced. |
| `rect.update(draft => { … })` | Mutate a draft of the view model (push to arrays, edit fields). Best for arrays/complex edits. |
| `rect.patch({ … })` | Apply an RFC 7386 merge patch directly. |
| `rect.dispatch('name', input?)` | Run one of the view's [named actions](#5-named-actions-optional) host-side. Resolves to `{ ok: true, … }` or `{ ok: false, code, … }` — never throws. |
| `rect.requestAttachmentUpload(opts?)` | Ask the parent host to pick + upload a file (`{ accept?, multiple?, maxSize?, label? }`). See [Attachments](#6-attachments). |
| `rect.revision` | Server revision number (advances as writes are confirmed). |
| `rect.meta` | `{ interactionId, viewType, wakeMessage, checkUrl }`. |
| `rect.connected` | Whether the store is connected. |
| `Rect.escape(value)` | HTML-escape a string when building `innerHTML`. |

**Reads return clones.** Mutating the object you got from `get()` does **not**
change anything — always write through `set`/`update`/`patch`.

---

## 4. A complete minimal example

A review checklist: the agent proposes items, the human checks them off.

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Review checklist</title>
    <script type="application/rect-view+json">
      {
        "name": "Review checklist",
        "description": "A human checks off agent-proposed items.",
        "example": {
          "title": "Pre-merge checklist",
          "items": [
            { "id": "i1", "label": "Tests pass", "done": false },
            { "id": "i2", "label": "Docs updated", "done": false }
          ]
        }
      }
    </script>
  </head>
  <body>
    <main id="app">Loading…</main>
    <script src="/rect-js/Rect.js"></script>
    <script>
      (async function () {
        const e = Rect.escape;
        const rect = await Rect.connect();
        const app = document.getElementById("app");

        function render(state) {
          const items = Array.isArray(state.items) ? state.items : [];

          app.innerHTML = [
            "<h1>" + e(state.title || "Checklist") + "</h1>",
            '<ul style="list-style:none;padding:0">',
            items
              .map(function (item) {
                return (
                  '<li><label><input type="checkbox" data-id="' +
                  e(item.id) +
                  '"' +
                  (item.done ? " checked" : "") +
                  "> " +
                  e(item.label) +
                  "</label></li>"
                );
              })
              .join(""),
            "</ul>",
          ].join("");
        }

        rect.subscribe(render);

        // Toggle an item: mutate the matching item in a draft.
        app.addEventListener("change", function (event) {
          const box = event.target.closest('input[type="checkbox"][data-id]');
          if (!box) return;
          rect.update(function (s) {
            const item = (s.items || []).find((i) => i.id === box.dataset.id);
            if (item) item.done = box.checked;
          });
        });
      })();
    </script>
  </body>
</html>
```

---

## 5. Named actions (optional)

Free-form patches are fine for edits with no rules attached. When the view has
**invariants** — "approve must fail while items are open", "totals must stay
consistent" — declare them as named actions, so the rule is enforced host-side
no matter who writes: the human, an agent over MCP, or the CLI.

Actions are two **inert** blocks in your HTML (the browser executes neither;
the host extracts them at upload and runs handlers in a sandbox on dispatch):

```html
<!-- 1. Catalog in the spec block: name, description, inputSchema -->
<script type="application/rect-view+json">
{
  "name": "Review checklist",
  "example": { "items": [] },
  "actions": {
    "approve": { "description": "Approve the list. Rejects while any item is open." }
  }
}
</script>

<!-- 2. Handlers in the actions block -->
<script type="application/rect-actions+js">
Rect.defineActions({
  approve: function (state, input, ctx) {
    if ((state.items || []).some(function (i) { return !i.done; })) {
      ctx.reject("ITEMS_OPEN", "Finish every item before approving.");
    }
    state.completion = { approved: true, at: ctx.now };
  },
});
</script>
```

- **Handler signature**: `(state, input, ctx) => void | nextState`. Mutate the
  `state` draft in place or return the next state. `input` arrives already
  validated against the action's `inputSchema`.
- **`ctx`** is everything a handler may not take from the environment:
  `ctx.now` (ISO timestamp fixed per dispatch), `ctx.id()` (deterministic
  unique ids), `ctx.actor` (`'agent' | 'user' | 'cli'`), and
  `ctx.reject(code, message?)` to stop with a business rejection.
- **Handlers must be pure JS** over their arguments: no DOM, no network, no
  `Date.now()` / `Math.random()` (the sandbox pins both so a retried dispatch
  computes the same result).
- **The two blocks must declare the same names.** A mismatch fails the upload,
  not a dispatch later.
- From the view, run one with `rect.dispatch('approve')`. A rule violation
  comes back as a value — `{ ok: false, code: 'rejected', rejectCode:
  'ITEMS_OPEN', message: … }` — show it in the UI instead of guarding the same
  rule twice.
- Declaring `"patchPolicy": "actions-only"` in the spec makes agents use the
  catalog: over MCP, free-form `rect_patch` is refused and pointed at your
  actions.

Views without invariants don't need any of this — skip both blocks and the
view is patch-only.

---

## 6. Attachments

Files attached to an instance live in the reserved **`$attachments`**
namespace of the view model — a registry the **host owns**:

```json
{ "$attachments": { "version": 1, "order": ["att_1"], "items": {
  "att_1": { "id": "att_1", "status": "done", "name": "report.pdf",
             "kind": "pdf", "src": "/api/rect/…/attachments/att_1/report.pdf" } } } }
```

- **Read it, never write it.** Patches and action handlers that touch
  `$attachments` are rejected with `code: "reserved_key"`. Render from it:
  show `status: "uploading"` progress, use `src` only when `status` is
  `"done"` (`<img src>`, `<a download>`).
- **To let the human attach a file**, don't open your own file input — call
  `rect.requestAttachmentUpload({ accept: "image/*,.pdf" })`. The parent host
  owns the picker, validation, and upload; your view just receives the next
  state snapshot with the new registry entry.
- Agents upload through the MCP attachment tools; those also write the
  registry for you.
- If the file needs **domain meaning** ("this attachment is the signed
  contract"), add a named action that stores the attachment `id` in your own
  field — keep the bytes and upload state in `$attachments`.

---

## 7. Patterns (do this)

- **Render from `subscribe`, write through the store.** One render function that
  takes `state` keeps the UI a pure function of the view model.
- **Give list items stable string `id`s** and key your DOM by them. It makes
  diffs, focus restoration, and reasoning about edits sane.
- **Use `update(draft => …)` for arrays** (move/add/remove/edit). Use
  `set(path, value)` for simple object fields.
- **Escape everything you interpolate into `innerHTML`** with `Rect.escape` —
  view-model strings may contain `<`, `&`, quotes.
- **Protect focused inputs.** A full `innerHTML` re-render recreates inputs and
  drops the caret. Either keep freeform inputs out of the re-rendered region, or
  skip updating an input while `document.activeElement === input`, or restore
  `selectionStart/End` after re-rendering.
- **Keep ephemeral UI state in plain JS variables**, not in the view model.
  Filters, "is this panel open", drafts-in-progress — none of that should sync
  to the server.
- **Tell the human how to hand back.** `rect.meta.wakeMessage` is the phrase the
  human should reply with so the agent knows to read the result.

---

## 8. Anti-patterns (avoid these)

- **Don't expect raw patches to be validated.** The server merges any
  well-formed JSON patch without understanding your shape. If a rule must
  hold, put it in a [named action](#5-named-actions-optional) — rendering
  logic in your HTML can't stop an agent's patch.
- **Don't write to `$attachments`.** It's host-owned; patches and handlers
  touching it are rejected with `reserved_key`. Read it to render, request
  uploads through `rect.requestAttachmentUpload`.
- **Don't mutate the result of `get()` and expect it to save.** `get()` returns a
  clone. Writes must go through `set`/`update`/`patch`.
- **Don't put secrets or large blobs in the view model.** It is stored as-is
  and visible to everyone with the instance URL. Keep it small and
  non-sensitive — file bytes belong in [attachments](#6-attachments), not the
  model. Remember merge patches replace whole arrays — huge arrays mean huge
  patches.
- **Don't assume a framework is present.** Rect.js bundles none. If you want one,
  load it yourself in your HTML.
- **Don't blow away focus.** Re-rendering everything on every keystroke without
  preserving the active input's focus/selection is the most common bad UX.
- **Don't reach outside the sandbox.** The view runs in
  `sandbox="allow-scripts allow-forms"`: **no same-origin**, no cookies, no
  `localStorage`, no access to the parent page. All host communication happens
  through Rect.js (postMessage). Don't try to `fetch` your own backend for view
  state or read `window.parent` — round-trip everything through the view model.
- **Don't depend on `revision` advancing synchronously.** A write is optimistic
  locally; `revision` advances when the host confirms, slightly later.
- **Don't hardcode an `interactionId`, session token, or absolute host URL.**
  Rect.js discovers them from the host at connect time.
- **Don't rewrite the Rect.js `src`.** Keep it exactly `/rect-js/Rect.js` — never
  `file:///…` or a full origin. The view is served from rect.sh, not your local
  filesystem.
- **Don't ship a view with no `<script type="application/rect-view+json">` block,
  or one missing `name`/`example`.** Creation will reject it.
- **Don't let the spec's `actions` catalog drift from `Rect.defineActions`.**
  The declared names must match exactly — a mismatch fails the upload.

---

## 9. Lifecycle

1. Your HTML is uploaded to rect.sh and registered as a reusable **template**.
2. An agent **issues** a live instance of it (passing an initial view model,
   or defaulting to your `example`). It gets back a `viewUrl` and `checkUrl`.
3. The agent sends the human the `viewUrl`.
4. The human opens it, interacts; every change syncs to the host as a merge
   patch. Meanwhile the agent can drive the same instance — dispatching your
   named actions (`rect_dispatch`) or patching free-form fields
   (`rect_patch`) — and every write lands in the open view live.
5. The human replies to the agent with the `wakeMessage`.
6. The agent reads the result back (`rect_get_result` / `checkUrl`) and acts
   on it.

Your job is step 4: make a clear, focused UI over a small JSON view model,
write changes back through the store, and put the rules worth enforcing in
named actions.
