D Docs · AI Computer Company Open Docs ↗

Dynamic views

Author your own UI and push it into the document — the custom view payload, sandbox constraints, and the postMessage bridge v1 protocol.

Need a visualization that no built-in view kind covers? Push kind: "custom" with a self-contained HTML/JS document and the app renders it in the Views pane next to the document. Charts, kanban boards, decision matrices, timelines — whatever you and the user need.

The custom view payload

POST /api/docs/:id/views
{
  "kind": "custom",
  "title": "Effort vs impact",
  "payload": {
    "html": "<!doctype html>…",
    "title": "…"
  }
}
FieldRequiredConstraints
payload.htmlyesA string containing a complete HTML document, ≤ 1 MB. Missing, non-string, or oversized → 400.
payload.titlenoThe iframe title (accessibility).
title (top level)noCard header in the Views pane.

Custom views upsert by kind like other views — one custom view per document by default. Pass "replace": false to keep several.

Sandbox constraints

Your HTML renders in <iframe sandbox="allow-scripts"> via srcdocno same-origin access. This is the security boundary that makes arbitrary agent code safe in a multiplayer app. Concretely, your code:

  • cannot touch the parent page’s DOM, cookies, auth tokens, or the collaboration socket;
  • has an opaque origin — no localStorage, and fetch to most APIs is subject to CORS;
  • must inline all CSS/JS or load it from CDNs;
  • talks to the host app only through the postMessage bridge below.

The bridge exposes nothing the human viewer cannot already see. A sandboxed view can still visually imitate UI inside its own frame, so treat views like any user-generated content.

postMessage bridge — protocol v1

Every message carries docsBridge: 1. There are exactly two things you can do in v1:

1. getDocument — read the document and review state

iframe → parent:
  {"docsBridge": 1, "type": "getDocument", "requestId": "<any string>"}

parent → iframe:
  {"docsBridge": 1, "type": "document", "requestId": "<echoed>",
   "markdown":    "<current doc text>",
   "reviewIndex": {"items": [...]},      // review items w/ verdicts
   "actors":      {"<actorId>": {...}},  // known collaborators
   "views":       [...]}                 // all pushed views

2. requestRefresh — ask the agent to regenerate

iframe → parent:
  {"docsBridge": 1, "type": "requestRefresh"}

The app fires the refresh-request endpoint for this view, which reaches the owning agent as a refresh-requested event on the view-events long-poll.

Nothing else is in v1.

Implementation notes: the parent answers with targetOrigin: '*' (sandboxed frames have an opaque origin), and only messages originating from your iframe are honored.

Complete working example

A live word count derived from the document, with a “refresh” button that pings the agent:

<!doctype html>
<html>
<body style="font-family: system-ui; padding: 16px">
  <h3>Word count</h3>
  <div id="out">loading…</div>
  <button id="refresh">Ask agent to refresh</button>
  <script>
    const requestId = String(Math.random())
    addEventListener('message', (event) => {
      const msg = event.data
      if (!msg || msg.docsBridge !== 1) return
      if (msg.type === 'document' && msg.requestId === requestId) {
        const words = msg.markdown.split(/\s+/).filter(Boolean).length
        document.getElementById('out').textContent = words + ' words'
      }
    })
    parent.postMessage({ docsBridge: 1, type: 'getDocument', requestId }, '*')
    document.getElementById('refresh').onclick = () =>
      parent.postMessage({ docsBridge: 1, type: 'requestRefresh' }, '*')
  </script>
</body>
</html>

Push it (jq packs the HTML file into the JSON payload):

curl -X POST https://docs.aicomputercompany.com/api/docs/doc_abc123/views \
  -H 'Content-Type: application/json' -H 'x-actor-id: agent_demo' \
  -d "$(jq -Rs '{kind: "custom", title: "Word count", payload: {html: .}}' < view.html)"

Closing the loop

To make your view live, run the view-events long-poll in your agent: when a refresh-requested event for your view arrives, re-read the document, rebuild the HTML (or just its data), and POST the view again. The pane updates in place for everyone looking at the document.