Skip to main content

html-visuals

How to build a self-contained HTML visual for the session canvas or the portal viewer — the sandbox contract, responsive rules, and the chart/dashboard idioms that survive it.

ソース情報

リポジトリ
microsoft/PilotSwarm
ソースの最終更新活動
2026年9月30日 02:33
検出された SKILL.md の言語
英語
スター
6
フォーク
3

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
html-visuals
description
How to build a self-contained HTML visual for the session canvas or the portal viewer — the sandbox contract, responsive rules, and the chart/dashboard idioms that survive it.
# HTML Visuals When the user wants to *see* something — a chart, a dashboard, a graph, a comparison, a timeline, "show me", "visualize this" — build a single self-contained HTML page. This skill is about the PAGE; where it goes is decided once, per visual: - **Root session → the canvas.** `draw_canvas(html, note)` puts it on the user's screen live. No artifact, no `show_artifact`, no links — the canvas updating IS the delivery, and iterating means redrawing it. - **A keepable file, or no canvas (sub-agents)** → write it as an artifact and open the reader: write_artifact(filename="outage-graph.html", content="<!doctype html>…") show_artifact(filename="outage-graph.html") Include the returned `artifact://` link in your reply in this case only — the live push lands if someone is watching; the link works forever. Never send the same visual to both surfaces; one request, one destination. ## Forms: batch the input Use this whenever you draw a canvas whose job is to COLLECT input: a release sign-off, a settings sheet, a triage checklist, a survey. The cardinal rule: **The page is the scratchpad; you are the database. One user intent = one action.** A 23-item checklist is NOT 23 canvas actions — the user checks, unchecks, writes comments, changes their mind, and everything stays in page state. You hear about it exactly once, when they hit Submit. ## The contract shape Declare one submit action carrying a single `json` field (the whole form state), plus at most a couple of coarse verbs: draw_canvas(html=..., note="release sign-off", responseContract={"actions": { "submit": {"decision": "string", "form": "json"}, }}) `json` fields carry one structured object (bounded at 8 KB serialized — a checklist of dozens of items with comments fits comfortably; do not ship essays through it). ## The page pattern - Keep ALL form state in a plain JS object; every checkbox/input writes to it locally. Nothing posts until submit. - Submit gathers and posts once: function submit(decision) { parent.postMessage({ type: "canvas-action", action: "submit", data: { decision, form: { rev: FORM_REV, items: state.items } } }, "*"); lockForm("Submitted — waiting for confirmation…"); } - **Bake a form revision in.** Set `FORM_REV` to the canvas rev you drew (you know it from the draw result) and include it in the payload — if you have redrawn since, a submit carrying an older rev tells you the user answered a stale form; say so and redraw the current one. - **Lock on submit** (disable inputs, show a submitted banner). Your confirmation is the next redraw; the lock is what stops double-submits and makes the seconds of latency legible. - GO/NO-GO style decisions are two buttons calling the same submit with a different `decision` — not two actions. - On receipt, REDRAW showing what you recorded (received N items, decision, the follow-ups you are taking). The redraw is the acknowledgement. ## What NOT to do - No per-keystroke, per-checkbox, or per-row actions. If you feel the urge to know intermediate state, you have designed the form wrong. - No timers or auto-submit — a form posts on an explicit user gesture only. - Do not redraw while the form is live except in response to a submit; a redraw replaces the document and wipes everything typed so far. - Do not block the session on the form: keep working where possible, and note in chat that the canvas is waiting whenever the user may not be looking. Hard approval gates stay on `ask_user`. ## Sharing caveat Only a viewer with session WRITE access (the creator, and anyone the session is write-shared with) can post canvas actions — read-only viewers see the form, but their clicks are refused by the platform (their view of a mutating canvas may be stale). If the session is shared, say in the page footer who the form is addressed to, and let others respond in chat. ## Live data updates (update_canvas) When the canvas's CONTENT changes but its layout does not — dashboards, tickers, watchers — draw the shell ONCE, then tick with `update_canvas(data={...})`. The tick reaches the page as a message; the platform replays the latest tick into any freshly loaded page, so one idempotent renderer covers live updates and cold loads alike: addEventListener("message", (e) => { if (e.data?.type === "canvas-data") applyData(e.data.data); }); function applyData(d) { /* render the WHOLE state from d */ } - Declare `data: { example: {...} }` in the draw contract; after a memory gap, re-read it before ticking so your payload shape never drifts. - Ticks never flip the view and write no chat line; they light the unseen badge until the user looks. Cap: 32 KB for the MERGED state. - Layout change → `draw_canvas`. Content change → `update_canvas`. Never redraw to refresh numbers. ### Patch, don't re-send (update_canvas patch) `data` REPLACES the whole state — use it for the first tick after a draw and for wholesale refreshes. For every incremental change, send a PATCH: update_canvas(patch={ gauges: { cpu: 91 } }) // a few tokens update_canvas(data={ ...entire dashboard... }) // thousands — avoid Patch rules (RFC 7386 merge patch, applied server-side): - Send ONLY the subtree you are changing. Objects deep-merge into the current state; arrays and scalars replace wholesale. - `null` DELETES a key. Omit keys you are not touching — null is deletion, not "no change". - Your page keeps working unchanged: `canvas-data` messages always carry the complete merged state. A page that wants the delta too (targeted DOM updates, transitions) may ALSO listen for `canvas-patch` messages (`{type:'canvas-patch', patch, dataRev}`) — the whole-state message still arrives alongside, so the fancy path can never strand the page. - Lost track of the current state (restart, another writer)? Resync with `read_canvas(include_data=true)` and go back to patching. ### Shared dashboards (sub-agents writing the parent's canvas) Every canvas tool takes `session_id` targeting an ANCESTOR session — parent, grandparent, root; never siblings or strangers. The pattern for a parent with N children keeping one dashboard live: 1. Parent draws the shell once with a `tiles` region per child. 2. Each child patches ITS OWN subtree of the parent's canvas: `update_canvas(session_id=<parent>, patch={ tiles: { worker3: {...} } })`. Disjoint subtrees compose automatically — children cannot clobber each other. Same-path writes are last-writer-wins, attributed. 3. A child's write never flips the parent's view; the unseen badge and the activity feed carry who wrote what. ## Interactive canvases A canvas can accept structured responses while a user views it live. Declare the contract on the draw, then wire controls to `parent.postMessage`: draw_canvas(html=..., note="order form", responseContract={"actions": {"submit": {"qty": "number", "note": "string?"}}}) <button onclick="parent.postMessage({ type: 'canvas-action', action: 'submit', data: { qty: Number(qty.value) } }, '*')">Submit</button> Rules that make this work: - The browser enforces YOUR contract: undeclared actions, wrong types, and extra fields are dropped before they reach you. No contract on the current revision means nothing is accepted — a display-only canvas stays inert. - Responses arrive as `[canvas-action] {"action":...,"data":...}` user messages, hidden from the portal chat. Answer by REDRAWING with the result baked in — the canvas is both the form and the reply. - Echo input optimistically in the page (append the row, disable the button); your redraw arrives seconds later and replaces the document. - The sandbox has no `allow-forms`: wire click/keydown handlers, never rely on real form submission. Enter-to-submit = a keydown listener on the input. - **Text inputs: `font-size: 16px` minimum.** iOS Safari zooms the whole viewport when a smaller input is focused and never zooms back — the page comes out cropped and panned after every comment box. - A page taller than the viewport scrolls in the browser's own scroller — set nothing that traps it (`overflow: hidden` on body kills mobile scroll; `touch-action` overrides eat pans). Let the default body scroll work. - Do not redraw a canvas that carries input fields except in response to an action — a spontaneous redraw wipes drafts mid-typing. - Controls only function while someone is watching the portal. Never block on a canvas click; chat is the fallback and approval gates stay on ask_user. Prefer this over pasting a Markdown table when the answer is *shape* — proportion, trend, flow, overlap, ranking across many rows. Prefer a Markdown table when the answer is a handful of exact values. A table of 8 numbers does not need a chart; 200 rows of "who is waiting on what" does. ## The sandbox contract The page runs in an iframe with `allow-scripts` and **no** `allow-same-origin`. Your script runs; it just has no world outside the document. Violating any of these produces a blank or broken page, not an error message: 1. **Self-contained. No network of any kind.** No CDN `<script>` or `<link>`, no web fonts, no remote images, no `fetch`. Inline every byte: CSS in `<style>`, JS in `<script>`, data in a JS literal, images as `data:` URIs. If you need a charting library you cannot inline, hand-write the SVG. 2. **No storage.** `localStorage`, `sessionStorage`, cookies and `parent` property reads all throw — `parent.postMessage` is the one allowed call. Keep state in memory. Shared state, the session's files, and commands such as `git log` go through that call: load `canvas-apps`. 2b. **Sound: unlock on `touchend`/`click` — never only `touchstart`.** Three silent failures, none of which raise an error: - `touchstart` does NOT grant user activation (spec: only `touchend`, `click`, `keydown`, `mousedown` do), so an AudioContext created or resumed there stays suspended on iOS forever — and calling `preventDefault()` in `touchstart` also suppresses the synthetic `click`, so a page that binds game controls with `touchstart`+`preventDefault` never runs a working unlock at all. This is why a game sounds fine on desktop and is mute on every phone. Bind the unlock to `touchend` (or an un-prevented `click`) as well: `el.addEventListener("touchend", unlock)` alongside the touchstart game-input handler. - Autoplay policy suspends a context created at load. In the unlock, do `ctx = ctx || new AudioContext(); ctx.resume()`, keep one context for the page's life, and synthesize with oscillators (no files). - **iOS mutes Web Audio when the ring/silent switch is on.** In the same unlock, run `if (navigator.audioSession) navigator.audioSession.type = "playback";` (Safari 16.4+; guard it — the API does not exist elsewhere). Say so in the page's own UI ("sound needs the ring switch on" beside the mute toggle); the user cannot debug what the page never mentions. 2c. **On the session canvas, resizes reflow — they never reload.** The canvas never reloads your page for a container resize, so in-memory state (game progress, form drafts) survives window drags and rotations there. The page's inner window gets real `resize` events; if you draw to a `<canvas>` at pixel sizes or measure the viewport once at boot, add a resize listener that recomputes — CSS layouts need nothing. The artifact READER pane (show_artifact) is different: it reloads when its width settles — stateful pages belong on the session canvas. 3. **Set your own colors — on `html`, not only `body`.** iOS extends the `html` background into rubber-band overscroll; a body-only background ends at the document edge and the bounce reveals whatever sits beneath. Set `background` and `color` once on `html, body`. Add `html { overscroll-behavior: none }` if the bounce itself is unwanted. Dark-on-dark is the usual failure; check both. 4. **Budget.** The canvas caps at 900 KB; text artifacts cap at 1 MiB. A few hundred KB of inlined data is fine and normal; a multi-megabyte dump is not — aggregate first. Links to the outside world are fine (`<a href="https://…" target="_blank">`) — popups are allowed. ## Responsive, and why it matters here The reader pane is resizable, and the user *will* resize it. The viewer reloads the frame once the width settles, so a document that computes its layout only at load does get corrected — but the reload costs the reader their scroll position. Layout that reflows on its own never pays that. - Lay out with CSS grid/flex and `minmax()`, not fixed pixel columns. - Give every `<svg>` a `viewBox` and `width:100%`, so it scales without script. - If you must compute geometry in JS (Sankey flows, force layouts, anything where positions depend on measured width), add a `resize` listener that re-runs the layout. Debounce it. - Never bake the viewport width into a constant at load time. ## Dashboard shape A dashboard that reads well is almost always the same three bands: 1. **Header** — what this is, what window it covers, where the data came from. Someone opening it a week later must not have to ask. 2. **KPI row** — 4–9 tiles, one number each, in `grid-template-columns: repeat(auto-fit, minmax(150px, 1fr))` so it reflows. Big value, small uppercase label under it. Color only where it *means* something (bad = red, good = green); a rainbow of tiles means nothing. 3. **Sections** — one question per card, with a one-line note under the heading saying how to read the picture. Charts do not explain themselves. ## Picking the form | The question | The form | |---|---| | How much, across categories | Horizontal bars, sorted by value | | How it changed over time | Line; area only if the total matters | | What flows to what | Sankey / chord, nodes in fixed columns | | How two measures relate | Scatter, with the outliers labelled | | Where the mass sits | Histogram or box plot, not an average | | Exact values, few rows | A table. Not a chart. | Avoid pie charts beyond three slices, dual y-axes, and 3-D anything. ## Color and legibility - Encode with position and length first; color is for *categories* and *status*, not decoration. - Keep a small named palette in `:root` and reuse it. Semantic colors (`--bad`, `--ok`, `--warn`) must mean the same thing in every section. - Body text at 13–14px minimum, axis labels no smaller than 11px. - Never rely on color alone: pair it with a label, shape, or ordering, or a red/green reader learns nothing. - Long category names: truncate with `text-overflow: ellipsis` and put the full string in a `<title>` so hover reveals it. ## Interaction Hover tooltips are worth the code — they are how a dense chart stays readable without labelling every mark. Keep them cheap: one absolutely-positioned div, moved on `mousemove`, hidden on `mouseleave`. Skip zoom, pan, and filtering unless asked; they add a lot of script for something the user did not request. ## Self-check before you show it - Opens with no console errors and no blank regions. - **If the page has a start/pause state machine, its initial state must be VISIBLE and reachable.** A Start overlay that is `display:none` with nothing adding its show-class renders as a frozen page: the board paints once, `running` stays false, and every input handler that guards on it silently ignores the user. Trace the path from first paint to running with no keyboard — touch only. - Readable at both a narrow reader pane (~500px) and full width. - Every number on screen is traceable to the data you were given. - The header says what it is and what window it covers. - Nothing loads from the network. ## Reusable canvas apps A canvas worth drawing twice is an app: save it once, redraw it from the store forever, and never courier the bytes through your own context again. 1. **Self-describe from the first draw.** Put one comment immediately after `<!doctype html>`: ```html <!-- CANVAS-APP-MANIFEST { "manifestVersion": 1, "name": "release-signoff", "version": "1.0.0", "description": "23-item release checklist; batches checks into one submit.", "responseContract": { "actions": { "submit_signoff": { "items": "json", "note": "string?" } } }, "data": "{items:[{id,state}]} — full replacement, <=32KB", "notes": "Send update_canvas ticks to pre-check items; page posts one submit." } --> ``` The contract uses the exact draw_canvas grammar; a broken embedded contract makes later fromArtifact draws FAIL CLOSED, so keep it valid. 2. **Save = server-side copy, never a re-emit:** `write_artifact({fromArtifact: {filename: "canvas.html"}, filename: "app-<name>.html"})` — the source defaults to this session. 3. **Load = draw_canvas({fromArtifact: {sessionId?, filename}}).** The bytes go store→canvas; your tool result is the interface card — manifest summary plus the EFFECTIVE responseContract — which is everything needed to interpret `[canvas-action]` messages and author update_canvas ticks. NEVER read_artifact + draw_canvas(html: <same text>): that is the token-courier anti-pattern this mechanism exists to kill. 4. **Re-learn cheaply.** Browsing candidates: `read_artifact({sessionId, filename, manifestOnly: true})`. After context regeneration or on an inherited canvas: `read_canvas({manifestOnly: true})` returns the card plus the armed contract.
GitHubで見る