Developer Guide

Workbench plugin development

A Cheers workbench plugin is one sandboxed HTML file that renders a channel file as an interactive UI β€” a checklist, a paper tracker, a review board β€” and writes edits back. No build step, no SDK: an embedded JSON manifest plus a small postMessage protocol.

1. Mental model: renderers are CSS for files

Renderers can be narrow ("markdown checklists with - [ ] lines") or broad. Small, focused renderers coexist, like CSS rules that each match a specific selector.

2. Anatomy of a plugin

A single .html file containing:

  1. an embedded manifest β€” parsed with DOMParser on upload, never executed;
  2. your rendering logic β€” vanilla JS or bundled framework code, all inlined;
  3. postMessage calls to talk to the host.

It runs in an <iframe sandbox="allow-scripts"> with an opaque (null) origin: it cannot read the host's token, cookies, or localStorage, and it can only reach the one file the host assigns to it.

<script type="application/json" id="cheers-plugin">
{
  "id": "md-checklist",
  "title": "Markdown checklist",
  "renderers": [
    { "id": "checklist", "title": "Checklist", "match": { "format": "markdown" } }
  ]
}
</script>
Manifest fieldMeaning
idGlobally unique plugin id (primary key on install)
titleHuman-readable name
renderers[]Renderers this plugin provides (one or more)
renderers[].match.formatmarkdown / json / toml / xml / text
renderers[].match.globOptional path narrowing, e.g. "reviews/*.md"
renderers[].match.requireAll / requireAnyContent must contain all / at least one of these substrings
renderers[].match.jsonHasJSON only: parsed object must have all these top-level keys
Two-layer acceptance. match is a cheap host-side pre-filter (your sandbox is not started). The final verdict is yours at runtime: when cheers:render arrives, actually parse the content β€” if it doesn't fit, reply cheers:unsupported {reason}.

3. The postMessage protocol

DirectiontypePayloadWhen
plugin β†’ hostcheers:readyβ€”iframe loaded; "assign me work"
host β†’ plugincheers:render{ path, format, content, version, rendererId }Assigns one file; re-sent when it changes externally
plugin β†’ hostcheers:unsupported{ reason? }Runtime verdict: can't render this content
plugin β†’ hostcheers:save{ content }Write the assigned file back
host β†’ plugincheers:saved{ ok, version, error? }Optimistic-lock result; on ok, update your version
plugin β†’ hostcheers:resource{ reqId, resource, params }Read-only channel info (whitelisted)
host β†’ plugincheers:resource:result{ reqId, ok, data|error }Resource read result

4. Minimal skeleton

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <script type="application/json" id="cheers-plugin">
    { "id": "my-plugin", "title": "My plugin",
      "renderers": [{ "id": "main", "title": "My renderer", "match": { "format": "markdown" } }] }
  </script>
</head>
<body>
  <div id="root"></div>
  <script>
    var ASSIGN = null;
    window.addEventListener("message", function (e) {
      var m = e.data; if (!m || typeof m !== "object") return;
      if (m.type === "cheers:render") {
        ASSIGN = m;
        // 1. parse m.content β€” if it doesn't fit, bail out:
        //    parent.postMessage({ type: "cheers:unsupported", reason: "…" }, "*");
        // 2. draw UI (textContent only for untrusted text)
        // 3. on edit:
        //    parent.postMessage({ type: "cheers:save", content: newContent }, "*");
      } else if (m.type === "cheers:saved") {
        if (m.ok) ASSIGN.version = m.version;
      }
    });
    parent.postMessage({ type: "cheers:ready" }, "*");
  </script>
</body>
</html>

5. Working examples

Ready to upload as-is (Settings β†’ Workbench extensions β†’ upload the .html):

Matching environment templates (Workbench drawer β†’ upload the .json): md-demo Β· lit-review Β· code-project. Full index: examples README.

6. Security model

  1. Opaque origin β€” sandbox="allow-scripts" without allow-same-origin: the plugin cannot steal host credentials.
  2. Single-file capability β€” the host proxy pins the plugin to the one assigned path; server-side channel-role auth is unchanged.
  3. Inert manifest β€” parsed with DOMParser, never executed.
Note: the sandbox isolates tokens and DOM, not network β€” that is why the channel-resource whitelist is read-only and conservative, and plugins are admin-installed (the installer vouches for them).

7. Install & bind

Read more