@kolisachint/hoocode-agent 0.5.41 → 0.5.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../../src/core/canvas/scaffold.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authoring a canvas: the request a person types, the files it writes, and the\n * brief the model builds from.\n *\n * Design: `docs/canvas-extensions-design.md` §9 Phase 3, §13.\n *\n * This lives in `core/` rather than beside the other `/new-*` scaffolds because\n * `/new-canvas` stopped being a file-writing command. Copilot's `/create-canvas`\n * takes a sentence, has the agent write the extension, and opens it in a panel to\n * iterate on; matching that means the command has to reach the canvas session to\n * open, and the agent loop to build. Every decision that does not need either —\n * what the name is, where the file goes, what goes in it, what the model is told —\n * is here, testable without a terminal, a fork or a model.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { MarketplacePlatform } from \"../extensions/plugins/formats/types.js\";\nimport { CANVAS_ENTRY_FILE } from \"./discovery.js\";\n\n/**\n * Where a canvas extension lives, per platform.\n *\n * This does not go through `WorkspaceLayout` like the other scaffolds, and the\n * reason is that a canvas has no Claude convention to emit into: the surface\n * exists in Copilot and in hoocode's own `.agents/` tree and nowhere else. A\n * layout method returning nothing for one adapter would be a worse lie than\n * naming the two real homes here — these are exactly the roots\n * `core/canvas/discovery.ts` searches, which is what makes a scaffold live on the\n * next `/canvas`.\n */\nexport const CANVAS_HOMES: Partial<Record<MarketplacePlatform, string[]>> = {\n\tagents: [\".agents\", \"extensions\"],\n\tgithub: [\".github\", \"extensions\"],\n};\n\n/** Validates a canvas name: lowercase a-z, 0-9, hyphens, no leading/trailing/double hyphens. */\nexport function validateCanvasName(name: string): string | null {\n\tif (!name) return \"name is required\";\n\tif (!/^[a-z0-9-]+$/.test(name)) return \"name must be lowercase a-z, 0-9, and hyphens only\";\n\tif (name.startsWith(\"-\") || name.endsWith(\"-\")) return \"name must not start or end with a hyphen\";\n\tif (name.includes(\"--\")) return \"name must not contain consecutive hyphens\";\n\treturn null;\n}\n\n/**\n * Words dropped wherever they appear, because they carry no subject.\n *\n * Only grammar, never subject matter: a list that reached further would start\n * deciding which of someone's nouns mattered.\n */\nconst FILLER = new Set([\n\t\"a\",\n\t\"about\",\n\t\"across\",\n\t\"an\",\n\t\"and\",\n\t\"are\",\n\t\"as\",\n\t\"at\",\n\t\"by\",\n\t\"be\",\n\t\"can\",\n\t\"for\",\n\t\"from\",\n\t\"i\",\n\t\"in\",\n\t\"is\",\n\t\"into\",\n\t\"it\",\n\t\"its\",\n\t\"me\",\n\t\"my\",\n\t\"of\",\n\t\"on\",\n\t\"one\",\n\t\"or\",\n\t\"our\",\n\t\"over\",\n\t\"so\",\n\t\"that\",\n\t\"the\",\n\t\"their\",\n\t\"them\",\n\t\"then\",\n\t\"this\",\n\t\"to\",\n\t\"us\",\n\t\"we\",\n\t\"which\",\n\t\"via\",\n\t\"when\",\n\t\"where\",\n\t\"while\",\n\t\"with\",\n\t\"you\",\n\t\"your\",\n]);\n\n/**\n * Words that open a request rather than describe one.\n *\n * People type `/new-canvas` as an instruction — \"create a…\", \"build me a…\",\n * \"help me…\", \"I want to…\" — and the opening clause was landing in the directory\n * name: `create-lightweight-games`, `help-compare-two`, `want-review-pull`. On a\n * spread of twelve realistic descriptions, seven produced a name that named the\n * request instead of the thing.\n *\n * These are dropped anywhere, not only at the front, because \"add a canvas that\n * shows X\" buries one in the middle.\n */\nconst REQUEST_WORDS = new Set([\n\t\"add\",\n\t\"build\",\n\t\"could\",\n\t\"create\",\n\t\"design\",\n\t\"generate\",\n\t\"give\",\n\t\"help\",\n\t\"let\",\n\t\"make\",\n\t\"need\",\n\t\"new\",\n\t\"please\",\n\t\"produce\",\n\t\"set\",\n\t\"show\",\n\t\"something\",\n\t\"up\",\n\t\"want\",\n\t\"would\",\n]);\n\n/** Counting words: \"compare two benchmark runs\" is about the runs. */\nconst CARDINALS = new Set([\"two\", \"three\", \"four\", \"five\", \"six\", \"seven\", \"eight\", \"nine\", \"ten\"]);\n\n/** How many words a derived name keeps. Enough to be recognisable, short enough to type. */\nconst DERIVED_NAME_WORDS = 3;\n\n/**\n * Whether a word is probably a verb form rather than the thing being described.\n *\n * Crude on purpose — no part-of-speech tagger for a directory name. It exists\n * because a participle sits between the request and its subject and pushes the\n * subject out of a three-word name: \"a dashboard **showing** flaky tests\" became\n * `dashboard-showing-flaky`, and \"a spreadsheet for **tracking** API latency\"\n * became `spreadsheet-tracking-api`. The length floor spares short words where\n * the ending is a coincidence (`feed`, `used`, `ring`).\n */\nfunction looksLikeVerbForm(word: string): boolean {\n\treturn word.length > 4 && (word.endsWith(\"ing\") || word.endsWith(\"ed\"));\n}\n\n/**\n * Turn a sentence into a directory name.\n *\n * Returns undefined when nothing usable survives — an all-punctuation request, or\n * a sentence of nothing but filler — because inventing `canvas-1` would hide from\n * the person that we did not understand them.\n */\nexport function canvasNameFromDescription(description: string): string | undefined {\n\tconst words = description\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, \" \")\n\t\t.trim()\n\t\t.split(\" \")\n\t\t.filter((word) => word.length > 0);\n\n\tconst content = words.filter((word) => !FILLER.has(word) && !REQUEST_WORDS.has(word) && !CARDINALS.has(word));\n\t// \"canvas\" is dropped only when something else remains: \"/new-canvas a canvas\"\n\t// should still produce `canvas` rather than nothing.\n\tconst named = content.filter((word) => word !== \"canvas\");\n\tlet kept = named.length > 0 ? named : content;\n\n\t// Prefer nouns — but only while enough of them remain. Where they do not, the\n\t// participle *is* the subject (\"a canvas for onboarding\") and dropping it would\n\t// leave nothing worth naming.\n\tconst nouns = kept.filter((word) => !looksLikeVerbForm(word));\n\tif (nouns.length >= 2) kept = nouns;\n\n\tconst name = kept.slice(0, DERIVED_NAME_WORDS).join(\"-\");\n\treturn validateCanvasName(name) === null ? name : undefined;\n}\n\n/** What a person asked `/new-canvas` for. */\nexport interface CanvasRequest {\n\t/** Directory and default canvas id. */\n\tname: string;\n\t/**\n\t * What they want it to do, in their words, or undefined when they only named\n\t * one. Present means the model is expected to build it (Copilot's\n\t * `/create-canvas` shape); absent means they want the template to edit by hand.\n\t */\n\tdescription: string | undefined;\n}\n\n/**\n * Parse `/new-canvas`'s argument.\n *\n * Three shapes, in the order they are tested:\n *\n * - `my-board` — a bare name. Unchanged from before descriptions existed, and\n * tested first so it can never be re-read as a one-word description.\n * - `my-board: a kanban board for the release checklist` — both, when someone\n * cares what the directory is called. A colon rather than a flag because the\n * rest of the line is prose and a flag parser would have to guess where it ends.\n * - `a kanban board for the release checklist` — a description, the shape\n * `/create-canvas` uses. The name is derived and reported.\n *\n * Returns a string when the input cannot become a canvas; the caller shows it.\n */\nexport function parseCanvasRequest(input: string): CanvasRequest | string {\n\tconst trimmed = input.trim();\n\tif (trimmed.length === 0) return \"name or description is required\";\n\n\tif (validateCanvasName(trimmed) === null) return { name: trimmed, description: undefined };\n\n\tconst colon = trimmed.indexOf(\":\");\n\tif (colon > 0) {\n\t\tconst name = trimmed.slice(0, colon).trim();\n\t\tconst description = trimmed.slice(colon + 1).trim();\n\t\tif (validateCanvasName(name) === null && description.length > 0) return { name, description };\n\t}\n\n\tconst derived = canvasNameFromDescription(trimmed);\n\tif (!derived) {\n\t\treturn `could not derive a directory name from that. Give one explicitly: /new-canvas <name>: ${trimmed}`;\n\t}\n\treturn { name: derived, description: trimmed };\n}\n\n/**\n * A working single-canvas extension.\n *\n * Deliberately complete rather than a stub: a canvas has no passive half — its\n * name, its actions and its UI all come from running its code — so a scaffold\n * that does not run teaches nothing and cannot be checked with `/canvas open`.\n * This one opens, serves a page, answers an action, and closes cleanly, which is\n * the whole contract; everything past that is the author's.\n *\n * Shaped like the catalog extensions hoocode already hosts: the only import is\n * `@github/copilot-sdk/extension` (host-resolved — never installed, see\n * `docs/canvas-extensions-design.md` §4.1) plus `node:` builtins, and the UI is\n * served from an ephemeral loopback port behind a per-instance token so nothing\n * else on the machine can read it.\n */\nexport const CANVAS_ENTRY_TEMPLATE = (name: string): string => `\\\n// A canvas extension. Run it with: /canvas open ${name}\n//\n// The \"@github/copilot-sdk/extension\" import is resolved by the host at fork\n// time. Do not install it, and do not add a node_modules here.\nimport { createCanvas, CanvasError, joinSession } from \"@github/copilot-sdk/extension\";\nimport { randomBytes } from \"node:crypto\";\nimport { createServer } from \"node:http\";\n\n// The canvas's identity, in one place. An open instance is bound to this id, so\n// changing it drops the canvas anyone is currently looking at — rename the\n// canvas with \\`/canvas rename\\`, or change NAME alone if you only want a\n// different label on the page.\nconst ID = \"${name}\";\nconst NAME = \"${name}\";\n\n/** Per-instance state. A canvas can be opened more than once at a time. */\nconst instances = new Map();\n\nconst session = await joinSession({\n\tcanvases: [\n\t\tcreateCanvas({\n\t\t\tid: ID,\n\t\t\tdisplayName: NAME,\n\t\t\tdescription: \"TODO: one sentence — the agent reads this to decide whether to open it.\",\n\t\t\tactions: [\n\t\t\t\t{\n\t\t\t\t\tname: \"add_note\",\n\t\t\t\t\tdescription: \"TODO: describe what the agent can do to this canvas.\",\n\t\t\t\t\tinputSchema: {\n\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\tproperties: { text: { type: \"string\" } },\n\t\t\t\t\t\trequired: [\"text\"],\n\t\t\t\t\t},\n\t\t\t\t\thandler: (ctx) => {\n\t\t\t\t\t\tconst entry = instances.get(ctx.instanceId);\n\t\t\t\t\t\t// CanvasError carries a code the host shows verbatim; a bare\n\t\t\t\t\t\t// throw arrives as an opaque internal error instead.\n\t\t\t\t\t\tif (!entry) throw new CanvasError(\"no_instance\", \\`Instance \"\\${ctx.instanceId}\" is not open.\\`);\n\t\t\t\t\t\tentry.notes.push(ctx.input.text);\n\t\t\t\t\t\treturn { notes: entry.notes.length };\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t\topen: async (ctx) => {\n\t\t\t\tconst token = randomBytes(32).toString(\"base64url\");\n\t\t\t\tconst notes = [];\n\t\t\t\tconst server = createServer((req, res) => {\n\t\t\t\t\tconst url = new URL(req.url ?? \"/\", \"http://127.0.0.1\");\n\t\t\t\t\tif (url.searchParams.get(\"token\") !== token) {\n\t\t\t\t\t\tres.writeHead(403, { \"Content-Type\": \"text/plain\" });\n\t\t\t\t\t\tres.end(\"forbidden\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(\n\t\t\t\t\t\t\\`<!doctype html><meta charset=\"utf-8\"><title>\\${NAME}</title>\\` +\n\t\t\t\t\t\t\t\\`<p>\\${notes.length} note(s). TODO: build the UI.\\`,\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t\t// Port 0 on 127.0.0.1: an ephemeral port, reachable only from this machine.\n\t\t\t\tawait new Promise((resolve) => server.listen(0, \"127.0.0.1\", resolve));\n\t\t\t\tconst { port } = server.address();\n\t\t\t\tinstances.set(ctx.instanceId, { server, notes });\n\t\t\t\treturn {\n\t\t\t\t\turl: \\`http://127.0.0.1:\\${port}/?token=\\${token}\\`,\n\t\t\t\t\ttitle: NAME,\n\t\t\t\t};\n\t\t\t},\n\t\t\tonClose: async (ctx) => {\n\t\t\t\tconst entry = instances.get(ctx.instanceId);\n\t\t\t\t// Called for an abandoned open too, so the instance may be unknown.\n\t\t\t\tif (!entry) return;\n\t\t\t\tinstances.delete(ctx.instanceId);\n\t\t\t\tawait new Promise((resolve) => entry.server.close(resolve));\n\t\t\t},\n\t\t}),\n\t],\n});\n\n// stdout is the protocol channel. Use session.log, never console.log.\nawait session.log(\\`\\${ID} ready\\`);\n`;\n\n/** What {@link scaffoldCanvas} wrote. */\nexport interface CanvasScaffoldResult {\n\t/** Workspace-relative entry files created, one per platform target. */\n\tcreated: string[];\n\t/** Workspace-relative entry files that already existed and were left alone. */\n\tskipped: string[];\n}\n\n/**\n * Write the template into every platform target that has a canvas home.\n *\n * Existing files are never clobbered — they are reported and skipped, so running\n * `/new-canvas` twice on a canvas you have been editing cannot lose it.\n */\nexport function scaffoldCanvas(cwd: string, name: string, platforms: MarketplacePlatform[]): CanvasScaffoldResult {\n\tconst created: string[] = [];\n\tconst skipped: string[] = [];\n\tfor (const platform of platforms) {\n\t\tconst home = CANVAS_HOMES[platform];\n\t\tif (!home) continue;\n\t\tconst relative = join(...home, name, CANVAS_ENTRY_FILE);\n\t\tconst absolute = join(cwd, relative);\n\t\tif (existsSync(absolute)) {\n\t\t\tskipped.push(relative);\n\t\t\tcontinue;\n\t\t}\n\t\tmkdirSync(dirname(absolute), { recursive: true });\n\t\twriteFileSync(absolute, CANVAS_ENTRY_TEMPLATE(name), \"utf8\");\n\t\tcreated.push(relative);\n\t}\n\treturn { created, skipped };\n}\n\n/** Where a build brief's canvas is, if opening it worked. */\nexport interface CanvasBriefTarget {\n\tinstanceId: string;\n\turl: string | undefined;\n}\n\n/**\n * The message the model builds from — hoocode's half of `/create-canvas`.\n *\n * A scaffold plus a sentence is not a canvas, and the gap between them is the\n * agent's work. This is what turns \"a kanban board for the release checklist\"\n * into a build task with the contract attached, so the model does not have to\n * infer the rules of a surface it cannot see from a template it has not read.\n *\n * Four of those rules are stated because getting them wrong fails in ways whose\n * symptom does not name the cause: an installed dependency (the resolver already\n * provides the SDK, and a `node_modules` here is a §4.1 violation), a\n * `console.log` (corrupts the JSON-RPC channel and surfaces as \"non-protocol\n * stdout\"), a write without a reload (changes nothing at all, because the running\n * child was forked from the old bytes), and a renamed canvas id.\n *\n * That last one is the newest and was found the hard way, by building a canvas\n * with this command: the scaffold names the canvas after the directory, a model\n * that thinks of a better name renames the `id`, and the next reload drops the\n * instance the person is looking at — correctly, since the canvas it was opened\n * against no longer exists. `displayName` is the half they actually see, so it\n * is the half to change.\n */\nexport function canvasBuildBrief(\n\tname: string,\n\tdescription: string,\n\tentryPath: string,\n\ttarget: CanvasBriefTarget | undefined,\n): string {\n\tconst lines = [\n\t\t`/new-canvas: build a canvas extension named \"${name}\".`,\n\t\t\"\",\n\t\t\"What the person asked for, in their words:\",\n\t\t` ${description}`,\n\t\t\"\",\n\t\t`A working template is already at ${entryPath}. Edit it until it does what was asked.`,\n\t];\n\n\tif (target) {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\ttarget.url\n\t\t\t\t? `It is already open at ${target.url} (instance ${target.instanceId}), so the person is watching it as you work.`\n\t\t\t\t: `It is already open as instance ${target.instanceId}.`,\n\t\t\t`After every edit, call reload_canvas with extensionId \"${name}\". A write to the file changes nothing on its own — the running process was forked from the old code. Reloading hands back a NEW url; give it to the person, because the tab they have open dies with the old process.`,\n\t\t);\n\t} else {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t`It is not open — the person will run /canvas open ${name} when they want to look. Build it anyway; you can check it runs by reading it carefully rather than by opening it yourself.`,\n\t\t);\n\t}\n\n\tlines.push(\n\t\t\"\",\n\t\t\"The contract this surface runs under, which is not yours to change:\",\n\t\t'- The only non-`node:` import allowed is \"@github/copilot-sdk/extension\". The host resolves it when it forks the extension. Do not install anything, and do not add a package.json or node_modules in the extension directory.',\n\t\t\"- stdout is the JSON-RPC channel. Use `session.log(...)`; a `console.log` corrupts the protocol.\",\n\t\t\"- Do not change the canvas's `id` (the template holds it in `ID`). An open instance is bound to it, so renaming it drops the canvas the person is watching on your next reload. Change `NAME` for a nicer label — that is the one they see — and if the directory name itself is wrong, say so and let them run `/canvas rename`, which moves everything at once.\",\n\t\t\"- After a reload, read the action list it reports back. That is the host telling you which of your actions it can actually see, and it is the only confirmation an action you just wrote is really callable.\",\n\t\t\"- Serve the UI from the loopback server the template already starts. Keep the per-instance token check, and keep `onClose` shutting the server down — that is what stops a port outliving the session.\",\n\t\t\"- Everything in `actions: [...]` becomes callable by you through invoke_canvas_action once it is open, so give each action a real description and inputSchema. That list is how you drive the canvas; the person drives the same state through the page.\",\n\t\t\"\",\n\t\t\"Build the thing that was asked for, not a stub: leave no TODO behind, and make the page render real state rather than a placeholder.\",\n\t);\n\treturn lines.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../../src/core/canvas/scaffold.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authoring a canvas: the request a person types, the files it writes, and the\n * brief the model builds from.\n *\n * Design: `docs/canvas-extensions-design.md` §9 Phase 3, §13.\n *\n * This lives in `core/` rather than beside the other `/new-*` scaffolds because\n * `/new-canvas` stopped being a file-writing command. Copilot's `/create-canvas`\n * takes a sentence, has the agent write the extension, and opens it in a panel to\n * iterate on; matching that means the command has to reach the canvas session to\n * open, and the agent loop to build. Every decision that does not need either —\n * what the name is, where the file goes, what goes in it, what the model is told —\n * is here, testable without a terminal, a fork or a model.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { MarketplacePlatform } from \"../extensions/plugins/formats/types.js\";\nimport { CANVAS_ENTRY_FILE } from \"./discovery.js\";\n\n/**\n * Where a canvas extension lives, per platform.\n *\n * This does not go through `WorkspaceLayout` like the other scaffolds, and the\n * reason is that a canvas has no Claude convention to emit into: the surface\n * exists in Copilot and in hoocode's own `.agents/` tree and nowhere else. A\n * layout method returning nothing for one adapter would be a worse lie than\n * naming the two real homes here — these are exactly the roots\n * `core/canvas/discovery.ts` searches, which is what makes a scaffold live on the\n * next `/canvas`.\n */\nexport const CANVAS_HOMES: Partial<Record<MarketplacePlatform, string[]>> = {\n\tagents: [\".agents\", \"extensions\"],\n\tgithub: [\".github\", \"extensions\"],\n};\n\n/** Validates a canvas name: lowercase a-z, 0-9, hyphens, no leading/trailing/double hyphens. */\nexport function validateCanvasName(name: string): string | null {\n\tif (!name) return \"name is required\";\n\tif (!/^[a-z0-9-]+$/.test(name)) return \"name must be lowercase a-z, 0-9, and hyphens only\";\n\tif (name.startsWith(\"-\") || name.endsWith(\"-\")) return \"name must not start or end with a hyphen\";\n\tif (name.includes(\"--\")) return \"name must not contain consecutive hyphens\";\n\treturn null;\n}\n\n/**\n * Words dropped wherever they appear, because they carry no subject.\n *\n * Only grammar, never subject matter: a list that reached further would start\n * deciding which of someone's nouns mattered.\n */\nconst FILLER = new Set([\n\t\"a\",\n\t\"about\",\n\t\"across\",\n\t\"an\",\n\t\"and\",\n\t\"are\",\n\t\"as\",\n\t\"at\",\n\t\"by\",\n\t\"be\",\n\t\"can\",\n\t\"for\",\n\t\"from\",\n\t\"i\",\n\t\"in\",\n\t\"is\",\n\t\"into\",\n\t\"it\",\n\t\"its\",\n\t\"me\",\n\t\"my\",\n\t\"of\",\n\t\"on\",\n\t\"one\",\n\t\"or\",\n\t\"our\",\n\t\"over\",\n\t\"so\",\n\t\"that\",\n\t\"the\",\n\t\"their\",\n\t\"them\",\n\t\"then\",\n\t\"this\",\n\t\"to\",\n\t\"us\",\n\t\"we\",\n\t\"which\",\n\t\"via\",\n\t\"when\",\n\t\"where\",\n\t\"while\",\n\t\"with\",\n\t\"you\",\n\t\"your\",\n]);\n\n/**\n * Words that open a request rather than describe one.\n *\n * People type `/new-canvas` as an instruction — \"create a…\", \"build me a…\",\n * \"help me…\", \"I want to…\" — and the opening clause was landing in the directory\n * name: `create-lightweight-games`, `help-compare-two`, `want-review-pull`. On a\n * spread of twelve realistic descriptions, seven produced a name that named the\n * request instead of the thing.\n *\n * These are dropped anywhere, not only at the front, because \"add a canvas that\n * shows X\" buries one in the middle.\n */\nconst REQUEST_WORDS = new Set([\n\t\"add\",\n\t\"build\",\n\t\"could\",\n\t\"create\",\n\t\"design\",\n\t\"generate\",\n\t\"give\",\n\t\"help\",\n\t\"let\",\n\t\"make\",\n\t\"need\",\n\t\"new\",\n\t\"please\",\n\t\"produce\",\n\t\"set\",\n\t\"show\",\n\t\"something\",\n\t\"up\",\n\t\"want\",\n\t\"would\",\n]);\n\n/** Counting words: \"compare two benchmark runs\" is about the runs. */\nconst CARDINALS = new Set([\"two\", \"three\", \"four\", \"five\", \"six\", \"seven\", \"eight\", \"nine\", \"ten\"]);\n\n/** How many words a derived name keeps. Enough to be recognisable, short enough to type. */\nconst DERIVED_NAME_WORDS = 3;\n\n/**\n * Whether a word is probably a verb form rather than the thing being described.\n *\n * Crude on purpose — no part-of-speech tagger for a directory name. It exists\n * because a participle sits between the request and its subject and pushes the\n * subject out of a three-word name: \"a dashboard **showing** flaky tests\" became\n * `dashboard-showing-flaky`, and \"a spreadsheet for **tracking** API latency\"\n * became `spreadsheet-tracking-api`. The length floor spares short words where\n * the ending is a coincidence (`feed`, `used`, `ring`).\n */\nfunction looksLikeVerbForm(word: string): boolean {\n\treturn word.length > 4 && (word.endsWith(\"ing\") || word.endsWith(\"ed\"));\n}\n\n/**\n * Turn a sentence into a directory name.\n *\n * Returns undefined when nothing usable survives — an all-punctuation request, or\n * a sentence of nothing but filler — because inventing `canvas-1` would hide from\n * the person that we did not understand them.\n */\nexport function canvasNameFromDescription(description: string): string | undefined {\n\tconst words = description\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, \" \")\n\t\t.trim()\n\t\t.split(\" \")\n\t\t.filter((word) => word.length > 0);\n\n\tconst content = words.filter((word) => !FILLER.has(word) && !REQUEST_WORDS.has(word) && !CARDINALS.has(word));\n\t// \"canvas\" is dropped only when something else remains: \"/new-canvas a canvas\"\n\t// should still produce `canvas` rather than nothing.\n\tconst named = content.filter((word) => word !== \"canvas\");\n\tlet kept = named.length > 0 ? named : content;\n\n\t// Prefer nouns — but only while enough of them remain. Where they do not, the\n\t// participle *is* the subject (\"a canvas for onboarding\") and dropping it would\n\t// leave nothing worth naming.\n\tconst nouns = kept.filter((word) => !looksLikeVerbForm(word));\n\tif (nouns.length >= 2) kept = nouns;\n\n\tconst name = kept.slice(0, DERIVED_NAME_WORDS).join(\"-\");\n\treturn validateCanvasName(name) === null ? name : undefined;\n}\n\n/** What a person asked `/new-canvas` for. */\nexport interface CanvasRequest {\n\t/** Directory and default canvas id. */\n\tname: string;\n\t/**\n\t * What they want it to do, in their words, or undefined when they only named\n\t * one. Present means the model is expected to build it (Copilot's\n\t * `/create-canvas` shape); absent means they want the template to edit by hand.\n\t */\n\tdescription: string | undefined;\n}\n\n/**\n * Parse `/new-canvas`'s argument.\n *\n * Three shapes, in the order they are tested:\n *\n * - `my-board` — a bare name. Unchanged from before descriptions existed, and\n * tested first so it can never be re-read as a one-word description.\n * - `my-board: a kanban board for the release checklist` — both, when someone\n * cares what the directory is called. A colon rather than a flag because the\n * rest of the line is prose and a flag parser would have to guess where it ends.\n * - `a kanban board for the release checklist` — a description, the shape\n * `/create-canvas` uses. The name is derived and reported.\n *\n * Returns a string when the input cannot become a canvas; the caller shows it.\n */\nexport function parseCanvasRequest(input: string): CanvasRequest | string {\n\tconst trimmed = input.trim();\n\tif (trimmed.length === 0) return \"name or description is required\";\n\n\tif (validateCanvasName(trimmed) === null) return { name: trimmed, description: undefined };\n\n\tconst colon = trimmed.indexOf(\":\");\n\tif (colon > 0) {\n\t\tconst name = trimmed.slice(0, colon).trim();\n\t\tconst description = trimmed.slice(colon + 1).trim();\n\t\tif (validateCanvasName(name) === null && description.length > 0) return { name, description };\n\t}\n\n\tconst derived = canvasNameFromDescription(trimmed);\n\tif (!derived) {\n\t\treturn `could not derive a directory name from that. Give one explicitly: /new-canvas <name>: ${trimmed}`;\n\t}\n\treturn { name: derived, description: trimmed };\n}\n\n/**\n * A working single-canvas extension.\n *\n * Deliberately complete rather than a stub: a canvas has no passive half — its\n * name, its actions and its UI all come from running its code — so a scaffold\n * that does not run teaches nothing and cannot be checked with `/canvas open`.\n * This one opens, serves a page, answers an action, and closes cleanly, which is\n * the whole contract; everything past that is the author's.\n *\n * Shaped like the catalog extensions hoocode already hosts: the only import is\n * `@github/copilot-sdk/extension` (host-resolved — never installed, see\n * `docs/canvas-extensions-design.md` §4.1) plus `node:` builtins, and the UI is\n * served from an ephemeral loopback port behind a per-instance token so nothing\n * else on the machine can read it.\n */\nexport const CANVAS_ENTRY_TEMPLATE = (name: string): string => `\\\n// A canvas extension. Run it with: /canvas open ${name}\n//\n// The \"@github/copilot-sdk/extension\" import is resolved by the host at fork\n// time. Do not install it, and do not add a node_modules here.\nimport { createCanvas, CanvasError, joinSession } from \"@github/copilot-sdk/extension\";\nimport { randomBytes } from \"node:crypto\";\nimport { createServer } from \"node:http\";\n\n// The canvas's identity, in one place. An open instance is bound to this id, so\n// changing it drops the canvas anyone is currently looking at — rename the\n// canvas with \\`/canvas rename\\`, or change NAME alone if you only want a\n// different label on the page.\nconst ID = \"${name}\";\nconst NAME = \"${name}\";\n\n/** Per-instance state. A canvas can be opened more than once at a time. */\nconst instances = new Map();\n\n// Styling, in one chunk so it stays findable. Plain strings and concatenation\n// throughout this section rather than template literals: a backtick or a\n// dollar-brace in markup you are generating is an escaping hazard for no gain.\n// Nothing is loaded from a CDN — this page is served from 127.0.0.1 and works\n// with no network, which a remote script would throw away.\nconst CSS = [\n\t\":root{--ground:#f7f8fa;--surface:#fff;--ink:#16191f;--muted:#5f6875;--line:#e2e6ec;--accent:#3862c4;color-scheme:light}\",\n\t\"@media(prefers-color-scheme:dark){:root{--ground:#0f1216;--surface:#171b21;--ink:#e8ebf0;--muted:#9aa3b0;--line:#262c35;--accent:#7aa2f7;color-scheme:dark}}\",\n\t\"*{box-sizing:border-box}\",\n\t\"body{margin:0;background:var(--ground);color:var(--ink);font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif}\",\n\t\"main{max-width:44rem;margin:0 auto;padding:2rem 1.25rem 3rem;display:flex;flex-direction:column;gap:1.25rem}\",\n\t\"header{display:flex;flex-direction:column;gap:.25rem;border-bottom:1px solid var(--line);padding-bottom:1rem}\",\n\t\"h1{margin:0;font-size:1.3rem;letter-spacing:-.01em}\",\n\t\"p.sub{margin:0;color:var(--muted);font-size:.85rem}\",\n\t\"ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.5rem}\",\n\t\"li{background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:4px;padding:.6rem .8rem}\",\n\t\"li.new{animation:in .6s ease}\",\n\t\"@keyframes in{from{background:color-mix(in srgb,var(--accent) 18%,var(--surface))}to{background:var(--surface)}}\",\n\t\"@media(prefers-reduced-motion:reduce){li.new{animation:none}}\",\n\t\"p.empty{margin:0;color:var(--muted);background:var(--surface);border:1px dashed var(--line);border-radius:4px;padding:1.25rem;text-align:center}\",\n].join(\"\");\n\n// Polls /state so state the agent changed appears without anyone reloading. It\n// only updates what the server already rendered, so the page is correct before\n// this runs and stays correct if it never does.\nconst CLIENT = [\n\t\"var seen=-1;\",\n\t\"async function tick(){\",\n\t\" try{\",\n\t\" var s=await (await fetch('/state'+location.search)).json();\",\n\t\" if(seen<0)seen=s.notes.length;\",\n\t\" var list=document.getElementById('list');\",\n\t\" document.getElementById('count').textContent=s.notes.length+' note(s)';\",\n\t\" document.getElementById('empty').hidden=s.notes.length>0;\",\n\t\" list.textContent='';\",\n\t\" s.notes.forEach(function(n,i){\",\n\t\" var li=document.createElement('li');\",\n\t\" li.textContent=n;\",\n\t\" if(i>=seen)li.className='new';\",\n\t\" list.appendChild(li);\",\n\t\" });\",\n\t\" seen=s.notes.length;\",\n\t\" }catch(e){}\",\n\t\" setTimeout(tick,1000);\",\n\t\"}tick();\",\n].join(\"\");\n\n/** Escape anything that came from state before it goes into markup. */\nconst esc = (s) =>\n\tString(s).replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\");\n\n/**\n * The served page, rendered from state on the server.\n *\n * Rendered here rather than left to the poll so the page is right the moment it\n * loads, and stays right with scripting off. CLIENT only updates it.\n */\nconst page = (name, notes) =>\n\t'<!doctype html><meta charset=\"utf-8\">' +\n\t'<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">' +\n\t\"<title>\" +\n\tesc(name) +\n\t\"</title><style>\" +\n\tCSS +\n\t\"</style>\" +\n\t\"<main><header><h1>\" +\n\tesc(name) +\n\t\"</h1>\" +\n\t'<p class=\"sub\"><span id=\"count\">' +\n\tnotes.length +\n\t\" note(s)</span>. What is below changes when the agent acts.</p></header>\" +\n\t'<ul id=\"list\">' +\n\tnotes\n\t\t.map(function (n) {\n\t\t\treturn \"<li>\" + esc(n) + \"</li>\";\n\t\t})\n\t\t.join(\"\") +\n\t\"</ul>\" +\n\t'<p class=\"empty\" id=\"empty\"' +\n\t(notes.length > 0 ? \" hidden\" : \"\") +\n\t\">Nothing yet. TODO: replace this with the real UI.</p>\" +\n\t\"</main><script>\" +\n\tCLIENT +\n\t\"</script>\";\n\nconst session = await joinSession({\n\tcanvases: [\n\t\tcreateCanvas({\n\t\t\tid: ID,\n\t\t\tdisplayName: NAME,\n\t\t\tdescription: \"TODO: one sentence — the agent reads this to decide whether to open it.\",\n\t\t\tactions: [\n\t\t\t\t{\n\t\t\t\t\tname: \"add_note\",\n\t\t\t\t\tdescription: \"TODO: describe what the agent can do to this canvas.\",\n\t\t\t\t\tinputSchema: {\n\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\tproperties: { text: { type: \"string\" } },\n\t\t\t\t\t\trequired: [\"text\"],\n\t\t\t\t\t},\n\t\t\t\t\thandler: (ctx) => {\n\t\t\t\t\t\tconst entry = instances.get(ctx.instanceId);\n\t\t\t\t\t\t// CanvasError carries a code the host shows verbatim; a bare\n\t\t\t\t\t\t// throw arrives as an opaque internal error instead.\n\t\t\t\t\t\tif (!entry) throw new CanvasError(\"no_instance\", \\`Instance \"\\${ctx.instanceId}\" is not open.\\`);\n\t\t\t\t\t\tentry.notes.push(ctx.input.text);\n\t\t\t\t\t\treturn { notes: entry.notes.length };\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t\topen: async (ctx) => {\n\t\t\t\tconst token = randomBytes(32).toString(\"base64url\");\n\t\t\t\tconst notes = [];\n\t\t\t\tconst server = createServer((req, res) => {\n\t\t\t\t\tconst url = new URL(req.url ?? \"/\", \"http://127.0.0.1\");\n\t\t\t\t\tif (url.searchParams.get(\"token\") !== token) {\n\t\t\t\t\t\tres.writeHead(403, { \"Content-Type\": \"text/plain\" });\n\t\t\t\t\t\tres.end(\"forbidden\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// The page renders from this, and so does the poll. One source of\n\t\t\t\t\t// truth means state the agent changed shows up without a reload.\n\t\t\t\t\tif (url.pathname === \"/state\") {\n\t\t\t\t\t\tres.writeHead(200, { \"Content-Type\": \"application/json\" });\n\t\t\t\t\t\tres.end(JSON.stringify({ notes }));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(page(NAME, notes));\n\t\t\t\t});\n\t\t\t\t// Port 0 on 127.0.0.1: an ephemeral port, reachable only from this machine.\n\t\t\t\tawait new Promise((resolve) => server.listen(0, \"127.0.0.1\", resolve));\n\t\t\t\tconst { port } = server.address();\n\t\t\t\tinstances.set(ctx.instanceId, { server, notes });\n\t\t\t\treturn {\n\t\t\t\t\turl: \\`http://127.0.0.1:\\${port}/?token=\\${token}\\`,\n\t\t\t\t\ttitle: NAME,\n\t\t\t\t};\n\t\t\t},\n\t\t\tonClose: async (ctx) => {\n\t\t\t\tconst entry = instances.get(ctx.instanceId);\n\t\t\t\t// Called for an abandoned open too, so the instance may be unknown.\n\t\t\t\tif (!entry) return;\n\t\t\t\tinstances.delete(ctx.instanceId);\n\t\t\t\tawait new Promise((resolve) => entry.server.close(resolve));\n\t\t\t},\n\t\t}),\n\t],\n});\n\n// stdout is the protocol channel. Use session.log, never console.log.\nawait session.log(\\`\\${ID} ready\\`);\n`;\n\n/** What {@link scaffoldCanvas} wrote. */\nexport interface CanvasScaffoldResult {\n\t/** Workspace-relative entry files created, one per platform target. */\n\tcreated: string[];\n\t/** Workspace-relative entry files that already existed and were left alone. */\n\tskipped: string[];\n}\n\n/**\n * Write the template into every platform target that has a canvas home.\n *\n * Existing files are never clobbered — they are reported and skipped, so running\n * `/new-canvas` twice on a canvas you have been editing cannot lose it.\n */\nexport function scaffoldCanvas(cwd: string, name: string, platforms: MarketplacePlatform[]): CanvasScaffoldResult {\n\tconst created: string[] = [];\n\tconst skipped: string[] = [];\n\tfor (const platform of platforms) {\n\t\tconst home = CANVAS_HOMES[platform];\n\t\tif (!home) continue;\n\t\tconst relative = join(...home, name, CANVAS_ENTRY_FILE);\n\t\tconst absolute = join(cwd, relative);\n\t\tif (existsSync(absolute)) {\n\t\t\tskipped.push(relative);\n\t\t\tcontinue;\n\t\t}\n\t\tmkdirSync(dirname(absolute), { recursive: true });\n\t\twriteFileSync(absolute, CANVAS_ENTRY_TEMPLATE(name), \"utf8\");\n\t\tcreated.push(relative);\n\t}\n\treturn { created, skipped };\n}\n\n/** Where a build brief's canvas is, if opening it worked. */\nexport interface CanvasBriefTarget {\n\tinstanceId: string;\n\turl: string | undefined;\n}\n\n/**\n * The message the model builds from — hoocode's half of `/create-canvas`.\n *\n * A scaffold plus a sentence is not a canvas, and the gap between them is the\n * agent's work. This is what turns \"a kanban board for the release checklist\"\n * into a build task with the contract attached, so the model does not have to\n * infer the rules of a surface it cannot see from a template it has not read.\n *\n * Four of those rules are stated because getting them wrong fails in ways whose\n * symptom does not name the cause: an installed dependency (the resolver already\n * provides the SDK, and a `node_modules` here is a §4.1 violation), a\n * `console.log` (corrupts the JSON-RPC channel and surfaces as \"non-protocol\n * stdout\"), a write without a reload (changes nothing at all, because the running\n * child was forked from the old bytes), and a renamed canvas id.\n *\n * That last one is the newest and was found the hard way, by building a canvas\n * with this command: the scaffold names the canvas after the directory, a model\n * that thinks of a better name renames the `id`, and the next reload drops the\n * instance the person is looking at — correctly, since the canvas it was opened\n * against no longer exists. `displayName` is the half they actually see, so it\n * is the half to change.\n */\nexport function canvasBuildBrief(\n\tname: string,\n\tdescription: string,\n\tentryPath: string,\n\ttarget: CanvasBriefTarget | undefined,\n\tguidePath?: string,\n): string {\n\tconst lines = [\n\t\t`/new-canvas: build a canvas extension named \"${name}\".`,\n\t\t\"\",\n\t\t\"What the person asked for, in their words:\",\n\t\t` ${description}`,\n\t\t\"\",\n\t\t`A working template is already at ${entryPath}. Edit it until it does what was asked.`,\n\t];\n\n\t// The craft half of this brief. It is a skill hidden from the per-turn skill\n\t// list precisely so it costs nothing until a canvas is actually being built,\n\t// which makes this line the only thing that surfaces it.\n\tif (guidePath) {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t`Read ${guidePath} before you design the page. The template renders an unstyled placeholder on purpose; that guide covers what the surface needs — the markup lives in a template string, no dependencies and no build are available, nothing about the theme is inherited, and the page has to render state that you and the person both change.`,\n\t\t);\n\t}\n\n\tif (target) {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\ttarget.url\n\t\t\t\t? `It is already open at ${target.url} (instance ${target.instanceId}), so the person is watching it as you work.`\n\t\t\t\t: `It is already open as instance ${target.instanceId}.`,\n\t\t\t`After every edit, call reload_canvas with extensionId \"${name}\". A write to the file changes nothing on its own — the running process was forked from the old code. Reloading hands back a NEW url; give it to the person, because the tab they have open dies with the old process.`,\n\t\t);\n\t} else {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t`It is not open — the person will run /canvas open ${name} when they want to look. Build it anyway; you can check it runs by reading it carefully rather than by opening it yourself.`,\n\t\t);\n\t}\n\n\tlines.push(\n\t\t\"\",\n\t\t\"The contract this surface runs under, which is not yours to change:\",\n\t\t'- The only non-`node:` import allowed is \"@github/copilot-sdk/extension\". The host resolves it when it forks the extension. Do not install anything, and do not add a package.json or node_modules in the extension directory.',\n\t\t\"- stdout is the JSON-RPC channel. Use `session.log(...)`; a `console.log` corrupts the protocol.\",\n\t\t\"- Do not change the canvas's `id` (the template holds it in `ID`). An open instance is bound to it, so renaming it drops the canvas the person is watching on your next reload. Change `NAME` for a nicer label — that is the one they see — and if the directory name itself is wrong, say so and let them run `/canvas rename`, which moves everything at once.\",\n\t\t\"- After a reload, read the action list it reports back. That is the host telling you which of your actions it can actually see, and it is the only confirmation an action you just wrote is really callable.\",\n\t\t\"- Serve the UI from the loopback server the template already starts. Keep the per-instance token check, and keep `onClose` shutting the server down — that is what stops a port outliving the session.\",\n\t\t\"- Everything in `actions: [...]` becomes callable by you through invoke_canvas_action once it is open, so give each action a real description and inputSchema. That list is how you drive the canvas; the person drives the same state through the page.\",\n\t\t\"\",\n\t\t\"Build the thing that was asked for, not a stub: leave no TODO behind, and make the page render real state rather than a placeholder.\",\n\t);\n\treturn lines.join(\"\\n\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/canvas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAYH,OAAO,EAGN,KAAK,oBAAoB,EAEzB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,KAAK,EAAE,YAAY,EAA2B,MAAM,gCAAgC,CAAC;AAwD5F;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;CACxD;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAuapF","sourcesContent":["/**\n * `/canvas` — the interactive surface for canvas extensions.\n *\n * Design: `docs/canvas-extensions-design.md` §11. Deliberately thin: every decision\n * lives in `core/canvas/session.ts`, which is testable without a terminal, so this\n * file only renders and supplies an `AbortSignal`.\n *\n * The signal is the point of the loader. `BorderedLoader` already gives Esc-to-cancel\n * and exposes an `AbortSignal`, and `registry.open` accepts one — so a person's Esc\n * reaches the abandon path (§11.6) and the extension is told to release the port it\n * may already have bound, rather than the spinner merely disappearing.\n *\n * `/new-canvas` is registered here rather than beside `/new-skill` and friends\n * because it is not a file-writing command any more: it opens what it scaffolds\n * and hands the agent a brief to build it, which needs this file's session and\n * `pi.sendUserMessage`. Its decisions live in `core/canvas/scaffold.ts`.\n *\n * The agent tools register on the first successful open and stay for the session:\n * `registerTool` has no counterpart to remove a tool. So a session that never opens a\n * canvas pays nothing for them, which is the case that matters (§11.5); after the\n * first open they cost ~235 tokens and answer honestly when nothing is open.\n */\n\nimport { homedir } from \"node:os\";\nimport { getAgentDir } from \"../../config.js\";\nimport { CATEGORY_GLYPH } from \"../../core/brand.js\";\n\n/** Canvas extensions are extensions, so they wear the extension glyph. */\nconst GLYPH = CATEGORY_GLYPH.extensions;\n\nimport { isCanvasRefusal } from \"../../core/canvas/lifecycle.js\";\nimport type { CanvasInstance } from \"../../core/canvas/registry.js\";\nimport { CANVAS_HOMES, canvasBuildBrief, parseCanvasRequest, scaffoldCanvas } from \"../../core/canvas/scaffold.js\";\nimport {\n\ttype CanvasOverview,\n\tCanvasSession,\n\ttype CanvasSessionOptions,\n\tparseCanvasRef,\n} from \"../../core/canvas/session.js\";\nimport { getWorkspacePlatforms } from \"../../core/extensions/plugins/formats/platform-targets.js\";\nimport { isWorkspaceTrusted, trustWorkspace } from \"../../core/extensions/plugins/trust.js\";\nimport type { ExtensionAPI, ExtensionCommandContext } from \"../../core/extensions/types.js\";\nimport { createCanvasToolDefinitions } from \"../../core/tools/canvas.js\";\nimport { BorderedLoader } from \"../../modes/interactive/components/bordered-loader.js\";\n\nconst SUBCOMMANDS = [\"list\", \"open\", \"close\", \"reload\", \"rename\", \"remove\"] as const;\n\n/** How an open attempt ended. `custom()` resolves with exactly one of these. */\ntype OpenOutcome =\n\t| { kind: \"opened\"; instance: CanvasInstance }\n\t| { kind: \"failed\"; message: string }\n\t| { kind: \"cancelled\" };\n\nfunction describeInstance(instance: CanvasInstance): string {\n\tconst title = instance.title ?? instance.canvasId;\n\treturn `${instance.instanceId} ${title}${instance.url ? ` ${instance.url}` : \"\"}`;\n}\n\nfunction renderOverview(overview: CanvasOverview): string {\n\tconst lines: string[] = [];\n\tif (!overview.availability.available) {\n\t\tlines.push(`Canvases are unavailable: ${overview.availability.reason}`, \"\");\n\t}\n\tif (overview.listings.length === 0) {\n\t\tlines.push(\n\t\t\t\"No canvas extensions found in .agents/extensions, .github/extensions, ~/.copilot/extensions,\",\n\t\t\t\"or any installed plugin. Create one with /new-canvas <what it should do>, or install one with /plugin.\",\n\t\t);\n\t\treturn lines.join(\"\\n\");\n\t}\n\tfor (const listing of overview.listings) {\n\t\tconst name = listing.canvasId ? `${listing.extensionId}:${listing.canvasId}` : listing.extensionId;\n\t\tconst label = listing.displayName ? ` ${listing.displayName}` : \"\";\n\t\tif (listing.withheld === \"untrusted-workspace\") {\n\t\t\tlines.push(`${GLYPH} ${name}${label} [withheld: untrusted workspace]`);\n\t\t\tcontinue;\n\t\t}\n\t\tlines.push(`${GLYPH} ${name}${label} (${listing.scope})`);\n\t\tfor (const instance of listing.open) {\n\t\t\tlines.push(` open ${describeInstance(instance)}`);\n\t\t\t// What a canvas can do is otherwise visible only to the model, through\n\t\t\t// `list_canvas_capabilities` — so the person driving the session could not\n\t\t\t// see the surface they were being asked about. Only for open instances,\n\t\t\t// because actions come from running the code (§5.1).\n\t\t\tconst actions = overview.actionsByInstance.get(instance.instanceId) ?? [];\n\t\t\tif (actions.length > 0) lines.push(` actions ${actions.join(\", \")}`);\n\t\t}\n\t}\n\tif (overview.withheldCount > 0) {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t`${overview.withheldCount} extension(s) came with this repository and are withheld. Run /plugin trust to allow this directory to run code it ships.`,\n\t\t);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Test seams, and only that.\n *\n * `/new-canvas` opens what it writes, so driving it without a terminal needs a\n * runtime that does not depend on hoocode having been built, and a home\n * directory that is not the developer's. Everything else this file does is\n * decided in `core/canvas/`, where it is testable without any of this.\n */\nexport interface CanvasSetupOverrides {\n\thomeDir?: string;\n\tresolveRuntime?: CanvasSessionOptions[\"resolveRuntime\"];\n}\n\nexport function setupCanvas(pi: ExtensionAPI, overrides?: CanvasSetupOverrides): void {\n\tlet session: CanvasSession | undefined;\n\tlet toolsRegistered = false;\n\t/**\n\t * Points at the most recent command's UI.\n\t *\n\t * A canvas keeps talking after the command that opened it has returned — logs,\n\t * stray stdout, a leaked-port warning — so the callbacks cannot close over one\n\t * invocation's `ctx`.\n\t */\n\tlet notify: (message: string, type?: \"info\" | \"warning\" | \"error\") => void = () => {};\n\n\tconst ensureSession = (ctx: ExtensionCommandContext): CanvasSession => {\n\t\tnotify = (message, type) => ctx.ui.notify(message, type);\n\t\tsession ??= new CanvasSession({\n\t\t\tcwd: ctx.cwd,\n\t\t\thomeDir: overrides?.homeDir ?? homedir(),\n\t\t\tagentDir: getAgentDir(),\n\t\t\tresolveRuntime: overrides?.resolveRuntime,\n\t\t\t// A canvas's own diagnostics are the user's business: a stray stdout line means\n\t\t\t// its author reached for console.log, and a possible leaked port is worth saying.\n\t\t\tonLog: (id, message) => notify(`[canvas ${id}] ${message}`, \"info\"),\n\t\t\tonStray: (id, line) => notify(`[canvas ${id}] non-protocol stdout (use session.log): ${line}`, \"warning\"),\n\t\t\tonDiagnostic: (id, message) => notify(`[canvas ${id}] ${message}`, \"warning\"),\n\t\t});\n\t\treturn session;\n\t};\n\n\tconst registerToolsOnce = (canvas: CanvasSession): void => {\n\t\tif (toolsRegistered) return;\n\t\tconst registry = canvas.registryOrUndefined();\n\t\tif (!registry) return;\n\t\ttoolsRegistered = true;\n\t\tfor (const definition of createCanvasToolDefinitions(registry)) pi.registerTool(definition);\n\t};\n\n\t/**\n\t * Open a canvas behind a cancellable loader.\n\t *\n\t * Shared by `/canvas open` and `/new-canvas`, which want identical behaviour:\n\t * opening forks a process and binds a port, so it can be slow and must be\n\t * interruptible. The loader's signal is what makes Esc mean something — it\n\t * reaches the registry's abandon path, which tells the extension to release a\n\t * port it may already have bound. Outside a terminal (--print, RPC) there is\n\t * nothing to draw and nothing to press, so the open simply runs.\n\t */\n\tconst openWithLoader = async (\n\t\tcanvas: CanvasSession,\n\t\tref: { extensionId: string; canvasId?: string },\n\t\tctx: ExtensionCommandContext,\n\t): Promise<OpenOutcome | undefined> =>\n\t\tctx.hasUI\n\t\t\t? await ctx.ui.custom<OpenOutcome | undefined>((tui, theme, _keybindings, done) => {\n\t\t\t\t\tconst loader = new BorderedLoader(tui, theme, `Opening ${ref.extensionId}…`);\n\t\t\t\t\tloader.onAbort = () => done({ kind: \"cancelled\" });\n\t\t\t\t\tvoid canvas\n\t\t\t\t\t\t.open(ref, { signal: loader.signal })\n\t\t\t\t\t\t.then((instance) => done({ kind: \"opened\", instance }))\n\t\t\t\t\t\t// Cancelling races: the signal rejects the pending call at the same moment\n\t\t\t\t\t\t// onAbort fires, and whichever lands first resolves `custom`. Deciding from\n\t\t\t\t\t\t// the signal rather than from who won means a cancel always reads as a\n\t\t\t\t\t\t// cancel instead of surfacing as an error.\n\t\t\t\t\t\t.catch((error: unknown) =>\n\t\t\t\t\t\t\tdone(\n\t\t\t\t\t\t\t\tloader.signal.aborted\n\t\t\t\t\t\t\t\t\t? { kind: \"cancelled\" }\n\t\t\t\t\t\t\t\t\t: { kind: \"failed\", message: error instanceof Error ? error.message : String(error) },\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\treturn loader;\n\t\t\t\t})\n\t\t\t: await canvas\n\t\t\t\t\t.open(ref)\n\t\t\t\t\t.then((instance): OpenOutcome => ({ kind: \"opened\", instance }))\n\t\t\t\t\t.catch(\n\t\t\t\t\t\t(error: unknown): OpenOutcome => ({\n\t\t\t\t\t\t\tkind: \"failed\",\n\t\t\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\n\tpi.registerCommand(\"canvas\", {\n\t\tdescription:\n\t\t\t\"Work with canvas extensions. /canvas list | open <extension>[:<canvas>] | reload [extension] | close <instanceId> | rename <extension> <new-name> | remove <extension>\",\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\tSUBCOMMANDS.filter((name) => name.startsWith(prefix)).map((name) => ({ value: name, label: name })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tconst canvas = ensureSession(ctx);\n\n\t\t\tif (trimmed.length === 0 || trimmed === \"list\") {\n\t\t\t\tctx.ui.notify(renderOverview(await canvas.list()), \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"close\")) {\n\t\t\t\tconst instanceId = trimmed.slice(\"close\".length).trim();\n\t\t\t\tif (!instanceId) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /canvas close <instanceId> (see /canvas list)\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst closed = await canvas.close(instanceId);\n\t\t\t\tif (!closed) ctx.ui.notify(`No open canvas instance \"${instanceId}\".`, \"warning\");\n\t\t\t\telse ctx.ui.notify(`Closed ${closed.canvasId} (${closed.instanceId}).`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"reload\")) {\n\t\t\t\t// A reload re-forks the extension, so it can take as long as an open and is\n\t\t\t\t// worth being able to abandon — but unlike an open there is nothing to\n\t\t\t\t// abandon *to*: the registry keeps the old child serving until the new one\n\t\t\t\t// answers, so a cancel here just stops waiting.\n\t\t\t\tconst requested = trimmed.slice(\"reload\".length).trim();\n\t\t\t\tconst running = canvas.runningExtensionIds();\n\t\t\t\tconst extensionId = requested || (running.length === 1 ? running[0] : undefined);\n\t\t\t\tif (!extensionId) {\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\trunning.length === 0\n\t\t\t\t\t\t\t? \"Nothing is open, so there is nothing to reload. Open a canvas first with /canvas open <extension>.\"\n\t\t\t\t\t\t\t: `Several extensions are open; name one: ${running.join(\", \")}.`,\n\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await canvas.reload(extensionId);\n\t\t\t\t\tconst lines = [`Reloaded ${result.extensionId} from disk.`];\n\t\t\t\t\t// Same reason the tool reports it: an edit to `actions: [...]` is\n\t\t\t\t\t// otherwise invisible, and a typo there fails by the action simply\n\t\t\t\t\t// not being there.\n\t\t\t\t\tconst { added, removed, changed } = result.actions;\n\t\t\t\t\tif (added.length > 0) lines.push(` + ${added.join(\", \")}`);\n\t\t\t\t\tif (removed.length > 0) lines.push(` - ${removed.join(\", \")}`);\n\t\t\t\t\tif (changed.length > 0) lines.push(` ~ ${changed.join(\", \")} (description or schema)`);\n\t\t\t\t\tfor (const instance of result.reopened) {\n\t\t\t\t\t\t// The url is the point of saying anything: the extension binds a new\n\t\t\t\t\t\t// port and mints a new token on every open, so the tab the person has\n\t\t\t\t\t\t// in front of them is now pointing at a closed port.\n\t\t\t\t\t\tlines.push(` ${describeInstance(instance)}`);\n\t\t\t\t\t}\n\t\t\t\t\tif (result.reopened.length > 0) lines.push(\"\", \"Open the new url(s) — the previous tab is dead.\");\n\t\t\t\t\tfor (const drop of result.dropped) {\n\t\t\t\t\t\tlines.push(` ${drop.canvasId} (${drop.instanceId}) did not come back: ${drop.reason}`);\n\t\t\t\t\t}\n\t\t\t\t\tctx.ui.notify(lines.join(\"\\n\"), result.dropped.length > 0 ? \"warning\" : \"info\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// The registry only swaps children once the new one is ready, so the\n\t\t\t\t\t// canvas the person is looking at survived this. Say so, or they will\n\t\t\t\t\t// think they just lost it.\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t`Reloading ${extensionId} failed, so it is still running the code it was started with: ` +\n\t\t\t\t\t\t\t`${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t\t\t\"error\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"rename\")) {\n\t\t\t\tconst [from, to, ...rest] = trimmed.slice(\"rename\".length).trim().split(/\\s+/).filter(Boolean);\n\t\t\t\tif (!from || !to || rest.length > 0) {\n\t\t\t\t\tctx.ui.notify(`Usage: /canvas rename <extension> <new-name> (see /canvas list)`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Renaming closes whatever the extension had open, because the directory\n\t\t\t\t// is about to move out from under it. Say so before doing it, not after.\n\t\t\t\tconst wasOpen = canvas.instances().filter((instance) => instance.extensionId === from);\n\t\t\t\tconst result = await canvas.rename(from, to);\n\t\t\t\tif (isCanvasRefusal(result)) {\n\t\t\t\t\tctx.ui.notify(`/canvas rename: ${result.detail}`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst lines = [`Renamed ${result.from} → ${result.to}.`, ` ${result.dir}`];\n\t\t\t\tif (result.rewrites.length > 0) {\n\t\t\t\t\tlines.push(\"\", \"Rewrote in extension.mjs:\");\n\t\t\t\t\tfor (const rewrite of result.rewrites) lines.push(` line ${rewrite.line}: ${rewrite.after.trim()}`);\n\t\t\t\t}\n\t\t\t\tif (result.leftovers.length > 0) {\n\t\t\t\t\t// Prose is not identity, so it is reported rather than edited — a rename\n\t\t\t\t\t// that silently rewrote a description would be worse than one that\n\t\t\t\t\t// admits what it left.\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t`\"${result.from}\" still appears on line(s) ${result.leftovers.join(\", \")}; those look like prose, so they were left alone.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (wasOpen.length > 0) lines.push(\"\", `Closed ${wasOpen.length} open instance(s) to move the directory.`);\n\t\t\t\tlines.push(\"\", `Open it with /canvas open ${result.to}.`);\n\t\t\t\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"remove\")) {\n\t\t\t\tconst target = trimmed.slice(\"remove\".length).trim();\n\t\t\t\tif (!target) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /canvas remove <extension> (see /canvas list)\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst known = canvas.knownExtensionIds();\n\t\t\t\tif (!known.includes(target)) {\n\t\t\t\t\tctx.ui.notify(`No canvas extension \"${target}\" (found: ${known.join(\", \") || \"none\"}).`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Deleting source is not undoable from here, so it is confirmed — and\n\t\t\t\t// outside a terminal there is nobody to ask, so it is refused rather than\n\t\t\t\t// assumed. `--print` and RPC should not be able to delete a directory\n\t\t\t\t// because a command happened to be piped in.\n\t\t\t\tif (!ctx.hasUI) {\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t`/canvas remove needs to ask before deleting ${target}, and there is no interactive surface here. Delete the directory yourself, or run this in a terminal.`,\n\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst confirmed = await ctx.ui.confirm(\n\t\t\t\t\t`Delete canvas \"${target}\"?`,\n\t\t\t\t\t\"This deletes the extension directory and everything in it. It is not undoable from here.\",\n\t\t\t\t);\n\t\t\t\tif (!confirmed) {\n\t\t\t\t\tctx.ui.notify(`Left ${target} alone.`, \"info\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst openCount = canvas.instances().filter((instance) => instance.extensionId === target).length;\n\t\t\t\tconst result = await canvas.remove(target);\n\t\t\t\tif (isCanvasRefusal(result)) {\n\t\t\t\t\tctx.ui.notify(`/canvas remove: ${result.detail}`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t[\n\t\t\t\t\t\t`Removed ${result.id}.`,\n\t\t\t\t\t\t` ${result.dir}`,\n\t\t\t\t\t\topenCount > 0 ? `Closed ${openCount} open instance(s) first.` : \"\",\n\t\t\t\t\t]\n\t\t\t\t\t\t.filter((line) => line.length > 0)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed.startsWith(\"open\")) {\n\t\t\t\tctx.ui.notify(`Unknown subcommand. Use ${SUBCOMMANDS.join(\", \")}.`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst ref = parseCanvasRef(trimmed.slice(\"open\".length));\n\t\t\tif (!ref) {\n\t\t\t\tctx.ui.notify(\"Usage: /canvas open <extension>[:<canvas>] (see /canvas list)\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst outcome = await openWithLoader(canvas, ref, ctx);\n\n\t\t\t// `custom` can also settle on its own when the overlay is dismissed, without\n\t\t\t// our `done` ever running — an escape that closes the surface leaves no\n\t\t\t// outcome. Treat that as the cancel it is rather than reading `.kind` off\n\t\t\t// undefined and failing silently.\n\t\t\tif (!outcome || outcome.kind === \"cancelled\") {\n\t\t\t\t// KNOWN ISSUE: this confirmation does not render when the cancel came from the\n\t\t\t\t// loader's own escape handling, though every effect of cancelling is correct\n\t\t\t\t// and verified (the open rejects, no instance is registered, and the extension\n\t\t\t\t// is told to close the instance it never finished opening). Ruled out: the\n\t\t\t\t// continuation does run and `ctx.ui.notify` works here — the failure path\n\t\t\t\t// through the same lines renders its error, and the canvas's own diagnostic\n\t\t\t\t// arrives moments later through this very function. Deferring a tick did not\n\t\t\t\t// help either. Left as an unexplained cosmetic gap rather than papered over\n\t\t\t\t// with a sleep; the loader disappearing is itself the signal.\n\t\t\t\tctx.ui.notify(\"Canvas open cancelled.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (outcome.kind === \"failed\") {\n\t\t\t\tctx.ui.notify(outcome.message, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tregisterToolsOnce(canvas);\n\t\t\tctx.ui.notify(\n\t\t\t\t[\n\t\t\t\t\t`Opened ${outcome.instance.canvasId} (${outcome.instance.instanceId}).`,\n\t\t\t\t\toutcome.instance.url ? `Open in a browser: ${outcome.instance.url}` : \"\",\n\t\t\t\t\t\"The agent can now read and drive it; close it with /canvas close <instanceId>.\",\n\t\t\t\t]\n\t\t\t\t\t.filter((line) => line.length > 0)\n\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n\n\t// ── /new-canvas <name> | <description> ────────────────────────────────────\n\t// Authoring, in Copilot's `/create-canvas` shape: describe what you want, the\n\t// agent writes it, and it is already open while it does. Design:\n\t// `docs/canvas-extensions-design.md` §9 Phase 3, §13.\n\t//\n\t// Unlike the other `/new-*` scaffolds this one needs no /reload: canvases are\n\t// discovered when /canvas runs, not loaded at session start.\n\n\tpi.registerCommand(\"new-canvas\", {\n\t\tdescription:\n\t\t\t\"Create a canvas extension. Usage: /new-canvas <what it should do> | /new-canvas <name> | /new-canvas <name>: <what it should do>\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst request = parseCanvasRequest(args);\n\t\t\tif (typeof request === \"string\") {\n\t\t\t\tctx.ui.notify(`/new-canvas: ${request}`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// `--platform claude` is dropped rather than redirected: Claude has no\n\t\t\t// canvas convention, and silently writing into someone else's marker\n\t\t\t// directory would put an extension where that vendor will never look.\n\t\t\tconst requested = getWorkspacePlatforms() ?? [\"agents\"];\n\t\t\tconst targets = requested.filter((platform) => CANVAS_HOMES[platform] !== undefined);\n\t\t\tif (targets.length === 0) {\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t`/new-canvas: no canvas home for platform \"${requested.join(\", \")}\". ` +\n\t\t\t\t\t\t\"Canvas extensions exist under .agents/extensions (agents) and .github/extensions (github) only.\",\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst { created, skipped } = scaffoldCanvas(ctx.cwd, request.name, targets);\n\n\t\t\t// A canvas lands in the working tree, which is where the trust gate looks\n\t\t\t// (`core/canvas/trust.ts`) — so without this the canvas you just asked for\n\t\t\t// is withheld the moment you try to open it, and refused with \"came with\n\t\t\t// this repository\", which is not true of a file created seconds ago.\n\t\t\t//\n\t\t\t// Granting is the same call `/plugin install --scope project` makes, for\n\t\t\t// the same stated reason: a person typing this command in this directory\n\t\t\t// is the human act workspace trust asks for. It is deliberately wider than\n\t\t\t// this one canvas — it also lets plugins already committed here run their\n\t\t\t// hooks and MCP servers — so it is said out loud and pointed at its\n\t\t\t// reverse, never done silently.\n\t\t\tlet trustNote = \"\";\n\t\t\tif (created.length > 0 && !isWorkspaceTrusted(ctx.cwd, getAgentDir())) {\n\t\t\t\ttrustWorkspace(ctx.cwd, getAgentDir());\n\t\t\t\ttrustNote =\n\t\t\t\t\t`Trusted this workspace so the canvas can run. Plugins committed here may now run hooks ` +\n\t\t\t\t\t`and MCP servers too; \\`/plugin untrust\\` reverses it.`;\n\t\t\t}\n\n\t\t\tif (created.length === 0) {\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t[\n\t\t\t\t\t\t\"Nothing created — these already exist:\",\n\t\t\t\t\t\t...skipped.map((file) => ` ${file}`),\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t`Open the existing one with /canvas open ${request.name}, or pick another name.`,\n\t\t\t\t\t].join(\"\\n\"),\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Open before saying anything. Copilot's `/create-canvas` puts the canvas\n\t\t\t// in front of the person and *then* builds it, and the order is the point:\n\t\t\t// a build brief that can name a live url and instance id is a different\n\t\t\t// instruction than one that cannot, and the person watches it change.\n\t\t\tconst canvas = ensureSession(ctx);\n\t\t\tconst outcome = await openWithLoader(canvas, { extensionId: request.name }, ctx);\n\t\t\tconst opened = outcome?.kind === \"opened\" ? outcome.instance : undefined;\n\t\t\tif (opened) registerToolsOnce(canvas);\n\n\t\t\tconst lines = [\"Canvas created:\", ...created.map((file) => ` ${file}`)];\n\t\t\tif (skipped.length > 0) lines.push(\"Skipped (already exist):\", ...skipped.map((file) => ` ${file}`));\n\t\t\tlines.push(\"\");\n\t\t\tif (opened) {\n\t\t\t\tlines.push(`Opened ${opened.canvasId} (${opened.instanceId}).`);\n\t\t\t\tif (opened.url) lines.push(`Open in a browser: ${opened.url}`);\n\t\t\t} else if (outcome?.kind === \"failed\") {\n\t\t\t\t// Not fatal: the file is written and discoverable, so say what broke and\n\t\t\t\t// leave them a way in rather than making it look like nothing happened.\n\t\t\t\tlines.push(\n\t\t\t\t\t`Created, but opening it failed: ${outcome.message}`,\n\t\t\t\t\t`Retry with /canvas open ${request.name}.`,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tlines.push(`Opening cancelled. Open it when you want with /canvas open ${request.name}.`);\n\t\t\t}\n\t\t\tif (trustNote) lines.push(\"\", trustNote);\n\n\t\t\tif (request.description) {\n\t\t\t\tlines.push(\n\t\t\t\t\t\"\",\n\t\t\t\t\t`Building it now from: \"${request.description}\"`,\n\t\t\t\t\t\"Steer it like any other turn, or interrupt to take over the file yourself.\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tlines.push(\"\", `Edit ${created[0]} and run /canvas reload ${request.name} to see each change.`);\n\t\t\t}\n\t\t\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n\n\t\t\t// The half that makes this `/create-canvas` rather than a scaffold. Queued\n\t\t\t// as a follow-up so it lands on the next turn whether or not the agent is\n\t\t\t// mid-stream, and only when a description was given: `/new-canvas my-board`\n\t\t\t// still means \"give me the template\", and starting a build nobody asked for\n\t\t\t// would burn a turn and overwrite the file they meant to edit.\n\t\t\tif (request.description) {\n\t\t\t\tawait pi.sendUserMessage(\n\t\t\t\t\tcanvasBuildBrief(\n\t\t\t\t\t\trequest.name,\n\t\t\t\t\t\trequest.description,\n\t\t\t\t\t\tcreated[0] as string,\n\t\t\t\t\t\topened ? { instanceId: opened.instanceId, url: opened.url } : undefined,\n\t\t\t\t\t),\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t});\n\n\t// Teardown on shutdown (§6): a browser tab gives no close signal, so without this\n\t// every child and loopback port outlives the session. `session_shutdown` is where\n\t// loop.ts stops its scheduler, and it is synchronous, so the dispose is fired and\n\t// not awaited.\n\tpi.on(\"session_shutdown\", () => {\n\t\tconst closing = session;\n\t\tsession = undefined;\n\t\tvoid closing?.dispose();\n\t});\n}\n"]}
1
+ {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/canvas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAaH,OAAO,EAGN,KAAK,oBAAoB,EAEzB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,KAAK,EAAE,YAAY,EAA2B,MAAM,gCAAgC,CAAC;AAwD5F;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;CACxD;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAwapF","sourcesContent":["/**\n * `/canvas` — the interactive surface for canvas extensions.\n *\n * Design: `docs/canvas-extensions-design.md` §11. Deliberately thin: every decision\n * lives in `core/canvas/session.ts`, which is testable without a terminal, so this\n * file only renders and supplies an `AbortSignal`.\n *\n * The signal is the point of the loader. `BorderedLoader` already gives Esc-to-cancel\n * and exposes an `AbortSignal`, and `registry.open` accepts one — so a person's Esc\n * reaches the abandon path (§11.6) and the extension is told to release the port it\n * may already have bound, rather than the spinner merely disappearing.\n *\n * `/new-canvas` is registered here rather than beside `/new-skill` and friends\n * because it is not a file-writing command any more: it opens what it scaffolds\n * and hands the agent a brief to build it, which needs this file's session and\n * `pi.sendUserMessage`. Its decisions live in `core/canvas/scaffold.ts`.\n *\n * The agent tools register on the first successful open and stay for the session:\n * `registerTool` has no counterpart to remove a tool. So a session that never opens a\n * canvas pays nothing for them, which is the case that matters (§11.5); after the\n * first open they cost ~235 tokens and answer honestly when nothing is open.\n */\n\nimport { homedir } from \"node:os\";\nimport { getAgentDir } from \"../../config.js\";\nimport { CATEGORY_GLYPH } from \"../../core/brand.js\";\nimport { canvasDesignGuidePath } from \"../../core/builtin-skills.js\";\n\n/** Canvas extensions are extensions, so they wear the extension glyph. */\nconst GLYPH = CATEGORY_GLYPH.extensions;\n\nimport { isCanvasRefusal } from \"../../core/canvas/lifecycle.js\";\nimport type { CanvasInstance } from \"../../core/canvas/registry.js\";\nimport { CANVAS_HOMES, canvasBuildBrief, parseCanvasRequest, scaffoldCanvas } from \"../../core/canvas/scaffold.js\";\nimport {\n\ttype CanvasOverview,\n\tCanvasSession,\n\ttype CanvasSessionOptions,\n\tparseCanvasRef,\n} from \"../../core/canvas/session.js\";\nimport { getWorkspacePlatforms } from \"../../core/extensions/plugins/formats/platform-targets.js\";\nimport { isWorkspaceTrusted, trustWorkspace } from \"../../core/extensions/plugins/trust.js\";\nimport type { ExtensionAPI, ExtensionCommandContext } from \"../../core/extensions/types.js\";\nimport { createCanvasToolDefinitions } from \"../../core/tools/canvas.js\";\nimport { BorderedLoader } from \"../../modes/interactive/components/bordered-loader.js\";\n\nconst SUBCOMMANDS = [\"list\", \"open\", \"close\", \"reload\", \"rename\", \"remove\"] as const;\n\n/** How an open attempt ended. `custom()` resolves with exactly one of these. */\ntype OpenOutcome =\n\t| { kind: \"opened\"; instance: CanvasInstance }\n\t| { kind: \"failed\"; message: string }\n\t| { kind: \"cancelled\" };\n\nfunction describeInstance(instance: CanvasInstance): string {\n\tconst title = instance.title ?? instance.canvasId;\n\treturn `${instance.instanceId} ${title}${instance.url ? ` ${instance.url}` : \"\"}`;\n}\n\nfunction renderOverview(overview: CanvasOverview): string {\n\tconst lines: string[] = [];\n\tif (!overview.availability.available) {\n\t\tlines.push(`Canvases are unavailable: ${overview.availability.reason}`, \"\");\n\t}\n\tif (overview.listings.length === 0) {\n\t\tlines.push(\n\t\t\t\"No canvas extensions found in .agents/extensions, .github/extensions, ~/.copilot/extensions,\",\n\t\t\t\"or any installed plugin. Create one with /new-canvas <what it should do>, or install one with /plugin.\",\n\t\t);\n\t\treturn lines.join(\"\\n\");\n\t}\n\tfor (const listing of overview.listings) {\n\t\tconst name = listing.canvasId ? `${listing.extensionId}:${listing.canvasId}` : listing.extensionId;\n\t\tconst label = listing.displayName ? ` ${listing.displayName}` : \"\";\n\t\tif (listing.withheld === \"untrusted-workspace\") {\n\t\t\tlines.push(`${GLYPH} ${name}${label} [withheld: untrusted workspace]`);\n\t\t\tcontinue;\n\t\t}\n\t\tlines.push(`${GLYPH} ${name}${label} (${listing.scope})`);\n\t\tfor (const instance of listing.open) {\n\t\t\tlines.push(` open ${describeInstance(instance)}`);\n\t\t\t// What a canvas can do is otherwise visible only to the model, through\n\t\t\t// `list_canvas_capabilities` — so the person driving the session could not\n\t\t\t// see the surface they were being asked about. Only for open instances,\n\t\t\t// because actions come from running the code (§5.1).\n\t\t\tconst actions = overview.actionsByInstance.get(instance.instanceId) ?? [];\n\t\t\tif (actions.length > 0) lines.push(` actions ${actions.join(\", \")}`);\n\t\t}\n\t}\n\tif (overview.withheldCount > 0) {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t`${overview.withheldCount} extension(s) came with this repository and are withheld. Run /plugin trust to allow this directory to run code it ships.`,\n\t\t);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Test seams, and only that.\n *\n * `/new-canvas` opens what it writes, so driving it without a terminal needs a\n * runtime that does not depend on hoocode having been built, and a home\n * directory that is not the developer's. Everything else this file does is\n * decided in `core/canvas/`, where it is testable without any of this.\n */\nexport interface CanvasSetupOverrides {\n\thomeDir?: string;\n\tresolveRuntime?: CanvasSessionOptions[\"resolveRuntime\"];\n}\n\nexport function setupCanvas(pi: ExtensionAPI, overrides?: CanvasSetupOverrides): void {\n\tlet session: CanvasSession | undefined;\n\tlet toolsRegistered = false;\n\t/**\n\t * Points at the most recent command's UI.\n\t *\n\t * A canvas keeps talking after the command that opened it has returned — logs,\n\t * stray stdout, a leaked-port warning — so the callbacks cannot close over one\n\t * invocation's `ctx`.\n\t */\n\tlet notify: (message: string, type?: \"info\" | \"warning\" | \"error\") => void = () => {};\n\n\tconst ensureSession = (ctx: ExtensionCommandContext): CanvasSession => {\n\t\tnotify = (message, type) => ctx.ui.notify(message, type);\n\t\tsession ??= new CanvasSession({\n\t\t\tcwd: ctx.cwd,\n\t\t\thomeDir: overrides?.homeDir ?? homedir(),\n\t\t\tagentDir: getAgentDir(),\n\t\t\tresolveRuntime: overrides?.resolveRuntime,\n\t\t\t// A canvas's own diagnostics are the user's business: a stray stdout line means\n\t\t\t// its author reached for console.log, and a possible leaked port is worth saying.\n\t\t\tonLog: (id, message) => notify(`[canvas ${id}] ${message}`, \"info\"),\n\t\t\tonStray: (id, line) => notify(`[canvas ${id}] non-protocol stdout (use session.log): ${line}`, \"warning\"),\n\t\t\tonDiagnostic: (id, message) => notify(`[canvas ${id}] ${message}`, \"warning\"),\n\t\t});\n\t\treturn session;\n\t};\n\n\tconst registerToolsOnce = (canvas: CanvasSession): void => {\n\t\tif (toolsRegistered) return;\n\t\tconst registry = canvas.registryOrUndefined();\n\t\tif (!registry) return;\n\t\ttoolsRegistered = true;\n\t\tfor (const definition of createCanvasToolDefinitions(registry)) pi.registerTool(definition);\n\t};\n\n\t/**\n\t * Open a canvas behind a cancellable loader.\n\t *\n\t * Shared by `/canvas open` and `/new-canvas`, which want identical behaviour:\n\t * opening forks a process and binds a port, so it can be slow and must be\n\t * interruptible. The loader's signal is what makes Esc mean something — it\n\t * reaches the registry's abandon path, which tells the extension to release a\n\t * port it may already have bound. Outside a terminal (--print, RPC) there is\n\t * nothing to draw and nothing to press, so the open simply runs.\n\t */\n\tconst openWithLoader = async (\n\t\tcanvas: CanvasSession,\n\t\tref: { extensionId: string; canvasId?: string },\n\t\tctx: ExtensionCommandContext,\n\t): Promise<OpenOutcome | undefined> =>\n\t\tctx.hasUI\n\t\t\t? await ctx.ui.custom<OpenOutcome | undefined>((tui, theme, _keybindings, done) => {\n\t\t\t\t\tconst loader = new BorderedLoader(tui, theme, `Opening ${ref.extensionId}…`);\n\t\t\t\t\tloader.onAbort = () => done({ kind: \"cancelled\" });\n\t\t\t\t\tvoid canvas\n\t\t\t\t\t\t.open(ref, { signal: loader.signal })\n\t\t\t\t\t\t.then((instance) => done({ kind: \"opened\", instance }))\n\t\t\t\t\t\t// Cancelling races: the signal rejects the pending call at the same moment\n\t\t\t\t\t\t// onAbort fires, and whichever lands first resolves `custom`. Deciding from\n\t\t\t\t\t\t// the signal rather than from who won means a cancel always reads as a\n\t\t\t\t\t\t// cancel instead of surfacing as an error.\n\t\t\t\t\t\t.catch((error: unknown) =>\n\t\t\t\t\t\t\tdone(\n\t\t\t\t\t\t\t\tloader.signal.aborted\n\t\t\t\t\t\t\t\t\t? { kind: \"cancelled\" }\n\t\t\t\t\t\t\t\t\t: { kind: \"failed\", message: error instanceof Error ? error.message : String(error) },\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\treturn loader;\n\t\t\t\t})\n\t\t\t: await canvas\n\t\t\t\t\t.open(ref)\n\t\t\t\t\t.then((instance): OpenOutcome => ({ kind: \"opened\", instance }))\n\t\t\t\t\t.catch(\n\t\t\t\t\t\t(error: unknown): OpenOutcome => ({\n\t\t\t\t\t\t\tkind: \"failed\",\n\t\t\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\n\tpi.registerCommand(\"canvas\", {\n\t\tdescription:\n\t\t\t\"Work with canvas extensions. /canvas list | open <extension>[:<canvas>] | reload [extension] | close <instanceId> | rename <extension> <new-name> | remove <extension>\",\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\tSUBCOMMANDS.filter((name) => name.startsWith(prefix)).map((name) => ({ value: name, label: name })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tconst canvas = ensureSession(ctx);\n\n\t\t\tif (trimmed.length === 0 || trimmed === \"list\") {\n\t\t\t\tctx.ui.notify(renderOverview(await canvas.list()), \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"close\")) {\n\t\t\t\tconst instanceId = trimmed.slice(\"close\".length).trim();\n\t\t\t\tif (!instanceId) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /canvas close <instanceId> (see /canvas list)\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst closed = await canvas.close(instanceId);\n\t\t\t\tif (!closed) ctx.ui.notify(`No open canvas instance \"${instanceId}\".`, \"warning\");\n\t\t\t\telse ctx.ui.notify(`Closed ${closed.canvasId} (${closed.instanceId}).`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"reload\")) {\n\t\t\t\t// A reload re-forks the extension, so it can take as long as an open and is\n\t\t\t\t// worth being able to abandon — but unlike an open there is nothing to\n\t\t\t\t// abandon *to*: the registry keeps the old child serving until the new one\n\t\t\t\t// answers, so a cancel here just stops waiting.\n\t\t\t\tconst requested = trimmed.slice(\"reload\".length).trim();\n\t\t\t\tconst running = canvas.runningExtensionIds();\n\t\t\t\tconst extensionId = requested || (running.length === 1 ? running[0] : undefined);\n\t\t\t\tif (!extensionId) {\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\trunning.length === 0\n\t\t\t\t\t\t\t? \"Nothing is open, so there is nothing to reload. Open a canvas first with /canvas open <extension>.\"\n\t\t\t\t\t\t\t: `Several extensions are open; name one: ${running.join(\", \")}.`,\n\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await canvas.reload(extensionId);\n\t\t\t\t\tconst lines = [`Reloaded ${result.extensionId} from disk.`];\n\t\t\t\t\t// Same reason the tool reports it: an edit to `actions: [...]` is\n\t\t\t\t\t// otherwise invisible, and a typo there fails by the action simply\n\t\t\t\t\t// not being there.\n\t\t\t\t\tconst { added, removed, changed } = result.actions;\n\t\t\t\t\tif (added.length > 0) lines.push(` + ${added.join(\", \")}`);\n\t\t\t\t\tif (removed.length > 0) lines.push(` - ${removed.join(\", \")}`);\n\t\t\t\t\tif (changed.length > 0) lines.push(` ~ ${changed.join(\", \")} (description or schema)`);\n\t\t\t\t\tfor (const instance of result.reopened) {\n\t\t\t\t\t\t// The url is the point of saying anything: the extension binds a new\n\t\t\t\t\t\t// port and mints a new token on every open, so the tab the person has\n\t\t\t\t\t\t// in front of them is now pointing at a closed port.\n\t\t\t\t\t\tlines.push(` ${describeInstance(instance)}`);\n\t\t\t\t\t}\n\t\t\t\t\tif (result.reopened.length > 0) lines.push(\"\", \"Open the new url(s) — the previous tab is dead.\");\n\t\t\t\t\tfor (const drop of result.dropped) {\n\t\t\t\t\t\tlines.push(` ${drop.canvasId} (${drop.instanceId}) did not come back: ${drop.reason}`);\n\t\t\t\t\t}\n\t\t\t\t\tctx.ui.notify(lines.join(\"\\n\"), result.dropped.length > 0 ? \"warning\" : \"info\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// The registry only swaps children once the new one is ready, so the\n\t\t\t\t\t// canvas the person is looking at survived this. Say so, or they will\n\t\t\t\t\t// think they just lost it.\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t`Reloading ${extensionId} failed, so it is still running the code it was started with: ` +\n\t\t\t\t\t\t\t`${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t\t\t\"error\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"rename\")) {\n\t\t\t\tconst [from, to, ...rest] = trimmed.slice(\"rename\".length).trim().split(/\\s+/).filter(Boolean);\n\t\t\t\tif (!from || !to || rest.length > 0) {\n\t\t\t\t\tctx.ui.notify(`Usage: /canvas rename <extension> <new-name> (see /canvas list)`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Renaming closes whatever the extension had open, because the directory\n\t\t\t\t// is about to move out from under it. Say so before doing it, not after.\n\t\t\t\tconst wasOpen = canvas.instances().filter((instance) => instance.extensionId === from);\n\t\t\t\tconst result = await canvas.rename(from, to);\n\t\t\t\tif (isCanvasRefusal(result)) {\n\t\t\t\t\tctx.ui.notify(`/canvas rename: ${result.detail}`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst lines = [`Renamed ${result.from} → ${result.to}.`, ` ${result.dir}`];\n\t\t\t\tif (result.rewrites.length > 0) {\n\t\t\t\t\tlines.push(\"\", \"Rewrote in extension.mjs:\");\n\t\t\t\t\tfor (const rewrite of result.rewrites) lines.push(` line ${rewrite.line}: ${rewrite.after.trim()}`);\n\t\t\t\t}\n\t\t\t\tif (result.leftovers.length > 0) {\n\t\t\t\t\t// Prose is not identity, so it is reported rather than edited — a rename\n\t\t\t\t\t// that silently rewrote a description would be worse than one that\n\t\t\t\t\t// admits what it left.\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t`\"${result.from}\" still appears on line(s) ${result.leftovers.join(\", \")}; those look like prose, so they were left alone.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (wasOpen.length > 0) lines.push(\"\", `Closed ${wasOpen.length} open instance(s) to move the directory.`);\n\t\t\t\tlines.push(\"\", `Open it with /canvas open ${result.to}.`);\n\t\t\t\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"remove\")) {\n\t\t\t\tconst target = trimmed.slice(\"remove\".length).trim();\n\t\t\t\tif (!target) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /canvas remove <extension> (see /canvas list)\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst known = canvas.knownExtensionIds();\n\t\t\t\tif (!known.includes(target)) {\n\t\t\t\t\tctx.ui.notify(`No canvas extension \"${target}\" (found: ${known.join(\", \") || \"none\"}).`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Deleting source is not undoable from here, so it is confirmed — and\n\t\t\t\t// outside a terminal there is nobody to ask, so it is refused rather than\n\t\t\t\t// assumed. `--print` and RPC should not be able to delete a directory\n\t\t\t\t// because a command happened to be piped in.\n\t\t\t\tif (!ctx.hasUI) {\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t`/canvas remove needs to ask before deleting ${target}, and there is no interactive surface here. Delete the directory yourself, or run this in a terminal.`,\n\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst confirmed = await ctx.ui.confirm(\n\t\t\t\t\t`Delete canvas \"${target}\"?`,\n\t\t\t\t\t\"This deletes the extension directory and everything in it. It is not undoable from here.\",\n\t\t\t\t);\n\t\t\t\tif (!confirmed) {\n\t\t\t\t\tctx.ui.notify(`Left ${target} alone.`, \"info\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst openCount = canvas.instances().filter((instance) => instance.extensionId === target).length;\n\t\t\t\tconst result = await canvas.remove(target);\n\t\t\t\tif (isCanvasRefusal(result)) {\n\t\t\t\t\tctx.ui.notify(`/canvas remove: ${result.detail}`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t[\n\t\t\t\t\t\t`Removed ${result.id}.`,\n\t\t\t\t\t\t` ${result.dir}`,\n\t\t\t\t\t\topenCount > 0 ? `Closed ${openCount} open instance(s) first.` : \"\",\n\t\t\t\t\t]\n\t\t\t\t\t\t.filter((line) => line.length > 0)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed.startsWith(\"open\")) {\n\t\t\t\tctx.ui.notify(`Unknown subcommand. Use ${SUBCOMMANDS.join(\", \")}.`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst ref = parseCanvasRef(trimmed.slice(\"open\".length));\n\t\t\tif (!ref) {\n\t\t\t\tctx.ui.notify(\"Usage: /canvas open <extension>[:<canvas>] (see /canvas list)\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst outcome = await openWithLoader(canvas, ref, ctx);\n\n\t\t\t// `custom` can also settle on its own when the overlay is dismissed, without\n\t\t\t// our `done` ever running — an escape that closes the surface leaves no\n\t\t\t// outcome. Treat that as the cancel it is rather than reading `.kind` off\n\t\t\t// undefined and failing silently.\n\t\t\tif (!outcome || outcome.kind === \"cancelled\") {\n\t\t\t\t// KNOWN ISSUE: this confirmation does not render when the cancel came from the\n\t\t\t\t// loader's own escape handling, though every effect of cancelling is correct\n\t\t\t\t// and verified (the open rejects, no instance is registered, and the extension\n\t\t\t\t// is told to close the instance it never finished opening). Ruled out: the\n\t\t\t\t// continuation does run and `ctx.ui.notify` works here — the failure path\n\t\t\t\t// through the same lines renders its error, and the canvas's own diagnostic\n\t\t\t\t// arrives moments later through this very function. Deferring a tick did not\n\t\t\t\t// help either. Left as an unexplained cosmetic gap rather than papered over\n\t\t\t\t// with a sleep; the loader disappearing is itself the signal.\n\t\t\t\tctx.ui.notify(\"Canvas open cancelled.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (outcome.kind === \"failed\") {\n\t\t\t\tctx.ui.notify(outcome.message, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tregisterToolsOnce(canvas);\n\t\t\tctx.ui.notify(\n\t\t\t\t[\n\t\t\t\t\t`Opened ${outcome.instance.canvasId} (${outcome.instance.instanceId}).`,\n\t\t\t\t\toutcome.instance.url ? `Open in a browser: ${outcome.instance.url}` : \"\",\n\t\t\t\t\t\"The agent can now read and drive it; close it with /canvas close <instanceId>.\",\n\t\t\t\t]\n\t\t\t\t\t.filter((line) => line.length > 0)\n\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n\n\t// ── /new-canvas <name> | <description> ────────────────────────────────────\n\t// Authoring, in Copilot's `/create-canvas` shape: describe what you want, the\n\t// agent writes it, and it is already open while it does. Design:\n\t// `docs/canvas-extensions-design.md` §9 Phase 3, §13.\n\t//\n\t// Unlike the other `/new-*` scaffolds this one needs no /reload: canvases are\n\t// discovered when /canvas runs, not loaded at session start.\n\n\tpi.registerCommand(\"new-canvas\", {\n\t\tdescription:\n\t\t\t\"Create a canvas extension. Usage: /new-canvas <what it should do> | /new-canvas <name> | /new-canvas <name>: <what it should do>\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst request = parseCanvasRequest(args);\n\t\t\tif (typeof request === \"string\") {\n\t\t\t\tctx.ui.notify(`/new-canvas: ${request}`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// `--platform claude` is dropped rather than redirected: Claude has no\n\t\t\t// canvas convention, and silently writing into someone else's marker\n\t\t\t// directory would put an extension where that vendor will never look.\n\t\t\tconst requested = getWorkspacePlatforms() ?? [\"agents\"];\n\t\t\tconst targets = requested.filter((platform) => CANVAS_HOMES[platform] !== undefined);\n\t\t\tif (targets.length === 0) {\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t`/new-canvas: no canvas home for platform \"${requested.join(\", \")}\". ` +\n\t\t\t\t\t\t\"Canvas extensions exist under .agents/extensions (agents) and .github/extensions (github) only.\",\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst { created, skipped } = scaffoldCanvas(ctx.cwd, request.name, targets);\n\n\t\t\t// A canvas lands in the working tree, which is where the trust gate looks\n\t\t\t// (`core/canvas/trust.ts`) — so without this the canvas you just asked for\n\t\t\t// is withheld the moment you try to open it, and refused with \"came with\n\t\t\t// this repository\", which is not true of a file created seconds ago.\n\t\t\t//\n\t\t\t// Granting is the same call `/plugin install --scope project` makes, for\n\t\t\t// the same stated reason: a person typing this command in this directory\n\t\t\t// is the human act workspace trust asks for. It is deliberately wider than\n\t\t\t// this one canvas — it also lets plugins already committed here run their\n\t\t\t// hooks and MCP servers — so it is said out loud and pointed at its\n\t\t\t// reverse, never done silently.\n\t\t\tlet trustNote = \"\";\n\t\t\tif (created.length > 0 && !isWorkspaceTrusted(ctx.cwd, getAgentDir())) {\n\t\t\t\ttrustWorkspace(ctx.cwd, getAgentDir());\n\t\t\t\ttrustNote =\n\t\t\t\t\t`Trusted this workspace so the canvas can run. Plugins committed here may now run hooks ` +\n\t\t\t\t\t`and MCP servers too; \\`/plugin untrust\\` reverses it.`;\n\t\t\t}\n\n\t\t\tif (created.length === 0) {\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t[\n\t\t\t\t\t\t\"Nothing created — these already exist:\",\n\t\t\t\t\t\t...skipped.map((file) => ` ${file}`),\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t`Open the existing one with /canvas open ${request.name}, or pick another name.`,\n\t\t\t\t\t].join(\"\\n\"),\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Open before saying anything. Copilot's `/create-canvas` puts the canvas\n\t\t\t// in front of the person and *then* builds it, and the order is the point:\n\t\t\t// a build brief that can name a live url and instance id is a different\n\t\t\t// instruction than one that cannot, and the person watches it change.\n\t\t\tconst canvas = ensureSession(ctx);\n\t\t\tconst outcome = await openWithLoader(canvas, { extensionId: request.name }, ctx);\n\t\t\tconst opened = outcome?.kind === \"opened\" ? outcome.instance : undefined;\n\t\t\tif (opened) registerToolsOnce(canvas);\n\n\t\t\tconst lines = [\"Canvas created:\", ...created.map((file) => ` ${file}`)];\n\t\t\tif (skipped.length > 0) lines.push(\"Skipped (already exist):\", ...skipped.map((file) => ` ${file}`));\n\t\t\tlines.push(\"\");\n\t\t\tif (opened) {\n\t\t\t\tlines.push(`Opened ${opened.canvasId} (${opened.instanceId}).`);\n\t\t\t\tif (opened.url) lines.push(`Open in a browser: ${opened.url}`);\n\t\t\t} else if (outcome?.kind === \"failed\") {\n\t\t\t\t// Not fatal: the file is written and discoverable, so say what broke and\n\t\t\t\t// leave them a way in rather than making it look like nothing happened.\n\t\t\t\tlines.push(\n\t\t\t\t\t`Created, but opening it failed: ${outcome.message}`,\n\t\t\t\t\t`Retry with /canvas open ${request.name}.`,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tlines.push(`Opening cancelled. Open it when you want with /canvas open ${request.name}.`);\n\t\t\t}\n\t\t\tif (trustNote) lines.push(\"\", trustNote);\n\n\t\t\tif (request.description) {\n\t\t\t\tlines.push(\n\t\t\t\t\t\"\",\n\t\t\t\t\t`Building it now from: \"${request.description}\"`,\n\t\t\t\t\t\"Steer it like any other turn, or interrupt to take over the file yourself.\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tlines.push(\"\", `Edit ${created[0]} and run /canvas reload ${request.name} to see each change.`);\n\t\t\t}\n\t\t\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n\n\t\t\t// The half that makes this `/create-canvas` rather than a scaffold. Queued\n\t\t\t// as a follow-up so it lands on the next turn whether or not the agent is\n\t\t\t// mid-stream, and only when a description was given: `/new-canvas my-board`\n\t\t\t// still means \"give me the template\", and starting a build nobody asked for\n\t\t\t// would burn a turn and overwrite the file they meant to edit.\n\t\t\tif (request.description) {\n\t\t\t\tawait pi.sendUserMessage(\n\t\t\t\t\tcanvasBuildBrief(\n\t\t\t\t\t\trequest.name,\n\t\t\t\t\t\trequest.description,\n\t\t\t\t\t\tcreated[0] as string,\n\t\t\t\t\t\topened ? { instanceId: opened.instanceId, url: opened.url } : undefined,\n\t\t\t\t\t\tcanvasDesignGuidePath(),\n\t\t\t\t\t),\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t});\n\n\t// Teardown on shutdown (§6): a browser tab gives no close signal, so without this\n\t// every child and loopback port outlives the session. `session_shutdown` is where\n\t// loop.ts stops its scheduler, and it is synchronous, so the dispose is fired and\n\t// not awaited.\n\tpi.on(\"session_shutdown\", () => {\n\t\tconst closing = session;\n\t\tsession = undefined;\n\t\tvoid closing?.dispose();\n\t});\n}\n"]}
@@ -23,6 +23,7 @@
23
23
  import { homedir } from "node:os";
24
24
  import { getAgentDir } from "../../config.js";
25
25
  import { CATEGORY_GLYPH } from "../../core/brand.js";
26
+ import { canvasDesignGuidePath } from "../../core/builtin-skills.js";
26
27
  /** Canvas extensions are extensions, so they wear the extension glyph. */
27
28
  const GLYPH = CATEGORY_GLYPH.extensions;
28
29
  import { isCanvasRefusal } from "../../core/canvas/lifecycle.js";
@@ -418,7 +419,7 @@ export function setupCanvas(pi, overrides) {
418
419
  // still means "give me the template", and starting a build nobody asked for
419
420
  // would burn a turn and overwrite the file they meant to edit.
420
421
  if (request.description) {
421
- await pi.sendUserMessage(canvasBuildBrief(request.name, request.description, created[0], opened ? { instanceId: opened.instanceId, url: opened.url } : undefined), { deliverAs: "followUp" });
422
+ await pi.sendUserMessage(canvasBuildBrief(request.name, request.description, created[0], opened ? { instanceId: opened.instanceId, url: opened.url } : undefined, canvasDesignGuidePath()), { deliverAs: "followUp" });
422
423
  }
423
424
  },
424
425
  });
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../../src/extensions/core/canvas.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `/canvas` — the interactive surface for canvas extensions.\n *\n * Design: `docs/canvas-extensions-design.md` §11. Deliberately thin: every decision\n * lives in `core/canvas/session.ts`, which is testable without a terminal, so this\n * file only renders and supplies an `AbortSignal`.\n *\n * The signal is the point of the loader. `BorderedLoader` already gives Esc-to-cancel\n * and exposes an `AbortSignal`, and `registry.open` accepts one — so a person's Esc\n * reaches the abandon path (§11.6) and the extension is told to release the port it\n * may already have bound, rather than the spinner merely disappearing.\n *\n * `/new-canvas` is registered here rather than beside `/new-skill` and friends\n * because it is not a file-writing command any more: it opens what it scaffolds\n * and hands the agent a brief to build it, which needs this file's session and\n * `pi.sendUserMessage`. Its decisions live in `core/canvas/scaffold.ts`.\n *\n * The agent tools register on the first successful open and stay for the session:\n * `registerTool` has no counterpart to remove a tool. So a session that never opens a\n * canvas pays nothing for them, which is the case that matters (§11.5); after the\n * first open they cost ~235 tokens and answer honestly when nothing is open.\n */\n\nimport { homedir } from \"node:os\";\nimport { getAgentDir } from \"../../config.js\";\nimport { CATEGORY_GLYPH } from \"../../core/brand.js\";\n\n/** Canvas extensions are extensions, so they wear the extension glyph. */\nconst GLYPH = CATEGORY_GLYPH.extensions;\n\nimport { isCanvasRefusal } from \"../../core/canvas/lifecycle.js\";\nimport type { CanvasInstance } from \"../../core/canvas/registry.js\";\nimport { CANVAS_HOMES, canvasBuildBrief, parseCanvasRequest, scaffoldCanvas } from \"../../core/canvas/scaffold.js\";\nimport {\n\ttype CanvasOverview,\n\tCanvasSession,\n\ttype CanvasSessionOptions,\n\tparseCanvasRef,\n} from \"../../core/canvas/session.js\";\nimport { getWorkspacePlatforms } from \"../../core/extensions/plugins/formats/platform-targets.js\";\nimport { isWorkspaceTrusted, trustWorkspace } from \"../../core/extensions/plugins/trust.js\";\nimport type { ExtensionAPI, ExtensionCommandContext } from \"../../core/extensions/types.js\";\nimport { createCanvasToolDefinitions } from \"../../core/tools/canvas.js\";\nimport { BorderedLoader } from \"../../modes/interactive/components/bordered-loader.js\";\n\nconst SUBCOMMANDS = [\"list\", \"open\", \"close\", \"reload\", \"rename\", \"remove\"] as const;\n\n/** How an open attempt ended. `custom()` resolves with exactly one of these. */\ntype OpenOutcome =\n\t| { kind: \"opened\"; instance: CanvasInstance }\n\t| { kind: \"failed\"; message: string }\n\t| { kind: \"cancelled\" };\n\nfunction describeInstance(instance: CanvasInstance): string {\n\tconst title = instance.title ?? instance.canvasId;\n\treturn `${instance.instanceId} ${title}${instance.url ? ` ${instance.url}` : \"\"}`;\n}\n\nfunction renderOverview(overview: CanvasOverview): string {\n\tconst lines: string[] = [];\n\tif (!overview.availability.available) {\n\t\tlines.push(`Canvases are unavailable: ${overview.availability.reason}`, \"\");\n\t}\n\tif (overview.listings.length === 0) {\n\t\tlines.push(\n\t\t\t\"No canvas extensions found in .agents/extensions, .github/extensions, ~/.copilot/extensions,\",\n\t\t\t\"or any installed plugin. Create one with /new-canvas <what it should do>, or install one with /plugin.\",\n\t\t);\n\t\treturn lines.join(\"\\n\");\n\t}\n\tfor (const listing of overview.listings) {\n\t\tconst name = listing.canvasId ? `${listing.extensionId}:${listing.canvasId}` : listing.extensionId;\n\t\tconst label = listing.displayName ? ` ${listing.displayName}` : \"\";\n\t\tif (listing.withheld === \"untrusted-workspace\") {\n\t\t\tlines.push(`${GLYPH} ${name}${label} [withheld: untrusted workspace]`);\n\t\t\tcontinue;\n\t\t}\n\t\tlines.push(`${GLYPH} ${name}${label} (${listing.scope})`);\n\t\tfor (const instance of listing.open) {\n\t\t\tlines.push(` open ${describeInstance(instance)}`);\n\t\t\t// What a canvas can do is otherwise visible only to the model, through\n\t\t\t// `list_canvas_capabilities` — so the person driving the session could not\n\t\t\t// see the surface they were being asked about. Only for open instances,\n\t\t\t// because actions come from running the code (§5.1).\n\t\t\tconst actions = overview.actionsByInstance.get(instance.instanceId) ?? [];\n\t\t\tif (actions.length > 0) lines.push(` actions ${actions.join(\", \")}`);\n\t\t}\n\t}\n\tif (overview.withheldCount > 0) {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t`${overview.withheldCount} extension(s) came with this repository and are withheld. Run /plugin trust to allow this directory to run code it ships.`,\n\t\t);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Test seams, and only that.\n *\n * `/new-canvas` opens what it writes, so driving it without a terminal needs a\n * runtime that does not depend on hoocode having been built, and a home\n * directory that is not the developer's. Everything else this file does is\n * decided in `core/canvas/`, where it is testable without any of this.\n */\nexport interface CanvasSetupOverrides {\n\thomeDir?: string;\n\tresolveRuntime?: CanvasSessionOptions[\"resolveRuntime\"];\n}\n\nexport function setupCanvas(pi: ExtensionAPI, overrides?: CanvasSetupOverrides): void {\n\tlet session: CanvasSession | undefined;\n\tlet toolsRegistered = false;\n\t/**\n\t * Points at the most recent command's UI.\n\t *\n\t * A canvas keeps talking after the command that opened it has returned — logs,\n\t * stray stdout, a leaked-port warning — so the callbacks cannot close over one\n\t * invocation's `ctx`.\n\t */\n\tlet notify: (message: string, type?: \"info\" | \"warning\" | \"error\") => void = () => {};\n\n\tconst ensureSession = (ctx: ExtensionCommandContext): CanvasSession => {\n\t\tnotify = (message, type) => ctx.ui.notify(message, type);\n\t\tsession ??= new CanvasSession({\n\t\t\tcwd: ctx.cwd,\n\t\t\thomeDir: overrides?.homeDir ?? homedir(),\n\t\t\tagentDir: getAgentDir(),\n\t\t\tresolveRuntime: overrides?.resolveRuntime,\n\t\t\t// A canvas's own diagnostics are the user's business: a stray stdout line means\n\t\t\t// its author reached for console.log, and a possible leaked port is worth saying.\n\t\t\tonLog: (id, message) => notify(`[canvas ${id}] ${message}`, \"info\"),\n\t\t\tonStray: (id, line) => notify(`[canvas ${id}] non-protocol stdout (use session.log): ${line}`, \"warning\"),\n\t\t\tonDiagnostic: (id, message) => notify(`[canvas ${id}] ${message}`, \"warning\"),\n\t\t});\n\t\treturn session;\n\t};\n\n\tconst registerToolsOnce = (canvas: CanvasSession): void => {\n\t\tif (toolsRegistered) return;\n\t\tconst registry = canvas.registryOrUndefined();\n\t\tif (!registry) return;\n\t\ttoolsRegistered = true;\n\t\tfor (const definition of createCanvasToolDefinitions(registry)) pi.registerTool(definition);\n\t};\n\n\t/**\n\t * Open a canvas behind a cancellable loader.\n\t *\n\t * Shared by `/canvas open` and `/new-canvas`, which want identical behaviour:\n\t * opening forks a process and binds a port, so it can be slow and must be\n\t * interruptible. The loader's signal is what makes Esc mean something — it\n\t * reaches the registry's abandon path, which tells the extension to release a\n\t * port it may already have bound. Outside a terminal (--print, RPC) there is\n\t * nothing to draw and nothing to press, so the open simply runs.\n\t */\n\tconst openWithLoader = async (\n\t\tcanvas: CanvasSession,\n\t\tref: { extensionId: string; canvasId?: string },\n\t\tctx: ExtensionCommandContext,\n\t): Promise<OpenOutcome | undefined> =>\n\t\tctx.hasUI\n\t\t\t? await ctx.ui.custom<OpenOutcome | undefined>((tui, theme, _keybindings, done) => {\n\t\t\t\t\tconst loader = new BorderedLoader(tui, theme, `Opening ${ref.extensionId}…`);\n\t\t\t\t\tloader.onAbort = () => done({ kind: \"cancelled\" });\n\t\t\t\t\tvoid canvas\n\t\t\t\t\t\t.open(ref, { signal: loader.signal })\n\t\t\t\t\t\t.then((instance) => done({ kind: \"opened\", instance }))\n\t\t\t\t\t\t// Cancelling races: the signal rejects the pending call at the same moment\n\t\t\t\t\t\t// onAbort fires, and whichever lands first resolves `custom`. Deciding from\n\t\t\t\t\t\t// the signal rather than from who won means a cancel always reads as a\n\t\t\t\t\t\t// cancel instead of surfacing as an error.\n\t\t\t\t\t\t.catch((error: unknown) =>\n\t\t\t\t\t\t\tdone(\n\t\t\t\t\t\t\t\tloader.signal.aborted\n\t\t\t\t\t\t\t\t\t? { kind: \"cancelled\" }\n\t\t\t\t\t\t\t\t\t: { kind: \"failed\", message: error instanceof Error ? error.message : String(error) },\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\treturn loader;\n\t\t\t\t})\n\t\t\t: await canvas\n\t\t\t\t\t.open(ref)\n\t\t\t\t\t.then((instance): OpenOutcome => ({ kind: \"opened\", instance }))\n\t\t\t\t\t.catch(\n\t\t\t\t\t\t(error: unknown): OpenOutcome => ({\n\t\t\t\t\t\t\tkind: \"failed\",\n\t\t\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\n\tpi.registerCommand(\"canvas\", {\n\t\tdescription:\n\t\t\t\"Work with canvas extensions. /canvas list | open <extension>[:<canvas>] | reload [extension] | close <instanceId> | rename <extension> <new-name> | remove <extension>\",\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\tSUBCOMMANDS.filter((name) => name.startsWith(prefix)).map((name) => ({ value: name, label: name })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tconst canvas = ensureSession(ctx);\n\n\t\t\tif (trimmed.length === 0 || trimmed === \"list\") {\n\t\t\t\tctx.ui.notify(renderOverview(await canvas.list()), \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"close\")) {\n\t\t\t\tconst instanceId = trimmed.slice(\"close\".length).trim();\n\t\t\t\tif (!instanceId) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /canvas close <instanceId> (see /canvas list)\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst closed = await canvas.close(instanceId);\n\t\t\t\tif (!closed) ctx.ui.notify(`No open canvas instance \"${instanceId}\".`, \"warning\");\n\t\t\t\telse ctx.ui.notify(`Closed ${closed.canvasId} (${closed.instanceId}).`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"reload\")) {\n\t\t\t\t// A reload re-forks the extension, so it can take as long as an open and is\n\t\t\t\t// worth being able to abandon — but unlike an open there is nothing to\n\t\t\t\t// abandon *to*: the registry keeps the old child serving until the new one\n\t\t\t\t// answers, so a cancel here just stops waiting.\n\t\t\t\tconst requested = trimmed.slice(\"reload\".length).trim();\n\t\t\t\tconst running = canvas.runningExtensionIds();\n\t\t\t\tconst extensionId = requested || (running.length === 1 ? running[0] : undefined);\n\t\t\t\tif (!extensionId) {\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\trunning.length === 0\n\t\t\t\t\t\t\t? \"Nothing is open, so there is nothing to reload. Open a canvas first with /canvas open <extension>.\"\n\t\t\t\t\t\t\t: `Several extensions are open; name one: ${running.join(\", \")}.`,\n\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await canvas.reload(extensionId);\n\t\t\t\t\tconst lines = [`Reloaded ${result.extensionId} from disk.`];\n\t\t\t\t\t// Same reason the tool reports it: an edit to `actions: [...]` is\n\t\t\t\t\t// otherwise invisible, and a typo there fails by the action simply\n\t\t\t\t\t// not being there.\n\t\t\t\t\tconst { added, removed, changed } = result.actions;\n\t\t\t\t\tif (added.length > 0) lines.push(` + ${added.join(\", \")}`);\n\t\t\t\t\tif (removed.length > 0) lines.push(` - ${removed.join(\", \")}`);\n\t\t\t\t\tif (changed.length > 0) lines.push(` ~ ${changed.join(\", \")} (description or schema)`);\n\t\t\t\t\tfor (const instance of result.reopened) {\n\t\t\t\t\t\t// The url is the point of saying anything: the extension binds a new\n\t\t\t\t\t\t// port and mints a new token on every open, so the tab the person has\n\t\t\t\t\t\t// in front of them is now pointing at a closed port.\n\t\t\t\t\t\tlines.push(` ${describeInstance(instance)}`);\n\t\t\t\t\t}\n\t\t\t\t\tif (result.reopened.length > 0) lines.push(\"\", \"Open the new url(s) — the previous tab is dead.\");\n\t\t\t\t\tfor (const drop of result.dropped) {\n\t\t\t\t\t\tlines.push(` ${drop.canvasId} (${drop.instanceId}) did not come back: ${drop.reason}`);\n\t\t\t\t\t}\n\t\t\t\t\tctx.ui.notify(lines.join(\"\\n\"), result.dropped.length > 0 ? \"warning\" : \"info\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// The registry only swaps children once the new one is ready, so the\n\t\t\t\t\t// canvas the person is looking at survived this. Say so, or they will\n\t\t\t\t\t// think they just lost it.\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t`Reloading ${extensionId} failed, so it is still running the code it was started with: ` +\n\t\t\t\t\t\t\t`${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t\t\t\"error\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"rename\")) {\n\t\t\t\tconst [from, to, ...rest] = trimmed.slice(\"rename\".length).trim().split(/\\s+/).filter(Boolean);\n\t\t\t\tif (!from || !to || rest.length > 0) {\n\t\t\t\t\tctx.ui.notify(`Usage: /canvas rename <extension> <new-name> (see /canvas list)`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Renaming closes whatever the extension had open, because the directory\n\t\t\t\t// is about to move out from under it. Say so before doing it, not after.\n\t\t\t\tconst wasOpen = canvas.instances().filter((instance) => instance.extensionId === from);\n\t\t\t\tconst result = await canvas.rename(from, to);\n\t\t\t\tif (isCanvasRefusal(result)) {\n\t\t\t\t\tctx.ui.notify(`/canvas rename: ${result.detail}`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst lines = [`Renamed ${result.from} → ${result.to}.`, ` ${result.dir}`];\n\t\t\t\tif (result.rewrites.length > 0) {\n\t\t\t\t\tlines.push(\"\", \"Rewrote in extension.mjs:\");\n\t\t\t\t\tfor (const rewrite of result.rewrites) lines.push(` line ${rewrite.line}: ${rewrite.after.trim()}`);\n\t\t\t\t}\n\t\t\t\tif (result.leftovers.length > 0) {\n\t\t\t\t\t// Prose is not identity, so it is reported rather than edited — a rename\n\t\t\t\t\t// that silently rewrote a description would be worse than one that\n\t\t\t\t\t// admits what it left.\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t`\"${result.from}\" still appears on line(s) ${result.leftovers.join(\", \")}; those look like prose, so they were left alone.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (wasOpen.length > 0) lines.push(\"\", `Closed ${wasOpen.length} open instance(s) to move the directory.`);\n\t\t\t\tlines.push(\"\", `Open it with /canvas open ${result.to}.`);\n\t\t\t\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"remove\")) {\n\t\t\t\tconst target = trimmed.slice(\"remove\".length).trim();\n\t\t\t\tif (!target) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /canvas remove <extension> (see /canvas list)\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst known = canvas.knownExtensionIds();\n\t\t\t\tif (!known.includes(target)) {\n\t\t\t\t\tctx.ui.notify(`No canvas extension \"${target}\" (found: ${known.join(\", \") || \"none\"}).`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Deleting source is not undoable from here, so it is confirmed — and\n\t\t\t\t// outside a terminal there is nobody to ask, so it is refused rather than\n\t\t\t\t// assumed. `--print` and RPC should not be able to delete a directory\n\t\t\t\t// because a command happened to be piped in.\n\t\t\t\tif (!ctx.hasUI) {\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t`/canvas remove needs to ask before deleting ${target}, and there is no interactive surface here. Delete the directory yourself, or run this in a terminal.`,\n\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst confirmed = await ctx.ui.confirm(\n\t\t\t\t\t`Delete canvas \"${target}\"?`,\n\t\t\t\t\t\"This deletes the extension directory and everything in it. It is not undoable from here.\",\n\t\t\t\t);\n\t\t\t\tif (!confirmed) {\n\t\t\t\t\tctx.ui.notify(`Left ${target} alone.`, \"info\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst openCount = canvas.instances().filter((instance) => instance.extensionId === target).length;\n\t\t\t\tconst result = await canvas.remove(target);\n\t\t\t\tif (isCanvasRefusal(result)) {\n\t\t\t\t\tctx.ui.notify(`/canvas remove: ${result.detail}`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t[\n\t\t\t\t\t\t`Removed ${result.id}.`,\n\t\t\t\t\t\t` ${result.dir}`,\n\t\t\t\t\t\topenCount > 0 ? `Closed ${openCount} open instance(s) first.` : \"\",\n\t\t\t\t\t]\n\t\t\t\t\t\t.filter((line) => line.length > 0)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed.startsWith(\"open\")) {\n\t\t\t\tctx.ui.notify(`Unknown subcommand. Use ${SUBCOMMANDS.join(\", \")}.`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst ref = parseCanvasRef(trimmed.slice(\"open\".length));\n\t\t\tif (!ref) {\n\t\t\t\tctx.ui.notify(\"Usage: /canvas open <extension>[:<canvas>] (see /canvas list)\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst outcome = await openWithLoader(canvas, ref, ctx);\n\n\t\t\t// `custom` can also settle on its own when the overlay is dismissed, without\n\t\t\t// our `done` ever running — an escape that closes the surface leaves no\n\t\t\t// outcome. Treat that as the cancel it is rather than reading `.kind` off\n\t\t\t// undefined and failing silently.\n\t\t\tif (!outcome || outcome.kind === \"cancelled\") {\n\t\t\t\t// KNOWN ISSUE: this confirmation does not render when the cancel came from the\n\t\t\t\t// loader's own escape handling, though every effect of cancelling is correct\n\t\t\t\t// and verified (the open rejects, no instance is registered, and the extension\n\t\t\t\t// is told to close the instance it never finished opening). Ruled out: the\n\t\t\t\t// continuation does run and `ctx.ui.notify` works here — the failure path\n\t\t\t\t// through the same lines renders its error, and the canvas's own diagnostic\n\t\t\t\t// arrives moments later through this very function. Deferring a tick did not\n\t\t\t\t// help either. Left as an unexplained cosmetic gap rather than papered over\n\t\t\t\t// with a sleep; the loader disappearing is itself the signal.\n\t\t\t\tctx.ui.notify(\"Canvas open cancelled.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (outcome.kind === \"failed\") {\n\t\t\t\tctx.ui.notify(outcome.message, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tregisterToolsOnce(canvas);\n\t\t\tctx.ui.notify(\n\t\t\t\t[\n\t\t\t\t\t`Opened ${outcome.instance.canvasId} (${outcome.instance.instanceId}).`,\n\t\t\t\t\toutcome.instance.url ? `Open in a browser: ${outcome.instance.url}` : \"\",\n\t\t\t\t\t\"The agent can now read and drive it; close it with /canvas close <instanceId>.\",\n\t\t\t\t]\n\t\t\t\t\t.filter((line) => line.length > 0)\n\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n\n\t// ── /new-canvas <name> | <description> ────────────────────────────────────\n\t// Authoring, in Copilot's `/create-canvas` shape: describe what you want, the\n\t// agent writes it, and it is already open while it does. Design:\n\t// `docs/canvas-extensions-design.md` §9 Phase 3, §13.\n\t//\n\t// Unlike the other `/new-*` scaffolds this one needs no /reload: canvases are\n\t// discovered when /canvas runs, not loaded at session start.\n\n\tpi.registerCommand(\"new-canvas\", {\n\t\tdescription:\n\t\t\t\"Create a canvas extension. Usage: /new-canvas <what it should do> | /new-canvas <name> | /new-canvas <name>: <what it should do>\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst request = parseCanvasRequest(args);\n\t\t\tif (typeof request === \"string\") {\n\t\t\t\tctx.ui.notify(`/new-canvas: ${request}`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// `--platform claude` is dropped rather than redirected: Claude has no\n\t\t\t// canvas convention, and silently writing into someone else's marker\n\t\t\t// directory would put an extension where that vendor will never look.\n\t\t\tconst requested = getWorkspacePlatforms() ?? [\"agents\"];\n\t\t\tconst targets = requested.filter((platform) => CANVAS_HOMES[platform] !== undefined);\n\t\t\tif (targets.length === 0) {\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t`/new-canvas: no canvas home for platform \"${requested.join(\", \")}\". ` +\n\t\t\t\t\t\t\"Canvas extensions exist under .agents/extensions (agents) and .github/extensions (github) only.\",\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst { created, skipped } = scaffoldCanvas(ctx.cwd, request.name, targets);\n\n\t\t\t// A canvas lands in the working tree, which is where the trust gate looks\n\t\t\t// (`core/canvas/trust.ts`) — so without this the canvas you just asked for\n\t\t\t// is withheld the moment you try to open it, and refused with \"came with\n\t\t\t// this repository\", which is not true of a file created seconds ago.\n\t\t\t//\n\t\t\t// Granting is the same call `/plugin install --scope project` makes, for\n\t\t\t// the same stated reason: a person typing this command in this directory\n\t\t\t// is the human act workspace trust asks for. It is deliberately wider than\n\t\t\t// this one canvas — it also lets plugins already committed here run their\n\t\t\t// hooks and MCP servers — so it is said out loud and pointed at its\n\t\t\t// reverse, never done silently.\n\t\t\tlet trustNote = \"\";\n\t\t\tif (created.length > 0 && !isWorkspaceTrusted(ctx.cwd, getAgentDir())) {\n\t\t\t\ttrustWorkspace(ctx.cwd, getAgentDir());\n\t\t\t\ttrustNote =\n\t\t\t\t\t`Trusted this workspace so the canvas can run. Plugins committed here may now run hooks ` +\n\t\t\t\t\t`and MCP servers too; \\`/plugin untrust\\` reverses it.`;\n\t\t\t}\n\n\t\t\tif (created.length === 0) {\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t[\n\t\t\t\t\t\t\"Nothing created — these already exist:\",\n\t\t\t\t\t\t...skipped.map((file) => ` ${file}`),\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t`Open the existing one with /canvas open ${request.name}, or pick another name.`,\n\t\t\t\t\t].join(\"\\n\"),\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Open before saying anything. Copilot's `/create-canvas` puts the canvas\n\t\t\t// in front of the person and *then* builds it, and the order is the point:\n\t\t\t// a build brief that can name a live url and instance id is a different\n\t\t\t// instruction than one that cannot, and the person watches it change.\n\t\t\tconst canvas = ensureSession(ctx);\n\t\t\tconst outcome = await openWithLoader(canvas, { extensionId: request.name }, ctx);\n\t\t\tconst opened = outcome?.kind === \"opened\" ? outcome.instance : undefined;\n\t\t\tif (opened) registerToolsOnce(canvas);\n\n\t\t\tconst lines = [\"Canvas created:\", ...created.map((file) => ` ${file}`)];\n\t\t\tif (skipped.length > 0) lines.push(\"Skipped (already exist):\", ...skipped.map((file) => ` ${file}`));\n\t\t\tlines.push(\"\");\n\t\t\tif (opened) {\n\t\t\t\tlines.push(`Opened ${opened.canvasId} (${opened.instanceId}).`);\n\t\t\t\tif (opened.url) lines.push(`Open in a browser: ${opened.url}`);\n\t\t\t} else if (outcome?.kind === \"failed\") {\n\t\t\t\t// Not fatal: the file is written and discoverable, so say what broke and\n\t\t\t\t// leave them a way in rather than making it look like nothing happened.\n\t\t\t\tlines.push(\n\t\t\t\t\t`Created, but opening it failed: ${outcome.message}`,\n\t\t\t\t\t`Retry with /canvas open ${request.name}.`,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tlines.push(`Opening cancelled. Open it when you want with /canvas open ${request.name}.`);\n\t\t\t}\n\t\t\tif (trustNote) lines.push(\"\", trustNote);\n\n\t\t\tif (request.description) {\n\t\t\t\tlines.push(\n\t\t\t\t\t\"\",\n\t\t\t\t\t`Building it now from: \"${request.description}\"`,\n\t\t\t\t\t\"Steer it like any other turn, or interrupt to take over the file yourself.\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tlines.push(\"\", `Edit ${created[0]} and run /canvas reload ${request.name} to see each change.`);\n\t\t\t}\n\t\t\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n\n\t\t\t// The half that makes this `/create-canvas` rather than a scaffold. Queued\n\t\t\t// as a follow-up so it lands on the next turn whether or not the agent is\n\t\t\t// mid-stream, and only when a description was given: `/new-canvas my-board`\n\t\t\t// still means \"give me the template\", and starting a build nobody asked for\n\t\t\t// would burn a turn and overwrite the file they meant to edit.\n\t\t\tif (request.description) {\n\t\t\t\tawait pi.sendUserMessage(\n\t\t\t\t\tcanvasBuildBrief(\n\t\t\t\t\t\trequest.name,\n\t\t\t\t\t\trequest.description,\n\t\t\t\t\t\tcreated[0] as string,\n\t\t\t\t\t\topened ? { instanceId: opened.instanceId, url: opened.url } : undefined,\n\t\t\t\t\t),\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t});\n\n\t// Teardown on shutdown (§6): a browser tab gives no close signal, so without this\n\t// every child and loopback port outlives the session. `session_shutdown` is where\n\t// loop.ts stops its scheduler, and it is synchronous, so the dispose is fired and\n\t// not awaited.\n\tpi.on(\"session_shutdown\", () => {\n\t\tconst closing = session;\n\t\tsession = undefined;\n\t\tvoid closing?.dispose();\n\t});\n}\n"]}
1
+ {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../../src/extensions/core/canvas.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `/canvas` — the interactive surface for canvas extensions.\n *\n * Design: `docs/canvas-extensions-design.md` §11. Deliberately thin: every decision\n * lives in `core/canvas/session.ts`, which is testable without a terminal, so this\n * file only renders and supplies an `AbortSignal`.\n *\n * The signal is the point of the loader. `BorderedLoader` already gives Esc-to-cancel\n * and exposes an `AbortSignal`, and `registry.open` accepts one — so a person's Esc\n * reaches the abandon path (§11.6) and the extension is told to release the port it\n * may already have bound, rather than the spinner merely disappearing.\n *\n * `/new-canvas` is registered here rather than beside `/new-skill` and friends\n * because it is not a file-writing command any more: it opens what it scaffolds\n * and hands the agent a brief to build it, which needs this file's session and\n * `pi.sendUserMessage`. Its decisions live in `core/canvas/scaffold.ts`.\n *\n * The agent tools register on the first successful open and stay for the session:\n * `registerTool` has no counterpart to remove a tool. So a session that never opens a\n * canvas pays nothing for them, which is the case that matters (§11.5); after the\n * first open they cost ~235 tokens and answer honestly when nothing is open.\n */\n\nimport { homedir } from \"node:os\";\nimport { getAgentDir } from \"../../config.js\";\nimport { CATEGORY_GLYPH } from \"../../core/brand.js\";\nimport { canvasDesignGuidePath } from \"../../core/builtin-skills.js\";\n\n/** Canvas extensions are extensions, so they wear the extension glyph. */\nconst GLYPH = CATEGORY_GLYPH.extensions;\n\nimport { isCanvasRefusal } from \"../../core/canvas/lifecycle.js\";\nimport type { CanvasInstance } from \"../../core/canvas/registry.js\";\nimport { CANVAS_HOMES, canvasBuildBrief, parseCanvasRequest, scaffoldCanvas } from \"../../core/canvas/scaffold.js\";\nimport {\n\ttype CanvasOverview,\n\tCanvasSession,\n\ttype CanvasSessionOptions,\n\tparseCanvasRef,\n} from \"../../core/canvas/session.js\";\nimport { getWorkspacePlatforms } from \"../../core/extensions/plugins/formats/platform-targets.js\";\nimport { isWorkspaceTrusted, trustWorkspace } from \"../../core/extensions/plugins/trust.js\";\nimport type { ExtensionAPI, ExtensionCommandContext } from \"../../core/extensions/types.js\";\nimport { createCanvasToolDefinitions } from \"../../core/tools/canvas.js\";\nimport { BorderedLoader } from \"../../modes/interactive/components/bordered-loader.js\";\n\nconst SUBCOMMANDS = [\"list\", \"open\", \"close\", \"reload\", \"rename\", \"remove\"] as const;\n\n/** How an open attempt ended. `custom()` resolves with exactly one of these. */\ntype OpenOutcome =\n\t| { kind: \"opened\"; instance: CanvasInstance }\n\t| { kind: \"failed\"; message: string }\n\t| { kind: \"cancelled\" };\n\nfunction describeInstance(instance: CanvasInstance): string {\n\tconst title = instance.title ?? instance.canvasId;\n\treturn `${instance.instanceId} ${title}${instance.url ? ` ${instance.url}` : \"\"}`;\n}\n\nfunction renderOverview(overview: CanvasOverview): string {\n\tconst lines: string[] = [];\n\tif (!overview.availability.available) {\n\t\tlines.push(`Canvases are unavailable: ${overview.availability.reason}`, \"\");\n\t}\n\tif (overview.listings.length === 0) {\n\t\tlines.push(\n\t\t\t\"No canvas extensions found in .agents/extensions, .github/extensions, ~/.copilot/extensions,\",\n\t\t\t\"or any installed plugin. Create one with /new-canvas <what it should do>, or install one with /plugin.\",\n\t\t);\n\t\treturn lines.join(\"\\n\");\n\t}\n\tfor (const listing of overview.listings) {\n\t\tconst name = listing.canvasId ? `${listing.extensionId}:${listing.canvasId}` : listing.extensionId;\n\t\tconst label = listing.displayName ? ` ${listing.displayName}` : \"\";\n\t\tif (listing.withheld === \"untrusted-workspace\") {\n\t\t\tlines.push(`${GLYPH} ${name}${label} [withheld: untrusted workspace]`);\n\t\t\tcontinue;\n\t\t}\n\t\tlines.push(`${GLYPH} ${name}${label} (${listing.scope})`);\n\t\tfor (const instance of listing.open) {\n\t\t\tlines.push(` open ${describeInstance(instance)}`);\n\t\t\t// What a canvas can do is otherwise visible only to the model, through\n\t\t\t// `list_canvas_capabilities` — so the person driving the session could not\n\t\t\t// see the surface they were being asked about. Only for open instances,\n\t\t\t// because actions come from running the code (§5.1).\n\t\t\tconst actions = overview.actionsByInstance.get(instance.instanceId) ?? [];\n\t\t\tif (actions.length > 0) lines.push(` actions ${actions.join(\", \")}`);\n\t\t}\n\t}\n\tif (overview.withheldCount > 0) {\n\t\tlines.push(\n\t\t\t\"\",\n\t\t\t`${overview.withheldCount} extension(s) came with this repository and are withheld. Run /plugin trust to allow this directory to run code it ships.`,\n\t\t);\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Test seams, and only that.\n *\n * `/new-canvas` opens what it writes, so driving it without a terminal needs a\n * runtime that does not depend on hoocode having been built, and a home\n * directory that is not the developer's. Everything else this file does is\n * decided in `core/canvas/`, where it is testable without any of this.\n */\nexport interface CanvasSetupOverrides {\n\thomeDir?: string;\n\tresolveRuntime?: CanvasSessionOptions[\"resolveRuntime\"];\n}\n\nexport function setupCanvas(pi: ExtensionAPI, overrides?: CanvasSetupOverrides): void {\n\tlet session: CanvasSession | undefined;\n\tlet toolsRegistered = false;\n\t/**\n\t * Points at the most recent command's UI.\n\t *\n\t * A canvas keeps talking after the command that opened it has returned — logs,\n\t * stray stdout, a leaked-port warning — so the callbacks cannot close over one\n\t * invocation's `ctx`.\n\t */\n\tlet notify: (message: string, type?: \"info\" | \"warning\" | \"error\") => void = () => {};\n\n\tconst ensureSession = (ctx: ExtensionCommandContext): CanvasSession => {\n\t\tnotify = (message, type) => ctx.ui.notify(message, type);\n\t\tsession ??= new CanvasSession({\n\t\t\tcwd: ctx.cwd,\n\t\t\thomeDir: overrides?.homeDir ?? homedir(),\n\t\t\tagentDir: getAgentDir(),\n\t\t\tresolveRuntime: overrides?.resolveRuntime,\n\t\t\t// A canvas's own diagnostics are the user's business: a stray stdout line means\n\t\t\t// its author reached for console.log, and a possible leaked port is worth saying.\n\t\t\tonLog: (id, message) => notify(`[canvas ${id}] ${message}`, \"info\"),\n\t\t\tonStray: (id, line) => notify(`[canvas ${id}] non-protocol stdout (use session.log): ${line}`, \"warning\"),\n\t\t\tonDiagnostic: (id, message) => notify(`[canvas ${id}] ${message}`, \"warning\"),\n\t\t});\n\t\treturn session;\n\t};\n\n\tconst registerToolsOnce = (canvas: CanvasSession): void => {\n\t\tif (toolsRegistered) return;\n\t\tconst registry = canvas.registryOrUndefined();\n\t\tif (!registry) return;\n\t\ttoolsRegistered = true;\n\t\tfor (const definition of createCanvasToolDefinitions(registry)) pi.registerTool(definition);\n\t};\n\n\t/**\n\t * Open a canvas behind a cancellable loader.\n\t *\n\t * Shared by `/canvas open` and `/new-canvas`, which want identical behaviour:\n\t * opening forks a process and binds a port, so it can be slow and must be\n\t * interruptible. The loader's signal is what makes Esc mean something — it\n\t * reaches the registry's abandon path, which tells the extension to release a\n\t * port it may already have bound. Outside a terminal (--print, RPC) there is\n\t * nothing to draw and nothing to press, so the open simply runs.\n\t */\n\tconst openWithLoader = async (\n\t\tcanvas: CanvasSession,\n\t\tref: { extensionId: string; canvasId?: string },\n\t\tctx: ExtensionCommandContext,\n\t): Promise<OpenOutcome | undefined> =>\n\t\tctx.hasUI\n\t\t\t? await ctx.ui.custom<OpenOutcome | undefined>((tui, theme, _keybindings, done) => {\n\t\t\t\t\tconst loader = new BorderedLoader(tui, theme, `Opening ${ref.extensionId}…`);\n\t\t\t\t\tloader.onAbort = () => done({ kind: \"cancelled\" });\n\t\t\t\t\tvoid canvas\n\t\t\t\t\t\t.open(ref, { signal: loader.signal })\n\t\t\t\t\t\t.then((instance) => done({ kind: \"opened\", instance }))\n\t\t\t\t\t\t// Cancelling races: the signal rejects the pending call at the same moment\n\t\t\t\t\t\t// onAbort fires, and whichever lands first resolves `custom`. Deciding from\n\t\t\t\t\t\t// the signal rather than from who won means a cancel always reads as a\n\t\t\t\t\t\t// cancel instead of surfacing as an error.\n\t\t\t\t\t\t.catch((error: unknown) =>\n\t\t\t\t\t\t\tdone(\n\t\t\t\t\t\t\t\tloader.signal.aborted\n\t\t\t\t\t\t\t\t\t? { kind: \"cancelled\" }\n\t\t\t\t\t\t\t\t\t: { kind: \"failed\", message: error instanceof Error ? error.message : String(error) },\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\treturn loader;\n\t\t\t\t})\n\t\t\t: await canvas\n\t\t\t\t\t.open(ref)\n\t\t\t\t\t.then((instance): OpenOutcome => ({ kind: \"opened\", instance }))\n\t\t\t\t\t.catch(\n\t\t\t\t\t\t(error: unknown): OpenOutcome => ({\n\t\t\t\t\t\t\tkind: \"failed\",\n\t\t\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\n\tpi.registerCommand(\"canvas\", {\n\t\tdescription:\n\t\t\t\"Work with canvas extensions. /canvas list | open <extension>[:<canvas>] | reload [extension] | close <instanceId> | rename <extension> <new-name> | remove <extension>\",\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\tSUBCOMMANDS.filter((name) => name.startsWith(prefix)).map((name) => ({ value: name, label: name })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tconst canvas = ensureSession(ctx);\n\n\t\t\tif (trimmed.length === 0 || trimmed === \"list\") {\n\t\t\t\tctx.ui.notify(renderOverview(await canvas.list()), \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"close\")) {\n\t\t\t\tconst instanceId = trimmed.slice(\"close\".length).trim();\n\t\t\t\tif (!instanceId) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /canvas close <instanceId> (see /canvas list)\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst closed = await canvas.close(instanceId);\n\t\t\t\tif (!closed) ctx.ui.notify(`No open canvas instance \"${instanceId}\".`, \"warning\");\n\t\t\t\telse ctx.ui.notify(`Closed ${closed.canvasId} (${closed.instanceId}).`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"reload\")) {\n\t\t\t\t// A reload re-forks the extension, so it can take as long as an open and is\n\t\t\t\t// worth being able to abandon — but unlike an open there is nothing to\n\t\t\t\t// abandon *to*: the registry keeps the old child serving until the new one\n\t\t\t\t// answers, so a cancel here just stops waiting.\n\t\t\t\tconst requested = trimmed.slice(\"reload\".length).trim();\n\t\t\t\tconst running = canvas.runningExtensionIds();\n\t\t\t\tconst extensionId = requested || (running.length === 1 ? running[0] : undefined);\n\t\t\t\tif (!extensionId) {\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\trunning.length === 0\n\t\t\t\t\t\t\t? \"Nothing is open, so there is nothing to reload. Open a canvas first with /canvas open <extension>.\"\n\t\t\t\t\t\t\t: `Several extensions are open; name one: ${running.join(\", \")}.`,\n\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await canvas.reload(extensionId);\n\t\t\t\t\tconst lines = [`Reloaded ${result.extensionId} from disk.`];\n\t\t\t\t\t// Same reason the tool reports it: an edit to `actions: [...]` is\n\t\t\t\t\t// otherwise invisible, and a typo there fails by the action simply\n\t\t\t\t\t// not being there.\n\t\t\t\t\tconst { added, removed, changed } = result.actions;\n\t\t\t\t\tif (added.length > 0) lines.push(` + ${added.join(\", \")}`);\n\t\t\t\t\tif (removed.length > 0) lines.push(` - ${removed.join(\", \")}`);\n\t\t\t\t\tif (changed.length > 0) lines.push(` ~ ${changed.join(\", \")} (description or schema)`);\n\t\t\t\t\tfor (const instance of result.reopened) {\n\t\t\t\t\t\t// The url is the point of saying anything: the extension binds a new\n\t\t\t\t\t\t// port and mints a new token on every open, so the tab the person has\n\t\t\t\t\t\t// in front of them is now pointing at a closed port.\n\t\t\t\t\t\tlines.push(` ${describeInstance(instance)}`);\n\t\t\t\t\t}\n\t\t\t\t\tif (result.reopened.length > 0) lines.push(\"\", \"Open the new url(s) — the previous tab is dead.\");\n\t\t\t\t\tfor (const drop of result.dropped) {\n\t\t\t\t\t\tlines.push(` ${drop.canvasId} (${drop.instanceId}) did not come back: ${drop.reason}`);\n\t\t\t\t\t}\n\t\t\t\t\tctx.ui.notify(lines.join(\"\\n\"), result.dropped.length > 0 ? \"warning\" : \"info\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// The registry only swaps children once the new one is ready, so the\n\t\t\t\t\t// canvas the person is looking at survived this. Say so, or they will\n\t\t\t\t\t// think they just lost it.\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t`Reloading ${extensionId} failed, so it is still running the code it was started with: ` +\n\t\t\t\t\t\t\t`${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t\t\t\"error\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"rename\")) {\n\t\t\t\tconst [from, to, ...rest] = trimmed.slice(\"rename\".length).trim().split(/\\s+/).filter(Boolean);\n\t\t\t\tif (!from || !to || rest.length > 0) {\n\t\t\t\t\tctx.ui.notify(`Usage: /canvas rename <extension> <new-name> (see /canvas list)`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Renaming closes whatever the extension had open, because the directory\n\t\t\t\t// is about to move out from under it. Say so before doing it, not after.\n\t\t\t\tconst wasOpen = canvas.instances().filter((instance) => instance.extensionId === from);\n\t\t\t\tconst result = await canvas.rename(from, to);\n\t\t\t\tif (isCanvasRefusal(result)) {\n\t\t\t\t\tctx.ui.notify(`/canvas rename: ${result.detail}`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst lines = [`Renamed ${result.from} → ${result.to}.`, ` ${result.dir}`];\n\t\t\t\tif (result.rewrites.length > 0) {\n\t\t\t\t\tlines.push(\"\", \"Rewrote in extension.mjs:\");\n\t\t\t\t\tfor (const rewrite of result.rewrites) lines.push(` line ${rewrite.line}: ${rewrite.after.trim()}`);\n\t\t\t\t}\n\t\t\t\tif (result.leftovers.length > 0) {\n\t\t\t\t\t// Prose is not identity, so it is reported rather than edited — a rename\n\t\t\t\t\t// that silently rewrote a description would be worse than one that\n\t\t\t\t\t// admits what it left.\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t`\"${result.from}\" still appears on line(s) ${result.leftovers.join(\", \")}; those look like prose, so they were left alone.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (wasOpen.length > 0) lines.push(\"\", `Closed ${wasOpen.length} open instance(s) to move the directory.`);\n\t\t\t\tlines.push(\"\", `Open it with /canvas open ${result.to}.`);\n\t\t\t\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"remove\")) {\n\t\t\t\tconst target = trimmed.slice(\"remove\".length).trim();\n\t\t\t\tif (!target) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /canvas remove <extension> (see /canvas list)\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst known = canvas.knownExtensionIds();\n\t\t\t\tif (!known.includes(target)) {\n\t\t\t\t\tctx.ui.notify(`No canvas extension \"${target}\" (found: ${known.join(\", \") || \"none\"}).`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Deleting source is not undoable from here, so it is confirmed — and\n\t\t\t\t// outside a terminal there is nobody to ask, so it is refused rather than\n\t\t\t\t// assumed. `--print` and RPC should not be able to delete a directory\n\t\t\t\t// because a command happened to be piped in.\n\t\t\t\tif (!ctx.hasUI) {\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t`/canvas remove needs to ask before deleting ${target}, and there is no interactive surface here. Delete the directory yourself, or run this in a terminal.`,\n\t\t\t\t\t\t\"warning\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst confirmed = await ctx.ui.confirm(\n\t\t\t\t\t`Delete canvas \"${target}\"?`,\n\t\t\t\t\t\"This deletes the extension directory and everything in it. It is not undoable from here.\",\n\t\t\t\t);\n\t\t\t\tif (!confirmed) {\n\t\t\t\t\tctx.ui.notify(`Left ${target} alone.`, \"info\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst openCount = canvas.instances().filter((instance) => instance.extensionId === target).length;\n\t\t\t\tconst result = await canvas.remove(target);\n\t\t\t\tif (isCanvasRefusal(result)) {\n\t\t\t\t\tctx.ui.notify(`/canvas remove: ${result.detail}`, \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t[\n\t\t\t\t\t\t`Removed ${result.id}.`,\n\t\t\t\t\t\t` ${result.dir}`,\n\t\t\t\t\t\topenCount > 0 ? `Closed ${openCount} open instance(s) first.` : \"\",\n\t\t\t\t\t]\n\t\t\t\t\t\t.filter((line) => line.length > 0)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed.startsWith(\"open\")) {\n\t\t\t\tctx.ui.notify(`Unknown subcommand. Use ${SUBCOMMANDS.join(\", \")}.`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst ref = parseCanvasRef(trimmed.slice(\"open\".length));\n\t\t\tif (!ref) {\n\t\t\t\tctx.ui.notify(\"Usage: /canvas open <extension>[:<canvas>] (see /canvas list)\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst outcome = await openWithLoader(canvas, ref, ctx);\n\n\t\t\t// `custom` can also settle on its own when the overlay is dismissed, without\n\t\t\t// our `done` ever running — an escape that closes the surface leaves no\n\t\t\t// outcome. Treat that as the cancel it is rather than reading `.kind` off\n\t\t\t// undefined and failing silently.\n\t\t\tif (!outcome || outcome.kind === \"cancelled\") {\n\t\t\t\t// KNOWN ISSUE: this confirmation does not render when the cancel came from the\n\t\t\t\t// loader's own escape handling, though every effect of cancelling is correct\n\t\t\t\t// and verified (the open rejects, no instance is registered, and the extension\n\t\t\t\t// is told to close the instance it never finished opening). Ruled out: the\n\t\t\t\t// continuation does run and `ctx.ui.notify` works here — the failure path\n\t\t\t\t// through the same lines renders its error, and the canvas's own diagnostic\n\t\t\t\t// arrives moments later through this very function. Deferring a tick did not\n\t\t\t\t// help either. Left as an unexplained cosmetic gap rather than papered over\n\t\t\t\t// with a sleep; the loader disappearing is itself the signal.\n\t\t\t\tctx.ui.notify(\"Canvas open cancelled.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (outcome.kind === \"failed\") {\n\t\t\t\tctx.ui.notify(outcome.message, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tregisterToolsOnce(canvas);\n\t\t\tctx.ui.notify(\n\t\t\t\t[\n\t\t\t\t\t`Opened ${outcome.instance.canvasId} (${outcome.instance.instanceId}).`,\n\t\t\t\t\toutcome.instance.url ? `Open in a browser: ${outcome.instance.url}` : \"\",\n\t\t\t\t\t\"The agent can now read and drive it; close it with /canvas close <instanceId>.\",\n\t\t\t\t]\n\t\t\t\t\t.filter((line) => line.length > 0)\n\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n\n\t// ── /new-canvas <name> | <description> ────────────────────────────────────\n\t// Authoring, in Copilot's `/create-canvas` shape: describe what you want, the\n\t// agent writes it, and it is already open while it does. Design:\n\t// `docs/canvas-extensions-design.md` §9 Phase 3, §13.\n\t//\n\t// Unlike the other `/new-*` scaffolds this one needs no /reload: canvases are\n\t// discovered when /canvas runs, not loaded at session start.\n\n\tpi.registerCommand(\"new-canvas\", {\n\t\tdescription:\n\t\t\t\"Create a canvas extension. Usage: /new-canvas <what it should do> | /new-canvas <name> | /new-canvas <name>: <what it should do>\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst request = parseCanvasRequest(args);\n\t\t\tif (typeof request === \"string\") {\n\t\t\t\tctx.ui.notify(`/new-canvas: ${request}`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// `--platform claude` is dropped rather than redirected: Claude has no\n\t\t\t// canvas convention, and silently writing into someone else's marker\n\t\t\t// directory would put an extension where that vendor will never look.\n\t\t\tconst requested = getWorkspacePlatforms() ?? [\"agents\"];\n\t\t\tconst targets = requested.filter((platform) => CANVAS_HOMES[platform] !== undefined);\n\t\t\tif (targets.length === 0) {\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t`/new-canvas: no canvas home for platform \"${requested.join(\", \")}\". ` +\n\t\t\t\t\t\t\"Canvas extensions exist under .agents/extensions (agents) and .github/extensions (github) only.\",\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst { created, skipped } = scaffoldCanvas(ctx.cwd, request.name, targets);\n\n\t\t\t// A canvas lands in the working tree, which is where the trust gate looks\n\t\t\t// (`core/canvas/trust.ts`) — so without this the canvas you just asked for\n\t\t\t// is withheld the moment you try to open it, and refused with \"came with\n\t\t\t// this repository\", which is not true of a file created seconds ago.\n\t\t\t//\n\t\t\t// Granting is the same call `/plugin install --scope project` makes, for\n\t\t\t// the same stated reason: a person typing this command in this directory\n\t\t\t// is the human act workspace trust asks for. It is deliberately wider than\n\t\t\t// this one canvas — it also lets plugins already committed here run their\n\t\t\t// hooks and MCP servers — so it is said out loud and pointed at its\n\t\t\t// reverse, never done silently.\n\t\t\tlet trustNote = \"\";\n\t\t\tif (created.length > 0 && !isWorkspaceTrusted(ctx.cwd, getAgentDir())) {\n\t\t\t\ttrustWorkspace(ctx.cwd, getAgentDir());\n\t\t\t\ttrustNote =\n\t\t\t\t\t`Trusted this workspace so the canvas can run. Plugins committed here may now run hooks ` +\n\t\t\t\t\t`and MCP servers too; \\`/plugin untrust\\` reverses it.`;\n\t\t\t}\n\n\t\t\tif (created.length === 0) {\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t[\n\t\t\t\t\t\t\"Nothing created — these already exist:\",\n\t\t\t\t\t\t...skipped.map((file) => ` ${file}`),\n\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t`Open the existing one with /canvas open ${request.name}, or pick another name.`,\n\t\t\t\t\t].join(\"\\n\"),\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Open before saying anything. Copilot's `/create-canvas` puts the canvas\n\t\t\t// in front of the person and *then* builds it, and the order is the point:\n\t\t\t// a build brief that can name a live url and instance id is a different\n\t\t\t// instruction than one that cannot, and the person watches it change.\n\t\t\tconst canvas = ensureSession(ctx);\n\t\t\tconst outcome = await openWithLoader(canvas, { extensionId: request.name }, ctx);\n\t\t\tconst opened = outcome?.kind === \"opened\" ? outcome.instance : undefined;\n\t\t\tif (opened) registerToolsOnce(canvas);\n\n\t\t\tconst lines = [\"Canvas created:\", ...created.map((file) => ` ${file}`)];\n\t\t\tif (skipped.length > 0) lines.push(\"Skipped (already exist):\", ...skipped.map((file) => ` ${file}`));\n\t\t\tlines.push(\"\");\n\t\t\tif (opened) {\n\t\t\t\tlines.push(`Opened ${opened.canvasId} (${opened.instanceId}).`);\n\t\t\t\tif (opened.url) lines.push(`Open in a browser: ${opened.url}`);\n\t\t\t} else if (outcome?.kind === \"failed\") {\n\t\t\t\t// Not fatal: the file is written and discoverable, so say what broke and\n\t\t\t\t// leave them a way in rather than making it look like nothing happened.\n\t\t\t\tlines.push(\n\t\t\t\t\t`Created, but opening it failed: ${outcome.message}`,\n\t\t\t\t\t`Retry with /canvas open ${request.name}.`,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tlines.push(`Opening cancelled. Open it when you want with /canvas open ${request.name}.`);\n\t\t\t}\n\t\t\tif (trustNote) lines.push(\"\", trustNote);\n\n\t\t\tif (request.description) {\n\t\t\t\tlines.push(\n\t\t\t\t\t\"\",\n\t\t\t\t\t`Building it now from: \"${request.description}\"`,\n\t\t\t\t\t\"Steer it like any other turn, or interrupt to take over the file yourself.\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tlines.push(\"\", `Edit ${created[0]} and run /canvas reload ${request.name} to see each change.`);\n\t\t\t}\n\t\t\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n\n\t\t\t// The half that makes this `/create-canvas` rather than a scaffold. Queued\n\t\t\t// as a follow-up so it lands on the next turn whether or not the agent is\n\t\t\t// mid-stream, and only when a description was given: `/new-canvas my-board`\n\t\t\t// still means \"give me the template\", and starting a build nobody asked for\n\t\t\t// would burn a turn and overwrite the file they meant to edit.\n\t\t\tif (request.description) {\n\t\t\t\tawait pi.sendUserMessage(\n\t\t\t\t\tcanvasBuildBrief(\n\t\t\t\t\t\trequest.name,\n\t\t\t\t\t\trequest.description,\n\t\t\t\t\t\tcreated[0] as string,\n\t\t\t\t\t\topened ? { instanceId: opened.instanceId, url: opened.url } : undefined,\n\t\t\t\t\t\tcanvasDesignGuidePath(),\n\t\t\t\t\t),\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t});\n\n\t// Teardown on shutdown (§6): a browser tab gives no close signal, so without this\n\t// every child and loopback port outlives the session. `session_shutdown` is where\n\t// loop.ts stops its scheduler, and it is synchronous, so the dispose is fired and\n\t// not awaited.\n\tpi.on(\"session_shutdown\", () => {\n\t\tconst closing = session;\n\t\tsession = undefined;\n\t\tvoid closing?.dispose();\n\t});\n}\n"]}