Agent pages
When an agent needs inputs that recompute, tabs, or a layout of its own, it writes a page instead of blocks: an .html file, or a .tsx or .jsx file.
show_doc runs it in a sandboxed frame, and the page answers the agent through the page around the frame.
An HTML page
Section titled “An HTML page”A form whose action is cgi-bin:pi-answer sends its fields to the agent when submitted.
A script can do the same with piBetterShow.send.
<form action="cgi-bin:pi-answer"> <label>Name <input name="name" value="Ada"></label> <label>Colour <select name="colour"><option>red</option><option>blue</option></select> </label> <button type="submit">Send to the agent</button></form><script> document.querySelector("button").addEventListener("click", () => { window.piBetterShow.send({ source: "script", clicked: "yes" }); });</script>The agent receives the answer under the page’s name, so it knows a page it wrote sent it:
Agent page notes.html sent:User submitted a form in the HTML review pane:- name: Ada- colour: redWhat `send` takes
- An object or a list of
[name, value]pairs becomes aformanswer; only string values are sent. - An
Answer({kind, fields}) is sent as it is, so a page may answer as any kind. - It resolves with
{accepted, reason?}; with no reply from the page server it gives up after a wait.
A TSX page
Section titled “A TSX page”A page that needs state writes a React component as its default export.
The server builds it with esbuild on each load, with the React this package ships, and the pi-kit module gives it the block wrappers.
import { PbsBlock, PbsChecklist, parse, send } from "pi-kit";import { useState } from "react";
const blocks = parse("```options\n- {title: Small, body: Fewer parts}\n- {title: Large, body: More room}\n```");
export default function Page() { const [count, setCount] = useState(0); return ( <main> <button type="button" onClick={() => setCount(count + 1)}>Clicked {count} times</button> <button type="button" onClick={() => void send({ clicks: String(count) })}>Send the count</button> {blocks.map((segment) => segment.type === "block" ? ( <PbsBlock key={segment.index} segment={segment} onAnswer={(answer) => void send(answer)} /> ) : null, )} <PbsChecklist data={{ title: "Ready?", items: [{ name: "tests", label: "Tests pass" }] }} onAnswer={(answer) => void send(answer)} /> </main> );}What `pi-kit` exports
- A wrapper per block (
PbsBlock,PbsForm,PbsOptions,PbsReview,PbsChecklist,PbsTable,PbsCallout,PbsCards,PbsMindmap,PbsSuggest,PbsSteps,PbsStats,PbsDiff,PbsDiagram,PbsTree) takingdataandonAnswer;PbsBlockdraws a parsed segment. parse,inline, andencodeAnswerfrom core, andsend.- Sibling modules import freely; CSS imports are emptied.
The sandbox
Section titled “The sandbox”A page runs in <iframe sandbox="allow-scripts allow-forms"> without allow-same-origin: no storage, no cookies, nothing loaded from other sites, and no way to answer on its own.
The server refuses a request from the frame itself; the frame asks the page around it, by postMessage, to carry the answer.
How an answer leaves the frame
The reference has the rules: which messages the parent trusts, and what the server checks.
- Feedback: the panel around the frame.
- Block reference: prefer blocks when they are enough.