Skip to main content

canvas-apps

Interactive and SHARED canvas apps — page actions, the canvas_kv store, requests the page queues, the session's files and declared commands (canvas-ws), saving apps. Load before any canvas that takes input, uses files, or is shared.

Informations de source

Dépôt
microsoft/PilotSwarm
Dernière activité de la source
30 septembre 2026 à 02:33
Langue détectée de SKILL.md
anglais
Étoiles
6
Forks
3

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
canvas-apps
description
Interactive and SHARED canvas apps — page actions, the canvas_kv store, requests the page queues, the session's files and declared commands (canvas-ws), saving apps. Load before any canvas that takes input, uses files, or is shared.
# Canvas Apps A canvas visual is disposable presentation. A canvas app holds state, lets several people use it at once, and asks the session to do things. For page layout, chart form, color and the sandbox rules, load `html-visuals` first. ## The three lanes | Lane | Who writes | Reaches | Wakes you | Durable | Costs you | |---|---|---|---|---|---| | **KV** (`canvas_kv`) | any permitted viewer, and you | every viewer, ~50 ms | no | yes | tens of tokens per key | | **Action** (`canvas-action`) | a viewer with session write | you | yes — one turn | as a message | a whole turn | | **Tick** (`update_canvas`) | you only | every viewer, ~50 ms | no | no | patch: tens · data: thousands | **The KV is for state, the action is for attention.** Never put a payload in an action when you can put an id there and the payload in the KV. Outbound costs output tokens, inbound costs a turn — the turn is the unit. Cheapest first: `canvas_kv(put)` one key · `update_canvas(patch)` a subtree · `draw_canvas(fromArtifact)` a filename · `update_canvas(data)` whole state · `draw_canvas(html)` the whole document. ## The KV store One row per key, per session, per slot. The page supplies only the key; the server stamps the writer (`by`) and the time (`at`). Keys: letters, digits, `. _ / -`, ≤200 chars. Values ≤16 KB; 1000 keys and 2 MB per canvas. Reserved prefixes: ``` app/… the app's shared state — notes, votes, the board req/<reqId> a request from the page to you (below) evt/<n> a note from you to the page (you write, viewers read) ui/<writerId> one viewer's ephemeral state (presence, drafts) — only theirs cfg/… you and the owner write, everyone reads ``` **Never keep a list in one key.** One key per item (`app/note/<uuid>`), one key per person (`app/vote/<me.id>`), a claim is `put` with `ifMatch: 0`, ordering is a field inside the item. Counters: per-person keys, count them. Free text several people edit: split into per-section keys and show `by`. Frame-rate state (a game board, live spectating) does not belong here. Your tool: `canvas_kv({op: get|put|list|delete, key, value, prefix, limit, after, ifMatch, slot, session_id})`. `ifMatch: 0` = create only; `ifMatch: N` = the current rev must be N. `list` pages by `after` (the last key of the previous page). `slot` picks the canvas (1-5, default 1); `session_id` targets an ANCESTOR session's canvas, as the other canvas tools do. ### Who may write Two switches, both needed. The **owner's policy** on the canvas (`owner` default | `readers` = anyone the session is read-shared with | `link`), set in the share dialog. And **your app's declaration** in the manifest: `"kv": { "write": "viewers" }`. Without both, only the owner, the session's writers, and you write. Rows are author-bound: a collaborator edits only rows they wrote unless you share the prefix (`"kv": { "write": "viewers", "shared": ["app/task/*"] }`). Everyone reads. Every write is attributed: `by.kind` is `user` (signed in), `link` (bearer — label unverified) or `agent`. ## The page side — paste this helper Nothing is injected into the document. Paste `CanvasKV()` into your page: ```js function CanvasKV() { const waiting = new Map(); let n = 0; const listeners = []; const ask = (msg, retryMs) => new Promise((res, rej) => { const id = ++n; waiting.set(id, { res, rej }); const send = () => parent.postMessage({ type: "canvas-kv", id, ...msg }, "*"); send(); // The host may not be listening yet on a cold open: re-post until answered. if (retryMs) { const t = setInterval(() => waiting.has(id) ? send() : clearInterval(t), retryMs); } }); window.addEventListener("message", (e) => { const m = e.data || {}; if (m.type === "canvas-kv-change") { listeners.forEach((f) => f(m)); return; } if (m.type !== "canvas-kv-result" && m.type !== "canvas-kv-ready") return; const w = waiting.get(m.id); if (!w) return; waiting.delete(m.id); m.ok ? w.res(m) : w.rej(Object.assign(new Error(m.error || "failed"), { code: m.code, rev: m.rev })); }); const listAll = async (prefix) => { // every key under a prefix, across pages let after = null, out = []; do { const r = await ask({ op: "list", prefix, after }); out = out.concat(r.entries); after = r.nextAfter; } while (after); return out; }; return { ready: ask({ op: "ready" }, 500), // → { me, canWrite, policy, entries (first 200) } listAll, // → [{ key, v, by, at, rev }] get: (key) => ask({ op: "get", key }).then((r) => r.entry), put: (key, value, ifMatch) => ask({ op: "put", key, value, ifMatch }), // → { rev } | rejects { code, rev } del: (key, ifMatch) => ask({ op: "delete", key, ifMatch }), onChange: (f) => listeners.push(f), // { key, rev, op, v?, by, at } }; } ``` ```js const kv = CanvasKV(); const { canWrite, me } = await kv.ready; renderAll(await kv.listAll("app/")); // list the app prefix — ready carries only the first 200 keys kv.onChange(async (c) => applyOne(c.v === undefined && c.op !== "delete" ? { ...c, v: (await kv.get(c.key))?.v } : c)); if (!canWrite) lockTheUI("You can view this board but not edit it."); setInterval(() => document.visibilityState === "visible" && kv.listAll("app/").then(renderAll), 15000); // resync if the live feed drops ``` `me` is `{ id, kind, label, relation, canWrite }`; `relation` is `owner | admin | collaborator | viewer | link`. **`me.id` is `provider/subject`** — it contains a slash, so a per-person key `app/vote/<me.id>` spans two segments (fine; list by prefix `app/vote/`). `label` is the person's display name (their email or subject when no name is known). Entries are `{ key, v, by, at, rev }` — `v` is what a page stored; `by` and `at` let you attribute and order. A `canvas-kv-change` may arrive **without `v`** (the value was too big to ride the ping): fetch the key, as above. `op: "delete"` drops the key. Rejections carry `code` — `FORBIDDEN`, `CONFLICT` (with the current `rev`, so a claim can show who won), `INVALID_KEY`, `TOO_LARGE`, `QUOTA`, `NOT_READY` (retry). Render from the KV, not from state baked into the document; preserve in-flight drafts across re-renders. New keys are open to every admitted writer; only overwrites are author-bound. "Only the owner may add" is a UI rule, not a server rule. ## Requests: the KV carries the payload, the action rings the doorbell ``` page put req/<id> { op, args, status: "queued" } every viewer sees it page canvas-action { action: "request", data: { id } } turn starts (if the viewer may ring) you canvas_kv get req/<id> → put { status: "working" } → put { status: "done", result } page renders the answer. No redraw. ``` Contract: `"responseContract": { "actions": { "request": { "id": "string" } } }`. Rows carry `{ op, args, status, result?, error? }` with `status` in `suggested | queued | working | done | failed`. **Only the owner and you decide what you act on. Everyone else suggests.** A collaborator's `req/*` row is capped to `status: "suggested"` by the server; the owner promotes it by writing `status: "queued"`. Your drain, on EVERY wake (a cron tick, a child completion, any turn): ``` canvas_kv list req/ → take every row with status "queued" (never "suggested") do the work, land each row in "done" or "failed" — never leave one "working" ``` The doorbell only shortens the wait; the status is the boundary. An app whose requests progress only while the owner has a tab open is broken for autonomous use: render `queued` honestly ("picked up on the next run"), and `suggested` as "waiting for the owner", never a spinner. Recovery after a memory gap: `canvas_kv list req/` (unfinished work), `canvas_kv list app/` (current state), `read_canvas(manifestOnly)` (the armed contract). This is why app state lives in the KV, not in chat. **Promotion is a decision, never a reflex.** Only the owner's ask or your own task moves KV content into an artifact, a shared fact, or an external write (ADO, a PR comment, email). Carry `by` across. Never promote link-written content into a shared fact without the owner saying so. ## Actions (the doorbell) ```js parent.postMessage({ type: "canvas-action", action: "request", data: { id } }, "*"); ``` The browser validates against your contract (no contract → nothing is accepted) and delivers conforming posts as `[canvas-action] {...}` user messages hidden from the chat pane. Answer on the canvas (a KV put or a patch), not with a redraw that wipes drafts. Actions are accepted only from a viewer with session write; everyone else's request lands `suggested`. Never BLOCK on a canvas control — the chat box is the fallback, and real approval gates stay on `ask_user`. Batch a whole form into ONE action with a `json` field. ## The manifest: the app's interface, for the agent who did not write it A canvas worth drawing twice is an app, and an app is driven by agents who never read its HTML — you after a context regeneration, and every other session that finds it in the catalog. The manifest comment right after `<!doctype html>` is the whole interface. Write it as if for a stranger: ```html <!-- CANVAS-APP-MANIFEST { "manifestVersion": 1, "name": "release-signoff", "version": "1.2.0", "description": "Use when several approvers sign off a release train together.", "tags": ["release", "approval", "collaborative"], "kv": { "write": "viewers", "shared": ["app/item/*"] }, "responseContract": { "actions": { "request": { "id": "string" } } }, "interface": { "keys": [ { "key": "app/item/<id>", "writer": "both", "shape": { "title": "Fix login redirect", "owner": "sam", "decision": "approve|reject|null", "note": "" }, "note": "One row per checklist item. The AGENT seeds them; approvers set decision and note." }, { "key": "cfg/deadline", "writer": "agent", "shape": "2026-09-01T17:00:00Z" }, { "key": "ui/<writerId>", "writer": "page", "shape": { "at": "iso" }, "note": "Presence heartbeat." } ], "requests": [ { "op": "seed", "args": { "items": [{ "id": "5502432", "title": "…", "owner": "…" }] }, "result": { "seeded": 12 }, "note": "Owner asks the agent to (re)load the checklist from the train." }, { "op": "store", "args": {}, "result": { "artifact": "signoff-<train>.md" }, "note": "Owner presses Store: the agent writes the decisions as a report artifact." } ], "events": [ { "key": "evt/banner", "shape": { "text": "Freeze at 17:00 UTC" }, "note": "Rendered at the top." } ], "notes": "Render everything from app/item/*. A row with decision null is pending. Count approvals by listing." } } --> ``` `interface.keys` says which KV keys exist, who writes them and what a value looks like. `interface.requests` says which `req/*` ops the page queues, with their args and what `result` the page renders when you land them. `interface.events` says which `evt/*` notes the page shows. Caps: 32 entries per list, 512 bytes per shape, 6 KB for the block. `description` is the text the catalog ranks on: say WHEN to use the app, not how it works. Re-learn any app with `read_canvas({manifestOnly: true})` — never by re-reading the bytes. `draw_canvas({fromArtifact})` hands you the same card. ## Publish and find ``` publish_canvas_app({ name: "release-signoff", description: "Use when …", tags: [...] }) → the canvas bytes → pinned artifact app-<name>.html on this session → the card (manifest + armed contract + source) → SHARED fact apps/<name> → every session can find it find_canvas_app({ query: "sign off a release" }) → ranked cards { key, name, description, tags, kv, source }
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub