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": "…"
}
}
| Field | Required | Constraints |
|---|---|---|
payload.html | yes | A string containing a complete HTML document, ≤ 1 MB. Missing, non-string, or oversized → 400. |
payload.title | no | The iframe title (accessibility). |
title (top level) | no | Card 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 srcdoc — no 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, andfetchto 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.