Skip to content
Agentmart
Submit a tool

create-canvas-app

Build a GitHub Copilot App canvas extension the right way, fast.

Skill by jongio

IndexedMIT★ 18
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/jongio-create-canvas-app

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Build a GitHub Copilot App canvas extension the right way, fast. Use when the user wants to create, scaffold, or improve an interactive canvas (a side-panel UI the agent can open and drive) — dashboards, editors, trackers, boards, document/preview surfaces. Provides a no-build Preact + htm kit with live SSE state, durable per-user storage, Primer theming, and the official GitHub Lucide icon set, plus a generator that stamps a working canvas in one command. Do NOT use for non-canvas extensions (plain agent tools) or for shipping web apps unrelated to Copilot canvases.

Use cases: documents, design, agent-building

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found
Runs as
Node.js package

Network hosts it talks to 4

  • api.github.com · found in code
  • codeload.github.com · found in install command
  • github.com · found in code
  • jsonplaceholder.typicode.com · found in code

Files it touches

  • ~/.claude/skills/create-canvas-app
  • Your home directory (~)

What's inside

Skill name
create-canvas-app
Description
Build a GitHub Copilot App canvas extension the right way, fast. Use when the user wants to create, scaffold, or improve an interactive canvas (a side-panel UI the agent can open and drive) — dashboards, editors, trackers, boards, document/preview surfaces. Provides a no-build Preact + htm kit with live SSE state, durable per-user storage, Primer theming, and the official GitHub Lucide icon set, plus a generator that stamps a working canvas in one command. Do NOT use for non-canvas extensions (plain agent tools) or for shipping web apps unrelated to Copilot canvases.
License
MIT
Metadata
version: 1.0.0
Instructions
407 lines in SKILL.md · view on GitHub

Before installing

  • Ships executable scripts
  • Instructions call the network

Files (62)

  • .npmrc
  • .vally.yaml
  • LICENSE
  • README.md
  • SKILL.md
  • invoke.png · docs/
  • stock-ticker.png · docs/
  • README.md · evals/
  • eval.yaml · evals/create-canvas-app/
  • client.mjs · kit/
  • core-client.mjs · kit/
  • deeplinks.mjs · kit/
  • format.mjs · kit/
  • github-store.mjs · kit/
  • icons.mjs · kit/
  • net.mjs · kit/
  • server.mjs · kit/
  • storage.mjs · kit/
  • theme.css · kit/
  • validate.mjs · kit/
  • lucide.mjs · kit/vendor/
  • preact-htm-standalone.mjs · kit/vendor/
  • version.mjs · kit/
  • package-lock.json
  • package.json
  • .kit-version.json · reference/decision-log/canvas-kit/
  • client.mjs · reference/decision-log/canvas-kit/
  • core-client.mjs · reference/decision-log/canvas-kit/
  • deeplinks.mjs · reference/decision-log/canvas-kit/
  • format.mjs · reference/decision-log/canvas-kit/
  • github-store.mjs · reference/decision-log/canvas-kit/
  • icons.mjs · reference/decision-log/canvas-kit/
  • net.mjs · reference/decision-log/canvas-kit/
  • server.mjs · reference/decision-log/canvas-kit/
  • storage.mjs · reference/decision-log/canvas-kit/
  • theme.css · reference/decision-log/canvas-kit/
  • validate.mjs · reference/decision-log/canvas-kit/
  • lucide.mjs · reference/decision-log/canvas-kit/vendor/
  • preact-htm-standalone.mjs · reference/decision-log/canvas-kit/vendor/
  • version.mjs · reference/decision-log/canvas-kit/
  • canvas.mjs · reference/decision-log/
  • copilot-extension.json · reference/decision-log/
  • extension.mjs · reference/decision-log/
  • app.mjs · reference/decision-log/web/
  • index.html · reference/decision-log/web/
  • deeplinks.md · reference/
  • advanced-patterns.md · references/
  • check-kit-freshness.mjs · scripts/
  • install-local.ps1 · scripts/
  • kit-features.mjs · scripts/
  • new-canvas.mjs · scripts/
  • sync-kit.mjs · scripts/
  • vendor-lucide.mjs · scripts/
  • client.test.mjs · test/
  • deeplinks.test.mjs · test/
  • generator.test.mjs · test/
  • http.test.mjs · test/
  • kit-parity.test.mjs · test/
  • kit-runtime.test.mjs · test/
  • tooling.test.mjs · test/
  • …and 2 more

SKILL.md (first lines)

# Create Canvas App

## What a canvas is (and when to build one)

A canvas is an interactive surface the agent opens in a side panel via
`open_canvas`. Both the **agent** and the **user** act on the **same state**
through the **same action handlers**. Reach for a canvas when chat text or a diff
isn't enough: live dashboards, editors, spreadsheets, trackers, kanban/board
views, document previews, tool-specific workflows.

If the user just needs a non-visual agent tool, build a normal extension tool
instead — not a canvas.

## Rendering tier — decide first

| Tier | Use when | How |
| --- | --- | --- |
| **Static HTML string** | Read-only or near-static content; no inputs to protect | The runtime's vanilla scaffold (`extensions_manage scaffold kind:canvas`). |
| **Preact + htm + this kit** | Anything interactive: inputs, live updates, lists, forms, shared state | This kit. **Default choice for real canvases.** |

For the static tier, use the vanilla scaffold as generated and keep the HTML in
its single extension module. Do not copy `canvas-kit/` into that extension. Move
to a generated Preact template as soon as the canvas needs inputs, shared state,
agent actions, or live updates. A one-file interactive counter is not the upgrade
path because it duplicates action logic and keeps state per panel.

The single most important reason to use the kit: **Preact diffs the DOM**, so a
live state push from the agent does **not** clobber focus, caret position, or
half-typed text in an input. The `innerHTML = ...` pattern most early canvases
use repaints the whole tree and loses keystrokes on every push. Don't do that.

## The model (read this before coding)

```text
extension.mjs   ── the ONLY file that imports the Copilot SDK (thin adapter; also wires host AI)
canvas.mjs      ── your canvas: id, schema, state load/save, action handlers (SDK-free)
canvas-kit/     ── the template's feature-scoped kit (do not edit)
web/index.html  ── shell: loads /kit/theme.css and ./app.mjs
web/app.mjs     ── your Preact view
```

- **State is shared and durable.** It's keyed by a *domain id* resolved from the
  open input (`resolveDomainId`), **not** by `instanceId` — open the same domain
  in two panels and they show the same data. Persistence goes through
  `userStore(extName, file)` → `$COPILOT_HOME/extensions/<name>/artifacts/<domain>.json`.
  Two more tiers exist in `kit/storage.mjs` for non-durable/scoped state:
  `sessionStore(sessionId, extName, file)` (per-session scratch, discarded with
  the session) and `workspaceStore(workspacePath, file)` (rooted at the session
  workspace). All three write atomically (temp + rename) and **serialize
  concurrent saves to the same file**, so a racing agent + UI save can't corrupt
  or `EPERM` the durable file.
- **Shared, multiplayer state (optional).** For a board multiple *people* edit,
  swap the local tier for `githubStore({ owner, repo, path })` from
  `kit/github-store.mjs`: it persists the same JSON to a file in a (private) repo
  via the GitHub Contents API, so every collaborator with push access edits ONE
  document — GitHub is both the store and the access control. Wire it as
  `loadState`/`saveState`, and add `syncState: () => store.poll()` +
  `syncIntervalMs` so the runtime polls for other people's edits (cheap ETag `304`
  when unchanged) and adopts them live — but only while a panel is being viewed.
  Writes are optimistic-locked by blob SHA (a conflicting commit re-reads +
  retries; last-writer-wins by default, or pass a `merge(remote, mine)`). Token
  comes from `GH_TOKEN`/`GITHUB_TOKEN` or `gh auth token` and is only ever sent as
  an `Authorization` header.
- **Agent and UI share handlers.** An action invoked by the agent and the same
  action invoked from a button run the identical handler and produce the identical
  state mutation. Write the logic once, in `canvas.mjs`.
- **Live updates are automatic.** The kit serves `GET /state`, `GET /events`
  (SSE), and `POST /action`. `mountCanvas` wires them up; every state change fans
  out to all open panels.
- **`server.mjs` is SDK-free** so the whole runtime is testable with plain Node
  HTTP (see `test/http.test.mjs`). Keep SDK calls in `extension.mjs` only.

### Two kinds of state in the view — keep them separate

- **Shared domain state** arrives via SSE and is re-rendered on every push. Treat
  it as read-only in the view; change it only by calling `invoke(action, input)`.
- **Local UI state** (a draft input value, the active tab/filter) lives in
  `useState` and must never be pushed to the server until the user commits it.

### Action results & errors — how failures surface

Both callers (agent and UI) hit the same handler, and the runtime wraps every
invocation in the same envelope, so model failures deliberately:

- **Return** a small plain object for success (`{ id, status }`, `{ count }`).
  `POST /action` answers `{ ok: true, result }`.
- **Throw** for a hard failure (bad input, missing id). The runtime answers
  `{ ok: false, code, message }` — HTTP **400** for a known kit error (e.g. an
  unknown action) and **500** for a handler `throw` — and `mountCanvas`'s
  `invoke` **rejects** with a `CanvasActionError` carrying that `code`/`message`,
  so a button handler can `try/catch` it. On the agent side, `extension.mjs` maps
  the same error to a `CanvasError`. A throw is *surfaced*, never a crash.
- For an **expected, transient** failure (a flaky upstream fetch on a background
  poll), don't throw — capture it into `state.error` and **return** so the error
  card shows while the poll stays quiet (see the data template's `refresh`).

### Input & state are schema-validated at the boundary

An action's `inputSchema` is **enforced by the runtime**, not just declared: a
payload that violates it (wrong type, missing required field, out-of-enum value,
or an unexpected property under `additionalProperties: false`) is rejected with
an `invalid_input` error (**HTTP 400**) *before* your handler runs — from the
agent side too. So `inputSchema` is your typed contract; write it precisely and
your handler only sees well-shaped input. **Business rules still live in the
handler** (a schema-valid but empty `"   "` title reaches the handler, which
throws → 500). Open input is validated against `config.inputSchema` the same way.

Optionally set `config.stateSchema` (same JSON-Schema subset, `kit/validate.mjs`)
to guard the **durable shape**: if a handler produces state that violates it, the
mutation is rolled back and the action fails loud (500) instead of persisting
corrupt state. It's opt-in — omit it and any state shape is allowed.

## Icons — official GitHub Lucide, always

The kit vendors the **exact Lucide set github-app ships** (`lucide-react@1.23.0`,
byte-identical). Use it for *every* icon; do not hand-write SVGs or pull a CDN.

```js
import { Icon } from "/kit/icons.mjs";

From jongio/skills, MIT.

More from jongio/skills

Signals

Stars
18
Forks
5
Open issues
2
Last commit (this folder)
Oct 22, 2026 (5 days ago)

From GitHub (fetched Oct 24, 2026).

Details

Version
1.0.0
License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from jongio

Cite this page

create-canvas-app skill. Agent Mart. https://agentmart.dev/skills/jongio-create-canvas-app (last updated 2026-10-24)