@kolisachint/hoocode-agent 0.5.23 → 0.5.25

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/core/brand.d.ts +1 -0
  3. package/dist/core/brand.d.ts.map +1 -1
  4. package/dist/core/brand.js +1 -0
  5. package/dist/core/brand.js.map +1 -1
  6. package/dist/core/canvas/discovery.d.ts +11 -3
  7. package/dist/core/canvas/discovery.d.ts.map +1 -1
  8. package/dist/core/canvas/discovery.js +18 -3
  9. package/dist/core/canvas/discovery.js.map +1 -1
  10. package/dist/core/canvas/plugin-canvases.d.ts +30 -0
  11. package/dist/core/canvas/plugin-canvases.d.ts.map +1 -0
  12. package/dist/core/canvas/plugin-canvases.js +51 -0
  13. package/dist/core/canvas/plugin-canvases.js.map +1 -0
  14. package/dist/core/canvas/session.d.ts +7 -0
  15. package/dist/core/canvas/session.d.ts.map +1 -1
  16. package/dist/core/canvas/session.js +8 -1
  17. package/dist/core/canvas/session.js.map +1 -1
  18. package/dist/core/canvas/trust.d.ts +4 -5
  19. package/dist/core/canvas/trust.d.ts.map +1 -1
  20. package/dist/core/canvas/trust.js +23 -8
  21. package/dist/core/canvas/trust.js.map +1 -1
  22. package/dist/core/extensions/loader.d.ts.map +1 -1
  23. package/dist/core/extensions/loader.js +25 -11
  24. package/dist/core/extensions/loader.js.map +1 -1
  25. package/dist/core/extensions/plugins/formats/copilot.d.ts +10 -0
  26. package/dist/core/extensions/plugins/formats/copilot.d.ts.map +1 -1
  27. package/dist/core/extensions/plugins/formats/copilot.js +36 -4
  28. package/dist/core/extensions/plugins/formats/copilot.js.map +1 -1
  29. package/dist/core/extensions/plugins/formats/jsonManifest.d.ts.map +1 -1
  30. package/dist/core/extensions/plugins/formats/jsonManifest.js +15 -2
  31. package/dist/core/extensions/plugins/formats/jsonManifest.js.map +1 -1
  32. package/dist/core/extensions/plugins/formats/shared.d.ts +30 -2
  33. package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -1
  34. package/dist/core/extensions/plugins/formats/shared.js +72 -0
  35. package/dist/core/extensions/plugins/formats/shared.js.map +1 -1
  36. package/dist/core/extensions/plugins/install.d.ts +31 -0
  37. package/dist/core/extensions/plugins/install.d.ts.map +1 -1
  38. package/dist/core/extensions/plugins/install.js +102 -12
  39. package/dist/core/extensions/plugins/install.js.map +1 -1
  40. package/dist/core/extensions/plugins/listing.d.ts.map +1 -1
  41. package/dist/core/extensions/plugins/listing.js +1 -0
  42. package/dist/core/extensions/plugins/listing.js.map +1 -1
  43. package/dist/core/extensions/plugins/manifest.d.ts +18 -0
  44. package/dist/core/extensions/plugins/manifest.d.ts.map +1 -1
  45. package/dist/core/extensions/plugins/manifest.js.map +1 -1
  46. package/dist/core/extensions/plugins/packaging.d.ts.map +1 -1
  47. package/dist/core/extensions/plugins/packaging.js +1 -0
  48. package/dist/core/extensions/plugins/packaging.js.map +1 -1
  49. package/dist/extensions/core/canvas.d.ts.map +1 -1
  50. package/dist/extensions/core/canvas.js +1 -1
  51. package/dist/extensions/core/canvas.js.map +1 -1
  52. package/dist/extensions/core/marketplace.d.ts.map +1 -1
  53. package/dist/extensions/core/marketplace.js +28 -1
  54. package/dist/extensions/core/marketplace.js.map +1 -1
  55. package/dist/extensions/core/scaffold.d.ts +1 -1
  56. package/dist/extensions/core/scaffold.d.ts.map +1 -1
  57. package/dist/extensions/core/scaffold.js +185 -1
  58. package/dist/extensions/core/scaffold.js.map +1 -1
  59. package/dist/modes/interactive/interactive-mode.d.ts +11 -0
  60. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  61. package/dist/modes/interactive/interactive-mode.js +40 -0
  62. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  63. package/dist/modes/interactive/resource-display.d.ts +11 -0
  64. package/dist/modes/interactive/resource-display.d.ts.map +1 -1
  65. package/dist/modes/interactive/resource-display.js +20 -1
  66. package/dist/modes/interactive/resource-display.js.map +1 -1
  67. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  68. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  69. package/examples/extensions/sandbox/package.json +1 -1
  70. package/examples/extensions/with-deps/package.json +1 -1
  71. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.25] - 2026-08-20
4
+
5
+ ### Added
6
+
7
+ - `/new-canvas <name>` — scaffold a canvas extension. Writes
8
+ `.agents/extensions/<name>/extension.mjs` (or `.github/extensions/` with
9
+ `--platform github`), pre-wired with a declared canvas, one agent-callable
10
+ action, and a token-gated loopback server. It runs as scaffolded, so
11
+ `/canvas open <name>` works immediately — no `/reload`, because canvases are
12
+ discovered when `/canvas` runs rather than loaded at session start.
13
+
14
+ Scaffolding trusts the workspace, the same grant `/plugin install --scope
15
+ project` makes and for the same reason: a canvas lands in the working tree,
16
+ which is where the trust gate looks, so without it the canvas you just asked
17
+ for would be refused with "came with this repository" seconds after you
18
+ created it. The grant is wider than the one canvas — plugins committed here
19
+ may then run hooks and MCP servers — so it is stated in the output and
20
+ `/plugin untrust` reverses it.
21
+
22
+ - Plugins can ship canvases, and `/canvas` now finds them. Both real catalog
23
+ layouts are read: the Copilot manifest's `"extensions": "<dir>"` path key
24
+ (`Redth/mobile-canvas-ghcp`) and the Agent Plugins vendor content namespace
25
+ `com.github.copilot/extensions/<id>/` (`github/awesome-copilot`). A plugin
26
+ whose root itself carries `extension.mjs` counts as one canvas named for the
27
+ plugin. Installed canvases are listed and opened exactly like hand-placed
28
+ ones, and a canvas from a project-scoped plugin goes through the same
29
+ workspace-trust gate as one in `.github/extensions/` — it arrives in every
30
+ collaborator's clone the same way.
31
+
32
+ - Subagents in the `com.github.copilot/agents/` namespace load. This is where
33
+ `github/awesome-copilot` publishes all 125 of its plugin agents; they were
34
+ previously read as nothing.
35
+
36
+ - Canvases appear in the startup and `/reload` surface — a counted cell, and a
37
+ detail row per canvas carrying either `/canvas open <id>` or the reason it is
38
+ withheld. Installing a canvas plugin used to report "1 plugin" and give no
39
+ sign a canvas existed, which is a large part of why the gap below went
40
+ unnoticed.
41
+
42
+ ### Fixed
43
+
44
+ - `github/awesome-copilot` plugins install with their content. hoocode cloned
45
+ the repository's default branch, where a `plugins/<name>/` directory holds
46
+ only `plugin.json` and `README.md` — the real content lives in top-level
47
+ trees and is materialized into each plugin directory by CI on the
48
+ `marketplace` branch, which is the branch the vendor's own installer reads.
49
+ Every entry therefore installed as an empty shell and reported that it
50
+ contributed no capabilities. The marketplace is now pinned to that branch,
51
+ and a cache left on the wrong ref is re-cloned rather than kept up to date
52
+ forever. `/plugin install arcade-canvas@awesome-copilot` now lands a working
53
+ canvas and says so.
54
+
55
+ The pin names a branch hoocode does not control, so a retired one falls back
56
+ to the default branch with a note saying entries there may be unbuilt, rather
57
+ than dropping the marketplace outright — and a fallback clone stays put
58
+ instead of being discarded and re-downloaded on every run.
59
+
60
+ - `/plugin list` and `/plugin install` refresh the curated marketplace indices
61
+ before reading them, as `SearchPlugins` already did. The human path never did,
62
+ so a stale — or, after the fix below, a *wrong-ref* — cache could persist
63
+ indefinitely: someone who only ever typed `/plugin install` would have kept
64
+ installing unbuilt stubs with no way to know why. TTL-respecting, so it is
65
+ free when the cache is fresh, and it says so before a fetch that will take a
66
+ moment.
67
+
68
+ - Starting a new session, resuming one, or forking one shows the same loaded
69
+ resources `/reload` does. The listing was rendered — extensions rebind before
70
+ the transcript is reset — and then wiped a moment later by that reset, so
71
+ every session change but `/reload` reported nothing.
72
+
73
+ ## [0.5.24] - 2026-08-20
74
+
75
+ ### Fixed
76
+
77
+ - Extension loading no longer depends on the sibling workspace packages having
78
+ been built. `getAliases()` resolved every workspace alias eagerly and threw if
79
+ any one of them was missing, so a single unresolvable specifier took down every
80
+ extension — including extensions that import nothing. The fallback also used
81
+ `import.meta.resolve`, which is undefined once the module goes through a
82
+ bundler or test transform (`__vite_ssr_import_meta__.resolve is not a
83
+ function`). Aliases now resolve via `require.resolve` and an unresolvable one
84
+ is omitted rather than fatal.
85
+
3
86
  ## [0.5.23] - 2026-08-19
4
87
 
5
88
  ### Added
@@ -34,6 +34,7 @@ export declare const CATEGORY_GLYPH: {
34
34
  readonly themes: "◒";
35
35
  readonly context: "❯";
36
36
  readonly extensions: "⊹";
37
+ readonly canvases: "▤";
37
38
  };
38
39
  export type CategoryKey = keyof typeof CATEGORY_GLYPH;
39
40
  /** A soft dot separator used between footer/summary segments. */
@@ -1 +1 @@
1
- {"version":3,"file":"brand.d.ts","sourceRoot":"","sources":["../../src/core/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA0E;AAC1E,eAAO,MAAM,UAAU,WAAM,CAAC;AAE9B;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;CAUjB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,cAAc,CAAC;AAEtD,iEAAiE;AACjE,eAAO,MAAM,WAAW,WAAM,CAAC;AAE/B,yCAAyC;AACzC,eAAO,MAAM,gBAAgB,WAAM,CAAC","sourcesContent":["/**\n * HooCode brand identity — the single source of truth for the product mark and\n * the glyph vocabulary the interactive surfaces share.\n *\n * The brand *colour* is the theme's `accent` token (see theme/dark.json,\n * theme/light.json) so it stays theme-aware and swappable; this module owns the\n * non-colour identity: the mark and the category glyphs used by the footer,\n * the startup resource summary, and anywhere a capability class is labelled.\n * Keeping them here means a future rebrand touches one file, not twenty.\n *\n * Glyphs are chosen to be single terminal cell, widely supported, and distinct\n * from the task-panel's own status/owner glyphs (◐ ◆ ◇ ▸ ⧉ ✓ ✗ ○) so the two\n * vocabularies never read as the same signal.\n *\n * This lives in `core/` rather than `modes/interactive/` because the `/plugin`\n * command extension labels the same capability classes, and extensions must not\n * reach into a mode's internals. The module is pure data — no theme, no\n * rendering — so it is safe for every surface.\n */\n\n/** The HooCode mark — a filled hexagon, rendered in the accent colour. */\nexport const BRAND_MARK = \"⬢\";\n\n/**\n * Glyphs for the capability classes a session can load. Used by the startup\n * \"resources ready\" summary and reusable anywhere a class needs a label. Each is\n * a single cell so counts and columns stay aligned.\n */\nexport const CATEGORY_GLYPH = {\n\tskills: \"✦\",\n\tcommands: \"⌘\",\n\tagents: \"◈\",\n\tmcp: \"⧉\",\n\tplugins: \"⬡\",\n\tmarketplaces: \"⊞\",\n\tthemes: \"◒\",\n\tcontext: \"❯\",\n\textensions: \"⊹\",\n} as const;\n\nexport type CategoryKey = keyof typeof CATEGORY_GLYPH;\n\n/** A soft dot separator used between footer/summary segments. */\nexport const SEGMENT_SEP = \"·\";\n\n/** Fork glyph preceding a git branch. */\nexport const GIT_BRANCH_GLYPH = \"⑂\";\n"]}
1
+ {"version":3,"file":"brand.d.ts","sourceRoot":"","sources":["../../src/core/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA0E;AAC1E,eAAO,MAAM,UAAU,WAAM,CAAC;AAE9B;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;CAWjB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,cAAc,CAAC;AAEtD,iEAAiE;AACjE,eAAO,MAAM,WAAW,WAAM,CAAC;AAE/B,yCAAyC;AACzC,eAAO,MAAM,gBAAgB,WAAM,CAAC","sourcesContent":["/**\n * HooCode brand identity — the single source of truth for the product mark and\n * the glyph vocabulary the interactive surfaces share.\n *\n * The brand *colour* is the theme's `accent` token (see theme/dark.json,\n * theme/light.json) so it stays theme-aware and swappable; this module owns the\n * non-colour identity: the mark and the category glyphs used by the footer,\n * the startup resource summary, and anywhere a capability class is labelled.\n * Keeping them here means a future rebrand touches one file, not twenty.\n *\n * Glyphs are chosen to be single terminal cell, widely supported, and distinct\n * from the task-panel's own status/owner glyphs (◐ ◆ ◇ ▸ ⧉ ✓ ✗ ○) so the two\n * vocabularies never read as the same signal.\n *\n * This lives in `core/` rather than `modes/interactive/` because the `/plugin`\n * command extension labels the same capability classes, and extensions must not\n * reach into a mode's internals. The module is pure data — no theme, no\n * rendering — so it is safe for every surface.\n */\n\n/** The HooCode mark — a filled hexagon, rendered in the accent colour. */\nexport const BRAND_MARK = \"⬢\";\n\n/**\n * Glyphs for the capability classes a session can load. Used by the startup\n * \"resources ready\" summary and reusable anywhere a class needs a label. Each is\n * a single cell so counts and columns stay aligned.\n */\nexport const CATEGORY_GLYPH = {\n\tskills: \"✦\",\n\tcommands: \"⌘\",\n\tagents: \"◈\",\n\tmcp: \"⧉\",\n\tplugins: \"⬡\",\n\tmarketplaces: \"⊞\",\n\tthemes: \"◒\",\n\tcontext: \"❯\",\n\textensions: \"⊹\",\n\tcanvases: \"▤\",\n} as const;\n\nexport type CategoryKey = keyof typeof CATEGORY_GLYPH;\n\n/** A soft dot separator used between footer/summary segments. */\nexport const SEGMENT_SEP = \"·\";\n\n/** Fork glyph preceding a git branch. */\nexport const GIT_BRANCH_GLYPH = \"⑂\";\n"]}
@@ -34,6 +34,7 @@ export const CATEGORY_GLYPH = {
34
34
  themes: "◒",
35
35
  context: "❯",
36
36
  extensions: "⊹",
37
+ canvases: "▤",
37
38
  };
38
39
  /** A soft dot separator used between footer/summary segments. */
39
40
  export const SEGMENT_SEP = "·";
@@ -1 +1 @@
1
- {"version":3,"file":"brand.js","sourceRoot":"","sources":["../../src/core/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA0E;AAC1E,MAAM,CAAC,MAAM,UAAU,GAAG,KAAG,CAAC;AAE9B;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC7B,MAAM,EAAE,KAAG;IACX,QAAQ,EAAE,KAAG;IACb,MAAM,EAAE,KAAG;IACX,GAAG,EAAE,KAAG;IACR,OAAO,EAAE,KAAG;IACZ,YAAY,EAAE,KAAG;IACjB,MAAM,EAAE,KAAG;IACX,OAAO,EAAE,KAAG;IACZ,UAAU,EAAE,KAAG;CACN,CAAC;AAIX,iEAAiE;AACjE,MAAM,CAAC,MAAM,WAAW,GAAG,IAAG,CAAC;AAE/B,yCAAyC;AACzC,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAG,CAAC","sourcesContent":["/**\n * HooCode brand identity — the single source of truth for the product mark and\n * the glyph vocabulary the interactive surfaces share.\n *\n * The brand *colour* is the theme's `accent` token (see theme/dark.json,\n * theme/light.json) so it stays theme-aware and swappable; this module owns the\n * non-colour identity: the mark and the category glyphs used by the footer,\n * the startup resource summary, and anywhere a capability class is labelled.\n * Keeping them here means a future rebrand touches one file, not twenty.\n *\n * Glyphs are chosen to be single terminal cell, widely supported, and distinct\n * from the task-panel's own status/owner glyphs (◐ ◆ ◇ ▸ ⧉ ✓ ✗ ○) so the two\n * vocabularies never read as the same signal.\n *\n * This lives in `core/` rather than `modes/interactive/` because the `/plugin`\n * command extension labels the same capability classes, and extensions must not\n * reach into a mode's internals. The module is pure data — no theme, no\n * rendering — so it is safe for every surface.\n */\n\n/** The HooCode mark — a filled hexagon, rendered in the accent colour. */\nexport const BRAND_MARK = \"⬢\";\n\n/**\n * Glyphs for the capability classes a session can load. Used by the startup\n * \"resources ready\" summary and reusable anywhere a class needs a label. Each is\n * a single cell so counts and columns stay aligned.\n */\nexport const CATEGORY_GLYPH = {\n\tskills: \"✦\",\n\tcommands: \"⌘\",\n\tagents: \"◈\",\n\tmcp: \"⧉\",\n\tplugins: \"⬡\",\n\tmarketplaces: \"⊞\",\n\tthemes: \"◒\",\n\tcontext: \"❯\",\n\textensions: \"⊹\",\n} as const;\n\nexport type CategoryKey = keyof typeof CATEGORY_GLYPH;\n\n/** A soft dot separator used between footer/summary segments. */\nexport const SEGMENT_SEP = \"·\";\n\n/** Fork glyph preceding a git branch. */\nexport const GIT_BRANCH_GLYPH = \"⑂\";\n"]}
1
+ {"version":3,"file":"brand.js","sourceRoot":"","sources":["../../src/core/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA0E;AAC1E,MAAM,CAAC,MAAM,UAAU,GAAG,KAAG,CAAC;AAE9B;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC7B,MAAM,EAAE,KAAG;IACX,QAAQ,EAAE,KAAG;IACb,MAAM,EAAE,KAAG;IACX,GAAG,EAAE,KAAG;IACR,OAAO,EAAE,KAAG;IACZ,YAAY,EAAE,KAAG;IACjB,MAAM,EAAE,KAAG;IACX,OAAO,EAAE,KAAG;IACZ,UAAU,EAAE,KAAG;IACf,QAAQ,EAAE,KAAG;CACJ,CAAC;AAIX,iEAAiE;AACjE,MAAM,CAAC,MAAM,WAAW,GAAG,IAAG,CAAC;AAE/B,yCAAyC;AACzC,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAG,CAAC","sourcesContent":["/**\n * HooCode brand identity — the single source of truth for the product mark and\n * the glyph vocabulary the interactive surfaces share.\n *\n * The brand *colour* is the theme's `accent` token (see theme/dark.json,\n * theme/light.json) so it stays theme-aware and swappable; this module owns the\n * non-colour identity: the mark and the category glyphs used by the footer,\n * the startup resource summary, and anywhere a capability class is labelled.\n * Keeping them here means a future rebrand touches one file, not twenty.\n *\n * Glyphs are chosen to be single terminal cell, widely supported, and distinct\n * from the task-panel's own status/owner glyphs (◐ ◆ ◇ ▸ ⧉ ✓ ✗ ○) so the two\n * vocabularies never read as the same signal.\n *\n * This lives in `core/` rather than `modes/interactive/` because the `/plugin`\n * command extension labels the same capability classes, and extensions must not\n * reach into a mode's internals. The module is pure data — no theme, no\n * rendering — so it is safe for every surface.\n */\n\n/** The HooCode mark — a filled hexagon, rendered in the accent colour. */\nexport const BRAND_MARK = \"⬢\";\n\n/**\n * Glyphs for the capability classes a session can load. Used by the startup\n * \"resources ready\" summary and reusable anywhere a class needs a label. Each is\n * a single cell so counts and columns stay aligned.\n */\nexport const CATEGORY_GLYPH = {\n\tskills: \"✦\",\n\tcommands: \"⌘\",\n\tagents: \"◈\",\n\tmcp: \"⧉\",\n\tplugins: \"⬡\",\n\tmarketplaces: \"⊞\",\n\tthemes: \"◒\",\n\tcontext: \"❯\",\n\textensions: \"⊹\",\n\tcanvases: \"▤\",\n} as const;\n\nexport type CategoryKey = keyof typeof CATEGORY_GLYPH;\n\n/** A soft dot separator used between footer/summary segments. */\nexport const SEGMENT_SEP = \"·\";\n\n/** Fork glyph preceding a git branch. */\nexport const GIT_BRANCH_GLYPH = \"⑂\";\n"]}
@@ -53,8 +53,16 @@ export interface DiscoveredCanvasExtension {
53
53
  */
54
54
  export declare function canvasSearchRoots(cwd: string, homeDir: string): CanvasSearchRoot[];
55
55
  /**
56
- * Discover canvas extensions across `roots`. First root wins on an id collision,
57
- * so a project-local extension shadows a same-named user-scope one.
56
+ * Discover canvas extensions across `roots`, then append `extra`. First wins on
57
+ * an id collision, so a project-local extension shadows a same-named user-scope
58
+ * one — and anything a person placed in a search root by hand shadows a
59
+ * same-named canvas that arrived inside a package.
60
+ *
61
+ * `extra` is how plugin-shipped canvases join the same list (see
62
+ * `plugin-canvases.ts`): a plugin's canvas directories are named by its manifest
63
+ * rather than by their position under a search root, so they cannot be expressed
64
+ * as a root — but once resolved they are ordinary discovered extensions, and
65
+ * every consumer downstream is better off not knowing the difference.
58
66
  */
59
- export declare function discoverCanvasExtensions(roots: CanvasSearchRoot[]): DiscoveredCanvasExtension[];
67
+ export declare function discoverCanvasExtensions(roots: CanvasSearchRoot[], extra?: readonly DiscoveredCanvasExtension[]): DiscoveredCanvasExtension[];
60
68
  //# sourceMappingURL=discovery.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,gCAAgC;AAChC,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD,+DAA+D;AAC/D,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;AAExD,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;CACnB;AAED,gCAAgC;AAChC,MAAM,WAAW,yBAAyB;IACzC,uDAAuD;IACvD,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,WAAW,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAMlF;AAqBD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,yBAAyB,EAAE,CAW/F","sourcesContent":["/**\n * Canvas extension discovery.\n *\n * Design: `docs/canvas-extensions-design.md` §4.1, §4.3. GitHub's\n * `docs/extensions.md` is specific: the CLI \"scans `.github/extensions/`\n * (project) and the user's copilot config extensions directory for\n * subdirectories containing `extension.mjs`\", the entry file is required and must\n * be named `extension.mjs`, and only ES modules are supported. All 23 extensions\n * in `github/awesome-copilot` comply.\n *\n * So discovery keys off `<dir>/extension.mjs` and nothing else. In particular it\n * does not read `package.json`: 3 of those 23 extensions ship without one, none\n * ships `node_modules`, and where a `package.json` does exist its `main` is\n * always `extension.mjs` anyway. Keying off it would add a failure mode without\n * adding information.\n *\n * The extension id is the directory name, matching how the catalog and the\n * Copilot app refer to extensions (`pr-artifact-explorer`).\n *\n * Some extensions also carry a `copilot-extension.json`, but it is neither\n * required nor informative: 8 of the 23 catalog extensions have one, every\n * instance holds exactly `{ name, version }`, and `name` always equals the\n * directory name. Reading it would add a parse and a disagreement case without\n * telling us anything the directory name does not.\n */\n\nimport { readdirSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\n\n/** Required entry file name. */\nexport const CANVAS_ENTRY_FILE = \"extension.mjs\";\n\n/** Where a canvas extension came from, in precedence order. */\nexport type CanvasScope = \"agents\" | \"project\" | \"user\";\n\n/** Directory searched for canvas extensions. */\nexport interface CanvasSearchRoot {\n\tdir: string;\n\tscope: CanvasScope;\n}\n\n/** One discovered extension. */\nexport interface DiscoveredCanvasExtension {\n\t/** Directory name, used as the provider identifier. */\n\tid: string;\n\t/** Absolute path to the extension directory. */\n\tdir: string;\n\t/** Absolute path to `extension.mjs`. */\n\tentry: string;\n\tscope: CanvasScope;\n}\n\n/**\n * Search roots in precedence order. `.agents/` first per\n * `docs/plugin-format-mapping.md` §0; the Copilot conventions follow as\n * compatibility inputs.\n *\n * `.github/extensions/` travels with a clone, so anything found there is subject\n * to the workspace-trust gate before it is ever forked (design doc §5). Discovery\n * itself is read-only and always safe to run.\n */\nexport function canvasSearchRoots(cwd: string, homeDir: string): CanvasSearchRoot[] {\n\treturn [\n\t\t{ dir: path.join(cwd, \".agents\", \"extensions\"), scope: \"agents\" },\n\t\t{ dir: path.join(cwd, \".github\", \"extensions\"), scope: \"project\" },\n\t\t{ dir: path.join(homeDir, \".copilot\", \"extensions\"), scope: \"user\" },\n\t];\n}\n\nfunction subdirectories(dir: string): string[] {\n\ttry {\n\t\treturn readdirSync(dir, { withFileTypes: true })\n\t\t\t.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())\n\t\t\t.map((entry) => entry.name)\n\t\t\t.sort();\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nfunction hasEntryFile(dir: string): boolean {\n\ttry {\n\t\treturn statSync(path.join(dir, CANVAS_ENTRY_FILE)).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Discover canvas extensions across `roots`. First root wins on an id collision,\n * so a project-local extension shadows a same-named user-scope one.\n */\nexport function discoverCanvasExtensions(roots: CanvasSearchRoot[]): DiscoveredCanvasExtension[] {\n\tconst found = new Map<string, DiscoveredCanvasExtension>();\n\tfor (const root of roots) {\n\t\tfor (const id of subdirectories(root.dir)) {\n\t\t\tif (found.has(id)) continue;\n\t\t\tconst dir = path.join(root.dir, id);\n\t\t\tif (!hasEntryFile(dir)) continue;\n\t\t\tfound.set(id, { id, dir, entry: path.join(dir, CANVAS_ENTRY_FILE), scope: root.scope });\n\t\t}\n\t}\n\treturn [...found.values()];\n}\n"]}
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,gCAAgC;AAChC,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD,+DAA+D;AAC/D,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;AAExD,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;CACnB;AAED,gCAAgC;AAChC,MAAM,WAAW,yBAAyB;IACzC,uDAAuD;IACvD,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,WAAW,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAMlF;AAqBD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACvC,KAAK,EAAE,gBAAgB,EAAE,EACzB,KAAK,GAAE,SAAS,yBAAyB,EAAO,GAC9C,yBAAyB,EAAE,CAgB7B","sourcesContent":["/**\n * Canvas extension discovery.\n *\n * Design: `docs/canvas-extensions-design.md` §4.1, §4.3. GitHub's\n * `docs/extensions.md` is specific: the CLI \"scans `.github/extensions/`\n * (project) and the user's copilot config extensions directory for\n * subdirectories containing `extension.mjs`\", the entry file is required and must\n * be named `extension.mjs`, and only ES modules are supported. All 23 extensions\n * in `github/awesome-copilot` comply.\n *\n * So discovery keys off `<dir>/extension.mjs` and nothing else. In particular it\n * does not read `package.json`: 3 of those 23 extensions ship without one, none\n * ships `node_modules`, and where a `package.json` does exist its `main` is\n * always `extension.mjs` anyway. Keying off it would add a failure mode without\n * adding information.\n *\n * The extension id is the directory name, matching how the catalog and the\n * Copilot app refer to extensions (`pr-artifact-explorer`).\n *\n * Some extensions also carry a `copilot-extension.json`, but it is neither\n * required nor informative: 8 of the 23 catalog extensions have one, every\n * instance holds exactly `{ name, version }`, and `name` always equals the\n * directory name. Reading it would add a parse and a disagreement case without\n * telling us anything the directory name does not.\n */\n\nimport { readdirSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\n\n/** Required entry file name. */\nexport const CANVAS_ENTRY_FILE = \"extension.mjs\";\n\n/** Where a canvas extension came from, in precedence order. */\nexport type CanvasScope = \"agents\" | \"project\" | \"user\";\n\n/** Directory searched for canvas extensions. */\nexport interface CanvasSearchRoot {\n\tdir: string;\n\tscope: CanvasScope;\n}\n\n/** One discovered extension. */\nexport interface DiscoveredCanvasExtension {\n\t/** Directory name, used as the provider identifier. */\n\tid: string;\n\t/** Absolute path to the extension directory. */\n\tdir: string;\n\t/** Absolute path to `extension.mjs`. */\n\tentry: string;\n\tscope: CanvasScope;\n}\n\n/**\n * Search roots in precedence order. `.agents/` first per\n * `docs/plugin-format-mapping.md` §0; the Copilot conventions follow as\n * compatibility inputs.\n *\n * `.github/extensions/` travels with a clone, so anything found there is subject\n * to the workspace-trust gate before it is ever forked (design doc §5). Discovery\n * itself is read-only and always safe to run.\n */\nexport function canvasSearchRoots(cwd: string, homeDir: string): CanvasSearchRoot[] {\n\treturn [\n\t\t{ dir: path.join(cwd, \".agents\", \"extensions\"), scope: \"agents\" },\n\t\t{ dir: path.join(cwd, \".github\", \"extensions\"), scope: \"project\" },\n\t\t{ dir: path.join(homeDir, \".copilot\", \"extensions\"), scope: \"user\" },\n\t];\n}\n\nfunction subdirectories(dir: string): string[] {\n\ttry {\n\t\treturn readdirSync(dir, { withFileTypes: true })\n\t\t\t.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())\n\t\t\t.map((entry) => entry.name)\n\t\t\t.sort();\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nfunction hasEntryFile(dir: string): boolean {\n\ttry {\n\t\treturn statSync(path.join(dir, CANVAS_ENTRY_FILE)).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Discover canvas extensions across `roots`, then append `extra`. First wins on\n * an id collision, so a project-local extension shadows a same-named user-scope\n * one — and anything a person placed in a search root by hand shadows a\n * same-named canvas that arrived inside a package.\n *\n * `extra` is how plugin-shipped canvases join the same list (see\n * `plugin-canvases.ts`): a plugin's canvas directories are named by its manifest\n * rather than by their position under a search root, so they cannot be expressed\n * as a root — but once resolved they are ordinary discovered extensions, and\n * every consumer downstream is better off not knowing the difference.\n */\nexport function discoverCanvasExtensions(\n\troots: CanvasSearchRoot[],\n\textra: readonly DiscoveredCanvasExtension[] = [],\n): DiscoveredCanvasExtension[] {\n\tconst found = new Map<string, DiscoveredCanvasExtension>();\n\tfor (const root of roots) {\n\t\tfor (const id of subdirectories(root.dir)) {\n\t\t\tif (found.has(id)) continue;\n\t\t\tconst dir = path.join(root.dir, id);\n\t\t\tif (!hasEntryFile(dir)) continue;\n\t\t\tfound.set(id, { id, dir, entry: path.join(dir, CANVAS_ENTRY_FILE), scope: root.scope });\n\t\t}\n\t}\n\tfor (const extension of extra) {\n\t\tif (found.has(extension.id)) continue;\n\t\tif (!hasEntryFile(extension.dir)) continue;\n\t\tfound.set(extension.id, extension);\n\t}\n\treturn [...found.values()];\n}\n"]}
@@ -63,10 +63,18 @@ function hasEntryFile(dir) {
63
63
  }
64
64
  }
65
65
  /**
66
- * Discover canvas extensions across `roots`. First root wins on an id collision,
67
- * so a project-local extension shadows a same-named user-scope one.
66
+ * Discover canvas extensions across `roots`, then append `extra`. First wins on
67
+ * an id collision, so a project-local extension shadows a same-named user-scope
68
+ * one — and anything a person placed in a search root by hand shadows a
69
+ * same-named canvas that arrived inside a package.
70
+ *
71
+ * `extra` is how plugin-shipped canvases join the same list (see
72
+ * `plugin-canvases.ts`): a plugin's canvas directories are named by its manifest
73
+ * rather than by their position under a search root, so they cannot be expressed
74
+ * as a root — but once resolved they are ordinary discovered extensions, and
75
+ * every consumer downstream is better off not knowing the difference.
68
76
  */
69
- export function discoverCanvasExtensions(roots) {
77
+ export function discoverCanvasExtensions(roots, extra = []) {
70
78
  const found = new Map();
71
79
  for (const root of roots) {
72
80
  for (const id of subdirectories(root.dir)) {
@@ -78,6 +86,13 @@ export function discoverCanvasExtensions(roots) {
78
86
  found.set(id, { id, dir, entry: path.join(dir, CANVAS_ENTRY_FILE), scope: root.scope });
79
87
  }
80
88
  }
89
+ for (const extension of extra) {
90
+ if (found.has(extension.id))
91
+ continue;
92
+ if (!hasEntryFile(extension.dir))
93
+ continue;
94
+ found.set(extension.id, extension);
95
+ }
81
96
  return [...found.values()];
82
97
  }
83
98
  //# sourceMappingURL=discovery.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../src/core/canvas/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,gCAAgC;AAChC,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAsBjD;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,OAAe,EAAsB;IACnF,OAAO;QACN,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE;QACjE,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE;QAClE,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;KACpE,CAAC;AAAA,CACF;AAED,SAAS,cAAc,CAAC,GAAW,EAAY;IAC9C,IAAI,CAAC;QACJ,OAAO,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;aAC9C,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;aAChE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;aAC1B,IAAI,EAAE,CAAC;IACV,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;IACX,CAAC;AAAA,CACD;AAED,SAAS,YAAY,CAAC,GAAW,EAAW;IAC3C,IAAI,CAAC;QACJ,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;AAAA,CACD;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAyB,EAA+B;IAChG,MAAM,KAAK,GAAG,IAAI,GAAG,EAAqC,CAAC;IAC3D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,MAAM,EAAE,IAAI,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3C,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,SAAS;YAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACpC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;gBAAE,SAAS;YACjC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QACzF,CAAC;IACF,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;AAAA,CAC3B","sourcesContent":["/**\n * Canvas extension discovery.\n *\n * Design: `docs/canvas-extensions-design.md` §4.1, §4.3. GitHub's\n * `docs/extensions.md` is specific: the CLI \"scans `.github/extensions/`\n * (project) and the user's copilot config extensions directory for\n * subdirectories containing `extension.mjs`\", the entry file is required and must\n * be named `extension.mjs`, and only ES modules are supported. All 23 extensions\n * in `github/awesome-copilot` comply.\n *\n * So discovery keys off `<dir>/extension.mjs` and nothing else. In particular it\n * does not read `package.json`: 3 of those 23 extensions ship without one, none\n * ships `node_modules`, and where a `package.json` does exist its `main` is\n * always `extension.mjs` anyway. Keying off it would add a failure mode without\n * adding information.\n *\n * The extension id is the directory name, matching how the catalog and the\n * Copilot app refer to extensions (`pr-artifact-explorer`).\n *\n * Some extensions also carry a `copilot-extension.json`, but it is neither\n * required nor informative: 8 of the 23 catalog extensions have one, every\n * instance holds exactly `{ name, version }`, and `name` always equals the\n * directory name. Reading it would add a parse and a disagreement case without\n * telling us anything the directory name does not.\n */\n\nimport { readdirSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\n\n/** Required entry file name. */\nexport const CANVAS_ENTRY_FILE = \"extension.mjs\";\n\n/** Where a canvas extension came from, in precedence order. */\nexport type CanvasScope = \"agents\" | \"project\" | \"user\";\n\n/** Directory searched for canvas extensions. */\nexport interface CanvasSearchRoot {\n\tdir: string;\n\tscope: CanvasScope;\n}\n\n/** One discovered extension. */\nexport interface DiscoveredCanvasExtension {\n\t/** Directory name, used as the provider identifier. */\n\tid: string;\n\t/** Absolute path to the extension directory. */\n\tdir: string;\n\t/** Absolute path to `extension.mjs`. */\n\tentry: string;\n\tscope: CanvasScope;\n}\n\n/**\n * Search roots in precedence order. `.agents/` first per\n * `docs/plugin-format-mapping.md` §0; the Copilot conventions follow as\n * compatibility inputs.\n *\n * `.github/extensions/` travels with a clone, so anything found there is subject\n * to the workspace-trust gate before it is ever forked (design doc §5). Discovery\n * itself is read-only and always safe to run.\n */\nexport function canvasSearchRoots(cwd: string, homeDir: string): CanvasSearchRoot[] {\n\treturn [\n\t\t{ dir: path.join(cwd, \".agents\", \"extensions\"), scope: \"agents\" },\n\t\t{ dir: path.join(cwd, \".github\", \"extensions\"), scope: \"project\" },\n\t\t{ dir: path.join(homeDir, \".copilot\", \"extensions\"), scope: \"user\" },\n\t];\n}\n\nfunction subdirectories(dir: string): string[] {\n\ttry {\n\t\treturn readdirSync(dir, { withFileTypes: true })\n\t\t\t.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())\n\t\t\t.map((entry) => entry.name)\n\t\t\t.sort();\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nfunction hasEntryFile(dir: string): boolean {\n\ttry {\n\t\treturn statSync(path.join(dir, CANVAS_ENTRY_FILE)).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Discover canvas extensions across `roots`. First root wins on an id collision,\n * so a project-local extension shadows a same-named user-scope one.\n */\nexport function discoverCanvasExtensions(roots: CanvasSearchRoot[]): DiscoveredCanvasExtension[] {\n\tconst found = new Map<string, DiscoveredCanvasExtension>();\n\tfor (const root of roots) {\n\t\tfor (const id of subdirectories(root.dir)) {\n\t\t\tif (found.has(id)) continue;\n\t\t\tconst dir = path.join(root.dir, id);\n\t\t\tif (!hasEntryFile(dir)) continue;\n\t\t\tfound.set(id, { id, dir, entry: path.join(dir, CANVAS_ENTRY_FILE), scope: root.scope });\n\t\t}\n\t}\n\treturn [...found.values()];\n}\n"]}
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../src/core/canvas/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,gCAAgC;AAChC,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAsBjD;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,OAAe,EAAsB;IACnF,OAAO;QACN,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE;QACjE,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE;QAClE,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;KACpE,CAAC;AAAA,CACF;AAED,SAAS,cAAc,CAAC,GAAW,EAAY;IAC9C,IAAI,CAAC;QACJ,OAAO,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;aAC9C,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;aAChE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;aAC1B,IAAI,EAAE,CAAC;IACV,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;IACX,CAAC;AAAA,CACD;AAED,SAAS,YAAY,CAAC,GAAW,EAAW;IAC3C,IAAI,CAAC;QACJ,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;AAAA,CACD;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CACvC,KAAyB,EACzB,KAAK,GAAyC,EAAE,EAClB;IAC9B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAqC,CAAC;IAC3D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,MAAM,EAAE,IAAI,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3C,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,SAAS;YAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACpC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;gBAAE,SAAS;YACjC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QACzF,CAAC;IACF,CAAC;IACD,KAAK,MAAM,SAAS,IAAI,KAAK,EAAE,CAAC;QAC/B,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAAE,SAAS;QACtC,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,GAAG,CAAC;YAAE,SAAS;QAC3C,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;AAAA,CAC3B","sourcesContent":["/**\n * Canvas extension discovery.\n *\n * Design: `docs/canvas-extensions-design.md` §4.1, §4.3. GitHub's\n * `docs/extensions.md` is specific: the CLI \"scans `.github/extensions/`\n * (project) and the user's copilot config extensions directory for\n * subdirectories containing `extension.mjs`\", the entry file is required and must\n * be named `extension.mjs`, and only ES modules are supported. All 23 extensions\n * in `github/awesome-copilot` comply.\n *\n * So discovery keys off `<dir>/extension.mjs` and nothing else. In particular it\n * does not read `package.json`: 3 of those 23 extensions ship without one, none\n * ships `node_modules`, and where a `package.json` does exist its `main` is\n * always `extension.mjs` anyway. Keying off it would add a failure mode without\n * adding information.\n *\n * The extension id is the directory name, matching how the catalog and the\n * Copilot app refer to extensions (`pr-artifact-explorer`).\n *\n * Some extensions also carry a `copilot-extension.json`, but it is neither\n * required nor informative: 8 of the 23 catalog extensions have one, every\n * instance holds exactly `{ name, version }`, and `name` always equals the\n * directory name. Reading it would add a parse and a disagreement case without\n * telling us anything the directory name does not.\n */\n\nimport { readdirSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\n\n/** Required entry file name. */\nexport const CANVAS_ENTRY_FILE = \"extension.mjs\";\n\n/** Where a canvas extension came from, in precedence order. */\nexport type CanvasScope = \"agents\" | \"project\" | \"user\";\n\n/** Directory searched for canvas extensions. */\nexport interface CanvasSearchRoot {\n\tdir: string;\n\tscope: CanvasScope;\n}\n\n/** One discovered extension. */\nexport interface DiscoveredCanvasExtension {\n\t/** Directory name, used as the provider identifier. */\n\tid: string;\n\t/** Absolute path to the extension directory. */\n\tdir: string;\n\t/** Absolute path to `extension.mjs`. */\n\tentry: string;\n\tscope: CanvasScope;\n}\n\n/**\n * Search roots in precedence order. `.agents/` first per\n * `docs/plugin-format-mapping.md` §0; the Copilot conventions follow as\n * compatibility inputs.\n *\n * `.github/extensions/` travels with a clone, so anything found there is subject\n * to the workspace-trust gate before it is ever forked (design doc §5). Discovery\n * itself is read-only and always safe to run.\n */\nexport function canvasSearchRoots(cwd: string, homeDir: string): CanvasSearchRoot[] {\n\treturn [\n\t\t{ dir: path.join(cwd, \".agents\", \"extensions\"), scope: \"agents\" },\n\t\t{ dir: path.join(cwd, \".github\", \"extensions\"), scope: \"project\" },\n\t\t{ dir: path.join(homeDir, \".copilot\", \"extensions\"), scope: \"user\" },\n\t];\n}\n\nfunction subdirectories(dir: string): string[] {\n\ttry {\n\t\treturn readdirSync(dir, { withFileTypes: true })\n\t\t\t.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())\n\t\t\t.map((entry) => entry.name)\n\t\t\t.sort();\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nfunction hasEntryFile(dir: string): boolean {\n\ttry {\n\t\treturn statSync(path.join(dir, CANVAS_ENTRY_FILE)).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Discover canvas extensions across `roots`, then append `extra`. First wins on\n * an id collision, so a project-local extension shadows a same-named user-scope\n * one — and anything a person placed in a search root by hand shadows a\n * same-named canvas that arrived inside a package.\n *\n * `extra` is how plugin-shipped canvases join the same list (see\n * `plugin-canvases.ts`): a plugin's canvas directories are named by its manifest\n * rather than by their position under a search root, so they cannot be expressed\n * as a root — but once resolved they are ordinary discovered extensions, and\n * every consumer downstream is better off not knowing the difference.\n */\nexport function discoverCanvasExtensions(\n\troots: CanvasSearchRoot[],\n\textra: readonly DiscoveredCanvasExtension[] = [],\n): DiscoveredCanvasExtension[] {\n\tconst found = new Map<string, DiscoveredCanvasExtension>();\n\tfor (const root of roots) {\n\t\tfor (const id of subdirectories(root.dir)) {\n\t\t\tif (found.has(id)) continue;\n\t\t\tconst dir = path.join(root.dir, id);\n\t\t\tif (!hasEntryFile(dir)) continue;\n\t\t\tfound.set(id, { id, dir, entry: path.join(dir, CANVAS_ENTRY_FILE), scope: root.scope });\n\t\t}\n\t}\n\tfor (const extension of extra) {\n\t\tif (found.has(extension.id)) continue;\n\t\tif (!hasEntryFile(extension.dir)) continue;\n\t\tfound.set(extension.id, extension);\n\t}\n\treturn [...found.values()];\n}\n"]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Canvas extensions that arrive inside an installed plugin.
3
+ *
4
+ * Design: `docs/canvas-extensions-design.md` §4.3. Discovery's three search roots
5
+ * (`.agents/extensions`, `.github/extensions`, `~/.copilot/extensions`) describe
6
+ * where a person *puts* a canvas by hand. They say nothing about where a package
7
+ * manager puts one, and a plugin install lands in `~/.agents/plugins/<name>/`
8
+ * instead — so a canvas plugin installed exactly as instructed was copied to disk
9
+ * intact and then never listed, because nothing looked there.
10
+ *
11
+ * The plugin readers already resolve each plugin's canvas directories
12
+ * (`NormalizedPlugin.canvasExtensions`), so this module is only the join: read
13
+ * the plugins on the discovery path, and present their canvases in the shape
14
+ * `discovery.ts` produces for everything else. Keeping the shape identical is
15
+ * what lets the trust gate, the listing and `open` stay unaware that a plugin was
16
+ * involved at all.
17
+ */
18
+ import { type DiscoveredCanvasExtension } from "./discovery.js";
19
+ /**
20
+ * Canvases shipped by every plugin on the discovery path.
21
+ *
22
+ * Scope is derived from where the plugin lives, not from the canvas directory:
23
+ * a plugin installed at project scope (or committed into the working tree) is
24
+ * repository-supplied whichever subdirectory its canvas sits in, and that is the
25
+ * question the trust gate asks. `pluginScopeOf`'s third answer, `repo`, folds
26
+ * into `project` here — for a canvas the two mean the same thing, since neither
27
+ * can distinguish "I put this here" from "this arrived in the clone".
28
+ */
29
+ export declare function pluginCanvasExtensions(cwd: string, agentDir?: string): DiscoveredCanvasExtension[];
30
+ //# sourceMappingURL=plugin-canvases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-canvases.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/plugin-canvases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,OAAO,EAAqB,KAAK,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAEnF;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,yBAAyB,EAAE,CAejH","sourcesContent":["/**\n * Canvas extensions that arrive inside an installed plugin.\n *\n * Design: `docs/canvas-extensions-design.md` §4.3. Discovery's three search roots\n * (`.agents/extensions`, `.github/extensions`, `~/.copilot/extensions`) describe\n * where a person *puts* a canvas by hand. They say nothing about where a package\n * manager puts one, and a plugin install lands in `~/.agents/plugins/<name>/`\n * instead — so a canvas plugin installed exactly as instructed was copied to disk\n * intact and then never listed, because nothing looked there.\n *\n * The plugin readers already resolve each plugin's canvas directories\n * (`NormalizedPlugin.canvasExtensions`), so this module is only the join: read\n * the plugins on the discovery path, and present their canvases in the shape\n * `discovery.ts` produces for everything else. Keeping the shape identical is\n * what lets the trust gate, the listing and `open` stay unaware that a plugin was\n * involved at all.\n */\n\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { defaultPluginDirs } from \"../extensions/loader.js\";\nimport { discoverPlugins } from \"../extensions/plugins/index.js\";\nimport { pluginScopeOf } from \"../extensions/plugins/locations.js\";\nimport { CANVAS_ENTRY_FILE, type DiscoveredCanvasExtension } from \"./discovery.js\";\n\n/**\n * Canvases shipped by every plugin on the discovery path.\n *\n * Scope is derived from where the plugin lives, not from the canvas directory:\n * a plugin installed at project scope (or committed into the working tree) is\n * repository-supplied whichever subdirectory its canvas sits in, and that is the\n * question the trust gate asks. `pluginScopeOf`'s third answer, `repo`, folds\n * into `project` here — for a canvas the two mean the same thing, since neither\n * can distinguish \"I put this here\" from \"this arrived in the clone\".\n */\nexport function pluginCanvasExtensions(cwd: string, agentDir: string = getAgentDir()): DiscoveredCanvasExtension[] {\n\tconst found: DiscoveredCanvasExtension[] = [];\n\tfor (const plugin of discoverPlugins(defaultPluginDirs(cwd, agentDir))) {\n\t\tif (!plugin.canvasExtensions?.length) continue;\n\t\tconst scope = pluginScopeOf(plugin.root, cwd) === \"user\" ? \"user\" : \"project\";\n\t\tfor (const extension of plugin.canvasExtensions) {\n\t\t\tfound.push({\n\t\t\t\tid: extension.id,\n\t\t\t\tdir: extension.dir,\n\t\t\t\tentry: path.join(extension.dir, CANVAS_ENTRY_FILE),\n\t\t\t\tscope,\n\t\t\t});\n\t\t}\n\t}\n\treturn found;\n}\n"]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Canvas extensions that arrive inside an installed plugin.
3
+ *
4
+ * Design: `docs/canvas-extensions-design.md` §4.3. Discovery's three search roots
5
+ * (`.agents/extensions`, `.github/extensions`, `~/.copilot/extensions`) describe
6
+ * where a person *puts* a canvas by hand. They say nothing about where a package
7
+ * manager puts one, and a plugin install lands in `~/.agents/plugins/<name>/`
8
+ * instead — so a canvas plugin installed exactly as instructed was copied to disk
9
+ * intact and then never listed, because nothing looked there.
10
+ *
11
+ * The plugin readers already resolve each plugin's canvas directories
12
+ * (`NormalizedPlugin.canvasExtensions`), so this module is only the join: read
13
+ * the plugins on the discovery path, and present their canvases in the shape
14
+ * `discovery.ts` produces for everything else. Keeping the shape identical is
15
+ * what lets the trust gate, the listing and `open` stay unaware that a plugin was
16
+ * involved at all.
17
+ */
18
+ import * as path from "node:path";
19
+ import { getAgentDir } from "../../config.js";
20
+ import { defaultPluginDirs } from "../extensions/loader.js";
21
+ import { discoverPlugins } from "../extensions/plugins/index.js";
22
+ import { pluginScopeOf } from "../extensions/plugins/locations.js";
23
+ import { CANVAS_ENTRY_FILE } from "./discovery.js";
24
+ /**
25
+ * Canvases shipped by every plugin on the discovery path.
26
+ *
27
+ * Scope is derived from where the plugin lives, not from the canvas directory:
28
+ * a plugin installed at project scope (or committed into the working tree) is
29
+ * repository-supplied whichever subdirectory its canvas sits in, and that is the
30
+ * question the trust gate asks. `pluginScopeOf`'s third answer, `repo`, folds
31
+ * into `project` here — for a canvas the two mean the same thing, since neither
32
+ * can distinguish "I put this here" from "this arrived in the clone".
33
+ */
34
+ export function pluginCanvasExtensions(cwd, agentDir = getAgentDir()) {
35
+ const found = [];
36
+ for (const plugin of discoverPlugins(defaultPluginDirs(cwd, agentDir))) {
37
+ if (!plugin.canvasExtensions?.length)
38
+ continue;
39
+ const scope = pluginScopeOf(plugin.root, cwd) === "user" ? "user" : "project";
40
+ for (const extension of plugin.canvasExtensions) {
41
+ found.push({
42
+ id: extension.id,
43
+ dir: extension.dir,
44
+ entry: path.join(extension.dir, CANVAS_ENTRY_FILE),
45
+ scope,
46
+ });
47
+ }
48
+ }
49
+ return found;
50
+ }
51
+ //# sourceMappingURL=plugin-canvases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-canvases.js","sourceRoot":"","sources":["../../../src/core/canvas/plugin-canvases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAkC,MAAM,gBAAgB,CAAC;AAEnF;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAAW,EAAE,QAAQ,GAAW,WAAW,EAAE,EAA+B;IAClH,MAAM,KAAK,GAAgC,EAAE,CAAC;IAC9C,KAAK,MAAM,MAAM,IAAI,eAAe,CAAC,iBAAiB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC;QACxE,IAAI,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM;YAAE,SAAS;QAC/C,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9E,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;YACjD,KAAK,CAAC,IAAI,CAAC;gBACV,EAAE,EAAE,SAAS,CAAC,EAAE;gBAChB,GAAG,EAAE,SAAS,CAAC,GAAG;gBAClB,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,iBAAiB,CAAC;gBAClD,KAAK;aACL,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb","sourcesContent":["/**\n * Canvas extensions that arrive inside an installed plugin.\n *\n * Design: `docs/canvas-extensions-design.md` §4.3. Discovery's three search roots\n * (`.agents/extensions`, `.github/extensions`, `~/.copilot/extensions`) describe\n * where a person *puts* a canvas by hand. They say nothing about where a package\n * manager puts one, and a plugin install lands in `~/.agents/plugins/<name>/`\n * instead — so a canvas plugin installed exactly as instructed was copied to disk\n * intact and then never listed, because nothing looked there.\n *\n * The plugin readers already resolve each plugin's canvas directories\n * (`NormalizedPlugin.canvasExtensions`), so this module is only the join: read\n * the plugins on the discovery path, and present their canvases in the shape\n * `discovery.ts` produces for everything else. Keeping the shape identical is\n * what lets the trust gate, the listing and `open` stay unaware that a plugin was\n * involved at all.\n */\n\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { defaultPluginDirs } from \"../extensions/loader.js\";\nimport { discoverPlugins } from \"../extensions/plugins/index.js\";\nimport { pluginScopeOf } from \"../extensions/plugins/locations.js\";\nimport { CANVAS_ENTRY_FILE, type DiscoveredCanvasExtension } from \"./discovery.js\";\n\n/**\n * Canvases shipped by every plugin on the discovery path.\n *\n * Scope is derived from where the plugin lives, not from the canvas directory:\n * a plugin installed at project scope (or committed into the working tree) is\n * repository-supplied whichever subdirectory its canvas sits in, and that is the\n * question the trust gate asks. `pluginScopeOf`'s third answer, `repo`, folds\n * into `project` here — for a canvas the two mean the same thing, since neither\n * can distinguish \"I put this here\" from \"this arrived in the clone\".\n */\nexport function pluginCanvasExtensions(cwd: string, agentDir: string = getAgentDir()): DiscoveredCanvasExtension[] {\n\tconst found: DiscoveredCanvasExtension[] = [];\n\tfor (const plugin of discoverPlugins(defaultPluginDirs(cwd, agentDir))) {\n\t\tif (!plugin.canvasExtensions?.length) continue;\n\t\tconst scope = pluginScopeOf(plugin.root, cwd) === \"user\" ? \"user\" : \"project\";\n\t\tfor (const extension of plugin.canvasExtensions) {\n\t\t\tfound.push({\n\t\t\t\tid: extension.id,\n\t\t\t\tdir: extension.dir,\n\t\t\t\tentry: path.join(extension.dir, CANVAS_ENTRY_FILE),\n\t\t\t\tscope,\n\t\t\t});\n\t\t}\n\t}\n\treturn found;\n}\n"]}
@@ -45,6 +45,12 @@ export interface CanvasSessionOptions extends CanvasRegistryEvents {
45
45
  agentDir?: string;
46
46
  /** Override the search roots; defaults to {@link canvasSearchRoots}. */
47
47
  roots?: CanvasSearchRoot[];
48
+ /**
49
+ * Override how plugin-shipped canvases are found; defaults to
50
+ * {@link pluginCanvasExtensions}. Pass `() => []` to look at the search roots
51
+ * and nothing else.
52
+ */
53
+ pluginExtensions?: () => DiscoveredCanvasExtension[];
48
54
  /** Override availability resolution, for tests and for hosts that already know. */
49
55
  resolveRuntime?: () => Promise<CanvasAvailability>;
50
56
  }
@@ -63,6 +69,7 @@ export declare function parseCanvasRef(input: string): CanvasRef | undefined;
63
69
  export declare class CanvasSession {
64
70
  private readonly options;
65
71
  private readonly roots;
72
+ private readonly pluginExtensions;
66
73
  /** Cached because resolving can spawn `node --version` and cannot change mid-session. */
67
74
  private availabilityPromise;
68
75
  /** Created on first successful open, not at construction: listing must not fork. */
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session-scoped canvas facade: everything the TUI needs, with no TUI in it.\n *\n * Design: `docs/canvas-extensions-design.md` §11. The pieces underneath — discovery,\n * the trust gate, availability, the registry — are each small and separately tested.\n * This is what stitches them into the four questions a user surface actually asks:\n * what is there, can it run, open this one, close that one.\n *\n * It holds no TUI types on purpose. `extensions/core/canvas.ts` renders and supplies\n * an `AbortSignal` from a cancellable loader; everything decided here stays testable\n * without a terminal.\n *\n * Availability is resolved once and cached, because resolving can spawn\n * `node --version` (§11.1) and the answer cannot change within a session.\n */\n\nimport { getAgentDir } from \"../../config.js\";\nimport type { DiscoveredCanvasExtension } from \"./discovery.js\";\nimport { type CanvasSearchRoot, canvasSearchRoots, discoverCanvasExtensions } from \"./discovery.js\";\nimport { type CanvasAvailability, resolveCanvasRuntime } from \"./launch.js\";\nimport { type CanvasInstance, CanvasRegistry, type CanvasRegistryEvents } from \"./registry.js\";\nimport type { CanvasCallOptions } from \"./runner.js\";\nimport { gateCanvasExtensions } from \"./trust.js\";\n\n/** One canvas a person could open, or has open. */\nexport interface CanvasListing {\n\textensionId: string;\n\t/** Undefined until the extension has been forked, since declarations come from it. */\n\tcanvasId: string | undefined;\n\tdisplayName: string | undefined;\n\tscope: DiscoveredCanvasExtension[\"scope\"];\n\t/** Why it cannot be opened, if it cannot. */\n\twithheld: \"untrusted-workspace\" | undefined;\n\t/** Instances of this canvas that are currently open. */\n\topen: CanvasInstance[];\n}\n\n/** What `list()` reports. */\nexport interface CanvasOverview {\n\t/** Absent `reason` means canvases can run here. */\n\tavailability: CanvasAvailability;\n\tlistings: CanvasListing[];\n\t/** Extensions withheld by the trust gate — surfaced, never hidden (§5.1). */\n\twithheldCount: number;\n}\n\n/** Configuration for a session's canvas facade. */\nexport interface CanvasSessionOptions extends CanvasRegistryEvents {\n\tcwd: string;\n\thomeDir: string;\n\tagentDir?: string;\n\t/** Override the search roots; defaults to {@link canvasSearchRoots}. */\n\troots?: CanvasSearchRoot[];\n\t/** Override availability resolution, for tests and for hosts that already know. */\n\tresolveRuntime?: () => Promise<CanvasAvailability>;\n}\n\n/** Reference to a canvas: an extension id, optionally narrowed to one of its canvases. */\nexport interface CanvasRef {\n\textensionId: string;\n\tcanvasId?: string;\n}\n\n/**\n * Parse `extension` or `extension:canvas`.\n *\n * Extension ids are directory names and canvas ids are provider-local, so a single\n * colon is unambiguous and needs no quoting.\n */\nexport function parseCanvasRef(input: string): CanvasRef | undefined {\n\tconst trimmed = input.trim();\n\tif (trimmed.length === 0) return undefined;\n\tconst colon = trimmed.indexOf(\":\");\n\tif (colon === -1) return { extensionId: trimmed };\n\tconst extensionId = trimmed.slice(0, colon).trim();\n\tconst canvasId = trimmed.slice(colon + 1).trim();\n\tif (extensionId.length === 0 || canvasId.length === 0) return undefined;\n\treturn { extensionId, canvasId };\n}\n\nexport class CanvasSession {\n\tprivate readonly options: CanvasSessionOptions;\n\tprivate readonly roots: CanvasSearchRoot[];\n\t/** Cached because resolving can spawn `node --version` and cannot change mid-session. */\n\tprivate availabilityPromise: Promise<CanvasAvailability> | undefined;\n\t/** Created on first successful open, not at construction: listing must not fork. */\n\tprivate registry: CanvasRegistry | undefined;\n\n\tconstructor(options: CanvasSessionOptions) {\n\t\tthis.options = options;\n\t\tthis.roots = options.roots ?? canvasSearchRoots(options.cwd, options.homeDir);\n\t}\n\n\t/** Discovered extensions, partitioned by the trust gate. Read-only and always safe. */\n\tdiscover(): { runnable: DiscoveredCanvasExtension[]; withheld: DiscoveredCanvasExtension[] } {\n\t\tconst gated = gateCanvasExtensions(\n\t\t\tdiscoverCanvasExtensions(this.roots),\n\t\t\tthis.options.cwd,\n\t\t\tthis.options.agentDir ?? getAgentDir(),\n\t\t);\n\t\treturn { runnable: gated.runnable, withheld: gated.withheld.map((entry) => entry.extension) };\n\t}\n\n\t/** Whether canvases can run here. Resolved once per session and cached. */\n\tasync availability(): Promise<CanvasAvailability> {\n\t\tthis.availabilityPromise ??= (this.options.resolveRuntime ?? resolveCanvasRuntime)();\n\t\treturn this.availabilityPromise;\n\t}\n\n\t/**\n\t * What is installed, what is open, and what is being withheld.\n\t *\n\t * Deliberately does not fork anything: listing must stay free and safe, so a\n\t * `canvasId` is only known for extensions already running. That is the visible\n\t * consequence of a canvas having no passive half (§5.1) — even its name comes from\n\t * running its code.\n\t */\n\tasync list(): Promise<CanvasOverview> {\n\t\tconst availability = await this.availability();\n\t\tconst { runnable, withheld } = this.discover();\n\t\tconst open = this.registryOrUndefined()?.listInstances() ?? [];\n\n\t\tconst listings: CanvasListing[] = [];\n\t\tfor (const extension of runnable) {\n\t\t\tconst instances = open.filter((instance) => instance.extensionId === extension.id);\n\t\t\tif (instances.length === 0) {\n\t\t\t\tlistings.push({\n\t\t\t\t\textensionId: extension.id,\n\t\t\t\t\tcanvasId: undefined,\n\t\t\t\t\tdisplayName: undefined,\n\t\t\t\t\tscope: extension.scope,\n\t\t\t\t\twithheld: undefined,\n\t\t\t\t\topen: [],\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const canvasId of new Set(instances.map((instance) => instance.canvasId))) {\n\t\t\t\tconst forCanvas = instances.filter((instance) => instance.canvasId === canvasId);\n\t\t\t\tlistings.push({\n\t\t\t\t\textensionId: extension.id,\n\t\t\t\t\tcanvasId,\n\t\t\t\t\tdisplayName: forCanvas[0]?.title,\n\t\t\t\t\tscope: extension.scope,\n\t\t\t\t\twithheld: undefined,\n\t\t\t\t\topen: forCanvas,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfor (const extension of withheld) {\n\t\t\tlistings.push({\n\t\t\t\textensionId: extension.id,\n\t\t\t\tcanvasId: undefined,\n\t\t\t\tdisplayName: undefined,\n\t\t\t\tscope: extension.scope,\n\t\t\t\twithheld: \"untrusted-workspace\",\n\t\t\t\topen: [],\n\t\t\t});\n\t\t}\n\t\treturn { availability, listings, withheldCount: withheld.length };\n\t}\n\n\t/**\n\t * Open a canvas.\n\t *\n\t * `options.signal` comes from the caller's cancellable loader, so a person's Esc\n\t * reaches the registry's abandon path (§11.6) rather than merely hiding a spinner.\n\t */\n\tasync open(ref: CanvasRef, options?: CanvasCallOptions): Promise<CanvasInstance> {\n\t\tconst availability = await this.availability();\n\t\tif (!availability.available) throw new Error(availability.reason);\n\n\t\tconst { runnable, withheld } = this.discover();\n\t\tconst extension = runnable.find((candidate) => candidate.id === ref.extensionId);\n\t\tif (!extension) {\n\t\t\tif (withheld.some((candidate) => candidate.id === ref.extensionId)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Canvas extension \"${ref.extensionId}\" came with this repository, which is not a trusted workspace. ` +\n\t\t\t\t\t\t\"Run /plugin trust to allow this directory to run code it ships.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst known = runnable.map((candidate) => candidate.id).join(\", \") || \"none\";\n\t\t\tthrow new Error(`No canvas extension \"${ref.extensionId}\" (found: ${known}).`);\n\t\t}\n\n\t\tconst registry = this.ensureRegistry(availability);\n\t\tconst canvasId = ref.canvasId ?? (await this.soleCanvasId(registry, extension));\n\t\treturn registry.open(extension, canvasId, undefined, options);\n\t}\n\n\t/** Close one open instance. Unknown ids are a no-op, so closing twice is harmless. */\n\tasync close(instanceId: string): Promise<CanvasInstance | undefined> {\n\t\tconst registry = this.registryOrUndefined();\n\t\tconst instance = registry?.listInstances().find((open) => open.instanceId === instanceId);\n\t\tif (!registry || !instance) return undefined;\n\t\tawait registry.close(instance);\n\t\treturn instance;\n\t}\n\n\t/** Every open instance. */\n\tinstances(): CanvasInstance[] {\n\t\treturn this.registryOrUndefined()?.listInstances() ?? [];\n\t}\n\n\t/**\n\t * The live registry, or undefined if nothing has been opened yet.\n\t *\n\t * Exposed so the host can hand it to the canvas tools, which read\n\t * `listInstances()` and `activeActions()` from it.\n\t */\n\tregistryOrUndefined(): CanvasRegistry | undefined {\n\t\treturn this.registry;\n\t}\n\n\t/** Advisory cleanup, driven by whoever owns the session clock. */\n\tasync reapIdle(): Promise<string[]> {\n\t\treturn (await this.registryOrUndefined()?.reapIdle()) ?? [];\n\t}\n\n\t/** Close everything and stop every child. Safe to call twice. */\n\tasync dispose(): Promise<void> {\n\t\tawait this.registry?.shutdown();\n\t\tthis.registry = undefined;\n\t}\n\n\tprivate ensureRegistry(availability: Extract<CanvasAvailability, { available: true }>): CanvasRegistry {\n\t\tif (!this.registry) {\n\t\t\tthis.registry = new CanvasRegistry({\n\t\t\t\truntime: availability.runtime,\n\t\t\t\tcwd: this.options.cwd,\n\t\t\t\tagentDir: this.options.agentDir,\n\t\t\t\tonLog: this.options.onLog,\n\t\t\t\tonStray: this.options.onStray,\n\t\t\t\tonStderr: this.options.onStderr,\n\t\t\t\tonDiagnostic: this.options.onDiagnostic,\n\t\t\t});\n\t\t}\n\t\treturn this.registry;\n\t}\n\n\t/**\n\t * Pick the canvas when the caller named only an extension.\n\t *\n\t * Forking to read the declarations is unavoidable: they arrive in the child's\n\t * `ready` message. A multi-canvas extension must be named explicitly rather than\n\t * guessed at.\n\t */\n\tprivate async soleCanvasId(registry: CanvasRegistry, extension: DiscoveredCanvasExtension): Promise<string> {\n\t\tconst declarations = await registry.declarations(extension);\n\t\tif (declarations.length === 1) return declarations[0]?.id as string;\n\t\tif (declarations.length === 0) throw new Error(`Canvas extension \"${extension.id}\" declares no canvases.`);\n\t\tconst ids = declarations.map((declaration) => `${extension.id}:${declaration.id}`).join(\", \");\n\t\tthrow new Error(`Canvas extension \"${extension.id}\" declares several canvases; name one of: ${ids}.`);\n\t}\n}\n"]}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session-scoped canvas facade: everything the TUI needs, with no TUI in it.\n *\n * Design: `docs/canvas-extensions-design.md` §11. The pieces underneath — discovery,\n * the trust gate, availability, the registry — are each small and separately tested.\n * This is what stitches them into the four questions a user surface actually asks:\n * what is there, can it run, open this one, close that one.\n *\n * It holds no TUI types on purpose. `extensions/core/canvas.ts` renders and supplies\n * an `AbortSignal` from a cancellable loader; everything decided here stays testable\n * without a terminal.\n *\n * Availability is resolved once and cached, because resolving can spawn\n * `node --version` (§11.1) and the answer cannot change within a session.\n */\n\nimport { getAgentDir } from \"../../config.js\";\nimport type { DiscoveredCanvasExtension } from \"./discovery.js\";\nimport { type CanvasSearchRoot, canvasSearchRoots, discoverCanvasExtensions } from \"./discovery.js\";\nimport { type CanvasAvailability, resolveCanvasRuntime } from \"./launch.js\";\nimport { pluginCanvasExtensions } from \"./plugin-canvases.js\";\nimport { type CanvasInstance, CanvasRegistry, type CanvasRegistryEvents } from \"./registry.js\";\nimport type { CanvasCallOptions } from \"./runner.js\";\nimport { gateCanvasExtensions } from \"./trust.js\";\n\n/** One canvas a person could open, or has open. */\nexport interface CanvasListing {\n\textensionId: string;\n\t/** Undefined until the extension has been forked, since declarations come from it. */\n\tcanvasId: string | undefined;\n\tdisplayName: string | undefined;\n\tscope: DiscoveredCanvasExtension[\"scope\"];\n\t/** Why it cannot be opened, if it cannot. */\n\twithheld: \"untrusted-workspace\" | undefined;\n\t/** Instances of this canvas that are currently open. */\n\topen: CanvasInstance[];\n}\n\n/** What `list()` reports. */\nexport interface CanvasOverview {\n\t/** Absent `reason` means canvases can run here. */\n\tavailability: CanvasAvailability;\n\tlistings: CanvasListing[];\n\t/** Extensions withheld by the trust gate — surfaced, never hidden (§5.1). */\n\twithheldCount: number;\n}\n\n/** Configuration for a session's canvas facade. */\nexport interface CanvasSessionOptions extends CanvasRegistryEvents {\n\tcwd: string;\n\thomeDir: string;\n\tagentDir?: string;\n\t/** Override the search roots; defaults to {@link canvasSearchRoots}. */\n\troots?: CanvasSearchRoot[];\n\t/**\n\t * Override how plugin-shipped canvases are found; defaults to\n\t * {@link pluginCanvasExtensions}. Pass `() => []` to look at the search roots\n\t * and nothing else.\n\t */\n\tpluginExtensions?: () => DiscoveredCanvasExtension[];\n\t/** Override availability resolution, for tests and for hosts that already know. */\n\tresolveRuntime?: () => Promise<CanvasAvailability>;\n}\n\n/** Reference to a canvas: an extension id, optionally narrowed to one of its canvases. */\nexport interface CanvasRef {\n\textensionId: string;\n\tcanvasId?: string;\n}\n\n/**\n * Parse `extension` or `extension:canvas`.\n *\n * Extension ids are directory names and canvas ids are provider-local, so a single\n * colon is unambiguous and needs no quoting.\n */\nexport function parseCanvasRef(input: string): CanvasRef | undefined {\n\tconst trimmed = input.trim();\n\tif (trimmed.length === 0) return undefined;\n\tconst colon = trimmed.indexOf(\":\");\n\tif (colon === -1) return { extensionId: trimmed };\n\tconst extensionId = trimmed.slice(0, colon).trim();\n\tconst canvasId = trimmed.slice(colon + 1).trim();\n\tif (extensionId.length === 0 || canvasId.length === 0) return undefined;\n\treturn { extensionId, canvasId };\n}\n\nexport class CanvasSession {\n\tprivate readonly options: CanvasSessionOptions;\n\tprivate readonly roots: CanvasSearchRoot[];\n\tprivate readonly pluginExtensions: () => DiscoveredCanvasExtension[];\n\t/** Cached because resolving can spawn `node --version` and cannot change mid-session. */\n\tprivate availabilityPromise: Promise<CanvasAvailability> | undefined;\n\t/** Created on first successful open, not at construction: listing must not fork. */\n\tprivate registry: CanvasRegistry | undefined;\n\n\tconstructor(options: CanvasSessionOptions) {\n\t\tthis.options = options;\n\t\tthis.roots = options.roots ?? canvasSearchRoots(options.cwd, options.homeDir);\n\t\t// Re-read on every discover rather than cached: /plugin install can add a\n\t\t// canvas mid-session, and a listing that cannot see it is the bug this\n\t\t// exists to fix.\n\t\tthis.pluginExtensions =\n\t\t\toptions.pluginExtensions ?? (() => pluginCanvasExtensions(options.cwd, options.agentDir ?? getAgentDir()));\n\t}\n\n\t/** Discovered extensions, partitioned by the trust gate. Read-only and always safe. */\n\tdiscover(): { runnable: DiscoveredCanvasExtension[]; withheld: DiscoveredCanvasExtension[] } {\n\t\tconst gated = gateCanvasExtensions(\n\t\t\tdiscoverCanvasExtensions(this.roots, this.pluginExtensions()),\n\t\t\tthis.options.cwd,\n\t\t\tthis.options.agentDir ?? getAgentDir(),\n\t\t);\n\t\treturn { runnable: gated.runnable, withheld: gated.withheld.map((entry) => entry.extension) };\n\t}\n\n\t/** Whether canvases can run here. Resolved once per session and cached. */\n\tasync availability(): Promise<CanvasAvailability> {\n\t\tthis.availabilityPromise ??= (this.options.resolveRuntime ?? resolveCanvasRuntime)();\n\t\treturn this.availabilityPromise;\n\t}\n\n\t/**\n\t * What is installed, what is open, and what is being withheld.\n\t *\n\t * Deliberately does not fork anything: listing must stay free and safe, so a\n\t * `canvasId` is only known for extensions already running. That is the visible\n\t * consequence of a canvas having no passive half (§5.1) — even its name comes from\n\t * running its code.\n\t */\n\tasync list(): Promise<CanvasOverview> {\n\t\tconst availability = await this.availability();\n\t\tconst { runnable, withheld } = this.discover();\n\t\tconst open = this.registryOrUndefined()?.listInstances() ?? [];\n\n\t\tconst listings: CanvasListing[] = [];\n\t\tfor (const extension of runnable) {\n\t\t\tconst instances = open.filter((instance) => instance.extensionId === extension.id);\n\t\t\tif (instances.length === 0) {\n\t\t\t\tlistings.push({\n\t\t\t\t\textensionId: extension.id,\n\t\t\t\t\tcanvasId: undefined,\n\t\t\t\t\tdisplayName: undefined,\n\t\t\t\t\tscope: extension.scope,\n\t\t\t\t\twithheld: undefined,\n\t\t\t\t\topen: [],\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const canvasId of new Set(instances.map((instance) => instance.canvasId))) {\n\t\t\t\tconst forCanvas = instances.filter((instance) => instance.canvasId === canvasId);\n\t\t\t\tlistings.push({\n\t\t\t\t\textensionId: extension.id,\n\t\t\t\t\tcanvasId,\n\t\t\t\t\tdisplayName: forCanvas[0]?.title,\n\t\t\t\t\tscope: extension.scope,\n\t\t\t\t\twithheld: undefined,\n\t\t\t\t\topen: forCanvas,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfor (const extension of withheld) {\n\t\t\tlistings.push({\n\t\t\t\textensionId: extension.id,\n\t\t\t\tcanvasId: undefined,\n\t\t\t\tdisplayName: undefined,\n\t\t\t\tscope: extension.scope,\n\t\t\t\twithheld: \"untrusted-workspace\",\n\t\t\t\topen: [],\n\t\t\t});\n\t\t}\n\t\treturn { availability, listings, withheldCount: withheld.length };\n\t}\n\n\t/**\n\t * Open a canvas.\n\t *\n\t * `options.signal` comes from the caller's cancellable loader, so a person's Esc\n\t * reaches the registry's abandon path (§11.6) rather than merely hiding a spinner.\n\t */\n\tasync open(ref: CanvasRef, options?: CanvasCallOptions): Promise<CanvasInstance> {\n\t\tconst availability = await this.availability();\n\t\tif (!availability.available) throw new Error(availability.reason);\n\n\t\tconst { runnable, withheld } = this.discover();\n\t\tconst extension = runnable.find((candidate) => candidate.id === ref.extensionId);\n\t\tif (!extension) {\n\t\t\tif (withheld.some((candidate) => candidate.id === ref.extensionId)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Canvas extension \"${ref.extensionId}\" came with this repository, which is not a trusted workspace. ` +\n\t\t\t\t\t\t\"Run /plugin trust to allow this directory to run code it ships.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst known = runnable.map((candidate) => candidate.id).join(\", \") || \"none\";\n\t\t\tthrow new Error(`No canvas extension \"${ref.extensionId}\" (found: ${known}).`);\n\t\t}\n\n\t\tconst registry = this.ensureRegistry(availability);\n\t\tconst canvasId = ref.canvasId ?? (await this.soleCanvasId(registry, extension));\n\t\treturn registry.open(extension, canvasId, undefined, options);\n\t}\n\n\t/** Close one open instance. Unknown ids are a no-op, so closing twice is harmless. */\n\tasync close(instanceId: string): Promise<CanvasInstance | undefined> {\n\t\tconst registry = this.registryOrUndefined();\n\t\tconst instance = registry?.listInstances().find((open) => open.instanceId === instanceId);\n\t\tif (!registry || !instance) return undefined;\n\t\tawait registry.close(instance);\n\t\treturn instance;\n\t}\n\n\t/** Every open instance. */\n\tinstances(): CanvasInstance[] {\n\t\treturn this.registryOrUndefined()?.listInstances() ?? [];\n\t}\n\n\t/**\n\t * The live registry, or undefined if nothing has been opened yet.\n\t *\n\t * Exposed so the host can hand it to the canvas tools, which read\n\t * `listInstances()` and `activeActions()` from it.\n\t */\n\tregistryOrUndefined(): CanvasRegistry | undefined {\n\t\treturn this.registry;\n\t}\n\n\t/** Advisory cleanup, driven by whoever owns the session clock. */\n\tasync reapIdle(): Promise<string[]> {\n\t\treturn (await this.registryOrUndefined()?.reapIdle()) ?? [];\n\t}\n\n\t/** Close everything and stop every child. Safe to call twice. */\n\tasync dispose(): Promise<void> {\n\t\tawait this.registry?.shutdown();\n\t\tthis.registry = undefined;\n\t}\n\n\tprivate ensureRegistry(availability: Extract<CanvasAvailability, { available: true }>): CanvasRegistry {\n\t\tif (!this.registry) {\n\t\t\tthis.registry = new CanvasRegistry({\n\t\t\t\truntime: availability.runtime,\n\t\t\t\tcwd: this.options.cwd,\n\t\t\t\tagentDir: this.options.agentDir,\n\t\t\t\tonLog: this.options.onLog,\n\t\t\t\tonStray: this.options.onStray,\n\t\t\t\tonStderr: this.options.onStderr,\n\t\t\t\tonDiagnostic: this.options.onDiagnostic,\n\t\t\t});\n\t\t}\n\t\treturn this.registry;\n\t}\n\n\t/**\n\t * Pick the canvas when the caller named only an extension.\n\t *\n\t * Forking to read the declarations is unavoidable: they arrive in the child's\n\t * `ready` message. A multi-canvas extension must be named explicitly rather than\n\t * guessed at.\n\t */\n\tprivate async soleCanvasId(registry: CanvasRegistry, extension: DiscoveredCanvasExtension): Promise<string> {\n\t\tconst declarations = await registry.declarations(extension);\n\t\tif (declarations.length === 1) return declarations[0]?.id as string;\n\t\tif (declarations.length === 0) throw new Error(`Canvas extension \"${extension.id}\" declares no canvases.`);\n\t\tconst ids = declarations.map((declaration) => `${extension.id}:${declaration.id}`).join(\", \");\n\t\tthrow new Error(`Canvas extension \"${extension.id}\" declares several canvases; name one of: ${ids}.`);\n\t}\n}\n"]}
@@ -16,6 +16,7 @@
16
16
  import { getAgentDir } from "../../config.js";
17
17
  import { canvasSearchRoots, discoverCanvasExtensions } from "./discovery.js";
18
18
  import { resolveCanvasRuntime } from "./launch.js";
19
+ import { pluginCanvasExtensions } from "./plugin-canvases.js";
19
20
  import { CanvasRegistry } from "./registry.js";
20
21
  import { gateCanvasExtensions } from "./trust.js";
21
22
  /**
@@ -40,6 +41,7 @@ export function parseCanvasRef(input) {
40
41
  export class CanvasSession {
41
42
  options;
42
43
  roots;
44
+ pluginExtensions;
43
45
  /** Cached because resolving can spawn `node --version` and cannot change mid-session. */
44
46
  availabilityPromise;
45
47
  /** Created on first successful open, not at construction: listing must not fork. */
@@ -47,10 +49,15 @@ export class CanvasSession {
47
49
  constructor(options) {
48
50
  this.options = options;
49
51
  this.roots = options.roots ?? canvasSearchRoots(options.cwd, options.homeDir);
52
+ // Re-read on every discover rather than cached: /plugin install can add a
53
+ // canvas mid-session, and a listing that cannot see it is the bug this
54
+ // exists to fix.
55
+ this.pluginExtensions =
56
+ options.pluginExtensions ?? (() => pluginCanvasExtensions(options.cwd, options.agentDir ?? getAgentDir()));
50
57
  }
51
58
  /** Discovered extensions, partitioned by the trust gate. Read-only and always safe. */
52
59
  discover() {
53
- const gated = gateCanvasExtensions(discoverCanvasExtensions(this.roots), this.options.cwd, this.options.agentDir ?? getAgentDir());
60
+ const gated = gateCanvasExtensions(discoverCanvasExtensions(this.roots, this.pluginExtensions()), this.options.cwd, this.options.agentDir ?? getAgentDir());
54
61
  return { runnable: gated.runnable, withheld: gated.withheld.map((entry) => entry.extension) };
55
62
  }
56
63
  /** Whether canvases can run here. Resolved once per session and cached. */
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","sourceRoot":"","sources":["../../../src/core/canvas/session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session-scoped canvas facade: everything the TUI needs, with no TUI in it.\n *\n * Design: `docs/canvas-extensions-design.md` §11. The pieces underneath — discovery,\n * the trust gate, availability, the registry — are each small and separately tested.\n * This is what stitches them into the four questions a user surface actually asks:\n * what is there, can it run, open this one, close that one.\n *\n * It holds no TUI types on purpose. `extensions/core/canvas.ts` renders and supplies\n * an `AbortSignal` from a cancellable loader; everything decided here stays testable\n * without a terminal.\n *\n * Availability is resolved once and cached, because resolving can spawn\n * `node --version` (§11.1) and the answer cannot change within a session.\n */\n\nimport { getAgentDir } from \"../../config.js\";\nimport type { DiscoveredCanvasExtension } from \"./discovery.js\";\nimport { type CanvasSearchRoot, canvasSearchRoots, discoverCanvasExtensions } from \"./discovery.js\";\nimport { type CanvasAvailability, resolveCanvasRuntime } from \"./launch.js\";\nimport { type CanvasInstance, CanvasRegistry, type CanvasRegistryEvents } from \"./registry.js\";\nimport type { CanvasCallOptions } from \"./runner.js\";\nimport { gateCanvasExtensions } from \"./trust.js\";\n\n/** One canvas a person could open, or has open. */\nexport interface CanvasListing {\n\textensionId: string;\n\t/** Undefined until the extension has been forked, since declarations come from it. */\n\tcanvasId: string | undefined;\n\tdisplayName: string | undefined;\n\tscope: DiscoveredCanvasExtension[\"scope\"];\n\t/** Why it cannot be opened, if it cannot. */\n\twithheld: \"untrusted-workspace\" | undefined;\n\t/** Instances of this canvas that are currently open. */\n\topen: CanvasInstance[];\n}\n\n/** What `list()` reports. */\nexport interface CanvasOverview {\n\t/** Absent `reason` means canvases can run here. */\n\tavailability: CanvasAvailability;\n\tlistings: CanvasListing[];\n\t/** Extensions withheld by the trust gate — surfaced, never hidden (§5.1). */\n\twithheldCount: number;\n}\n\n/** Configuration for a session's canvas facade. */\nexport interface CanvasSessionOptions extends CanvasRegistryEvents {\n\tcwd: string;\n\thomeDir: string;\n\tagentDir?: string;\n\t/** Override the search roots; defaults to {@link canvasSearchRoots}. */\n\troots?: CanvasSearchRoot[];\n\t/** Override availability resolution, for tests and for hosts that already know. */\n\tresolveRuntime?: () => Promise<CanvasAvailability>;\n}\n\n/** Reference to a canvas: an extension id, optionally narrowed to one of its canvases. */\nexport interface CanvasRef {\n\textensionId: string;\n\tcanvasId?: string;\n}\n\n/**\n * Parse `extension` or `extension:canvas`.\n *\n * Extension ids are directory names and canvas ids are provider-local, so a single\n * colon is unambiguous and needs no quoting.\n */\nexport function parseCanvasRef(input: string): CanvasRef | undefined {\n\tconst trimmed = input.trim();\n\tif (trimmed.length === 0) return undefined;\n\tconst colon = trimmed.indexOf(\":\");\n\tif (colon === -1) return { extensionId: trimmed };\n\tconst extensionId = trimmed.slice(0, colon).trim();\n\tconst canvasId = trimmed.slice(colon + 1).trim();\n\tif (extensionId.length === 0 || canvasId.length === 0) return undefined;\n\treturn { extensionId, canvasId };\n}\n\nexport class CanvasSession {\n\tprivate readonly options: CanvasSessionOptions;\n\tprivate readonly roots: CanvasSearchRoot[];\n\t/** Cached because resolving can spawn `node --version` and cannot change mid-session. */\n\tprivate availabilityPromise: Promise<CanvasAvailability> | undefined;\n\t/** Created on first successful open, not at construction: listing must not fork. */\n\tprivate registry: CanvasRegistry | undefined;\n\n\tconstructor(options: CanvasSessionOptions) {\n\t\tthis.options = options;\n\t\tthis.roots = options.roots ?? canvasSearchRoots(options.cwd, options.homeDir);\n\t}\n\n\t/** Discovered extensions, partitioned by the trust gate. Read-only and always safe. */\n\tdiscover(): { runnable: DiscoveredCanvasExtension[]; withheld: DiscoveredCanvasExtension[] } {\n\t\tconst gated = gateCanvasExtensions(\n\t\t\tdiscoverCanvasExtensions(this.roots),\n\t\t\tthis.options.cwd,\n\t\t\tthis.options.agentDir ?? getAgentDir(),\n\t\t);\n\t\treturn { runnable: gated.runnable, withheld: gated.withheld.map((entry) => entry.extension) };\n\t}\n\n\t/** Whether canvases can run here. Resolved once per session and cached. */\n\tasync availability(): Promise<CanvasAvailability> {\n\t\tthis.availabilityPromise ??= (this.options.resolveRuntime ?? resolveCanvasRuntime)();\n\t\treturn this.availabilityPromise;\n\t}\n\n\t/**\n\t * What is installed, what is open, and what is being withheld.\n\t *\n\t * Deliberately does not fork anything: listing must stay free and safe, so a\n\t * `canvasId` is only known for extensions already running. That is the visible\n\t * consequence of a canvas having no passive half (§5.1) — even its name comes from\n\t * running its code.\n\t */\n\tasync list(): Promise<CanvasOverview> {\n\t\tconst availability = await this.availability();\n\t\tconst { runnable, withheld } = this.discover();\n\t\tconst open = this.registryOrUndefined()?.listInstances() ?? [];\n\n\t\tconst listings: CanvasListing[] = [];\n\t\tfor (const extension of runnable) {\n\t\t\tconst instances = open.filter((instance) => instance.extensionId === extension.id);\n\t\t\tif (instances.length === 0) {\n\t\t\t\tlistings.push({\n\t\t\t\t\textensionId: extension.id,\n\t\t\t\t\tcanvasId: undefined,\n\t\t\t\t\tdisplayName: undefined,\n\t\t\t\t\tscope: extension.scope,\n\t\t\t\t\twithheld: undefined,\n\t\t\t\t\topen: [],\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const canvasId of new Set(instances.map((instance) => instance.canvasId))) {\n\t\t\t\tconst forCanvas = instances.filter((instance) => instance.canvasId === canvasId);\n\t\t\t\tlistings.push({\n\t\t\t\t\textensionId: extension.id,\n\t\t\t\t\tcanvasId,\n\t\t\t\t\tdisplayName: forCanvas[0]?.title,\n\t\t\t\t\tscope: extension.scope,\n\t\t\t\t\twithheld: undefined,\n\t\t\t\t\topen: forCanvas,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfor (const extension of withheld) {\n\t\t\tlistings.push({\n\t\t\t\textensionId: extension.id,\n\t\t\t\tcanvasId: undefined,\n\t\t\t\tdisplayName: undefined,\n\t\t\t\tscope: extension.scope,\n\t\t\t\twithheld: \"untrusted-workspace\",\n\t\t\t\topen: [],\n\t\t\t});\n\t\t}\n\t\treturn { availability, listings, withheldCount: withheld.length };\n\t}\n\n\t/**\n\t * Open a canvas.\n\t *\n\t * `options.signal` comes from the caller's cancellable loader, so a person's Esc\n\t * reaches the registry's abandon path (§11.6) rather than merely hiding a spinner.\n\t */\n\tasync open(ref: CanvasRef, options?: CanvasCallOptions): Promise<CanvasInstance> {\n\t\tconst availability = await this.availability();\n\t\tif (!availability.available) throw new Error(availability.reason);\n\n\t\tconst { runnable, withheld } = this.discover();\n\t\tconst extension = runnable.find((candidate) => candidate.id === ref.extensionId);\n\t\tif (!extension) {\n\t\t\tif (withheld.some((candidate) => candidate.id === ref.extensionId)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Canvas extension \"${ref.extensionId}\" came with this repository, which is not a trusted workspace. ` +\n\t\t\t\t\t\t\"Run /plugin trust to allow this directory to run code it ships.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst known = runnable.map((candidate) => candidate.id).join(\", \") || \"none\";\n\t\t\tthrow new Error(`No canvas extension \"${ref.extensionId}\" (found: ${known}).`);\n\t\t}\n\n\t\tconst registry = this.ensureRegistry(availability);\n\t\tconst canvasId = ref.canvasId ?? (await this.soleCanvasId(registry, extension));\n\t\treturn registry.open(extension, canvasId, undefined, options);\n\t}\n\n\t/** Close one open instance. Unknown ids are a no-op, so closing twice is harmless. */\n\tasync close(instanceId: string): Promise<CanvasInstance | undefined> {\n\t\tconst registry = this.registryOrUndefined();\n\t\tconst instance = registry?.listInstances().find((open) => open.instanceId === instanceId);\n\t\tif (!registry || !instance) return undefined;\n\t\tawait registry.close(instance);\n\t\treturn instance;\n\t}\n\n\t/** Every open instance. */\n\tinstances(): CanvasInstance[] {\n\t\treturn this.registryOrUndefined()?.listInstances() ?? [];\n\t}\n\n\t/**\n\t * The live registry, or undefined if nothing has been opened yet.\n\t *\n\t * Exposed so the host can hand it to the canvas tools, which read\n\t * `listInstances()` and `activeActions()` from it.\n\t */\n\tregistryOrUndefined(): CanvasRegistry | undefined {\n\t\treturn this.registry;\n\t}\n\n\t/** Advisory cleanup, driven by whoever owns the session clock. */\n\tasync reapIdle(): Promise<string[]> {\n\t\treturn (await this.registryOrUndefined()?.reapIdle()) ?? [];\n\t}\n\n\t/** Close everything and stop every child. Safe to call twice. */\n\tasync dispose(): Promise<void> {\n\t\tawait this.registry?.shutdown();\n\t\tthis.registry = undefined;\n\t}\n\n\tprivate ensureRegistry(availability: Extract<CanvasAvailability, { available: true }>): CanvasRegistry {\n\t\tif (!this.registry) {\n\t\t\tthis.registry = new CanvasRegistry({\n\t\t\t\truntime: availability.runtime,\n\t\t\t\tcwd: this.options.cwd,\n\t\t\t\tagentDir: this.options.agentDir,\n\t\t\t\tonLog: this.options.onLog,\n\t\t\t\tonStray: this.options.onStray,\n\t\t\t\tonStderr: this.options.onStderr,\n\t\t\t\tonDiagnostic: this.options.onDiagnostic,\n\t\t\t});\n\t\t}\n\t\treturn this.registry;\n\t}\n\n\t/**\n\t * Pick the canvas when the caller named only an extension.\n\t *\n\t * Forking to read the declarations is unavoidable: they arrive in the child's\n\t * `ready` message. A multi-canvas extension must be named explicitly rather than\n\t * guessed at.\n\t */\n\tprivate async soleCanvasId(registry: CanvasRegistry, extension: DiscoveredCanvasExtension): Promise<string> {\n\t\tconst declarations = await registry.declarations(extension);\n\t\tif (declarations.length === 1) return declarations[0]?.id as string;\n\t\tif (declarations.length === 0) throw new Error(`Canvas extension \"${extension.id}\" declares no canvases.`);\n\t\tconst ids = declarations.map((declaration) => `${extension.id}:${declaration.id}`).join(\", \");\n\t\tthrow new Error(`Canvas extension \"${extension.id}\" declares several canvases; name one of: ${ids}.`);\n\t}\n}\n"]}
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../../src/core/canvas/session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session-scoped canvas facade: everything the TUI needs, with no TUI in it.\n *\n * Design: `docs/canvas-extensions-design.md` §11. The pieces underneath — discovery,\n * the trust gate, availability, the registry — are each small and separately tested.\n * This is what stitches them into the four questions a user surface actually asks:\n * what is there, can it run, open this one, close that one.\n *\n * It holds no TUI types on purpose. `extensions/core/canvas.ts` renders and supplies\n * an `AbortSignal` from a cancellable loader; everything decided here stays testable\n * without a terminal.\n *\n * Availability is resolved once and cached, because resolving can spawn\n * `node --version` (§11.1) and the answer cannot change within a session.\n */\n\nimport { getAgentDir } from \"../../config.js\";\nimport type { DiscoveredCanvasExtension } from \"./discovery.js\";\nimport { type CanvasSearchRoot, canvasSearchRoots, discoverCanvasExtensions } from \"./discovery.js\";\nimport { type CanvasAvailability, resolveCanvasRuntime } from \"./launch.js\";\nimport { pluginCanvasExtensions } from \"./plugin-canvases.js\";\nimport { type CanvasInstance, CanvasRegistry, type CanvasRegistryEvents } from \"./registry.js\";\nimport type { CanvasCallOptions } from \"./runner.js\";\nimport { gateCanvasExtensions } from \"./trust.js\";\n\n/** One canvas a person could open, or has open. */\nexport interface CanvasListing {\n\textensionId: string;\n\t/** Undefined until the extension has been forked, since declarations come from it. */\n\tcanvasId: string | undefined;\n\tdisplayName: string | undefined;\n\tscope: DiscoveredCanvasExtension[\"scope\"];\n\t/** Why it cannot be opened, if it cannot. */\n\twithheld: \"untrusted-workspace\" | undefined;\n\t/** Instances of this canvas that are currently open. */\n\topen: CanvasInstance[];\n}\n\n/** What `list()` reports. */\nexport interface CanvasOverview {\n\t/** Absent `reason` means canvases can run here. */\n\tavailability: CanvasAvailability;\n\tlistings: CanvasListing[];\n\t/** Extensions withheld by the trust gate — surfaced, never hidden (§5.1). */\n\twithheldCount: number;\n}\n\n/** Configuration for a session's canvas facade. */\nexport interface CanvasSessionOptions extends CanvasRegistryEvents {\n\tcwd: string;\n\thomeDir: string;\n\tagentDir?: string;\n\t/** Override the search roots; defaults to {@link canvasSearchRoots}. */\n\troots?: CanvasSearchRoot[];\n\t/**\n\t * Override how plugin-shipped canvases are found; defaults to\n\t * {@link pluginCanvasExtensions}. Pass `() => []` to look at the search roots\n\t * and nothing else.\n\t */\n\tpluginExtensions?: () => DiscoveredCanvasExtension[];\n\t/** Override availability resolution, for tests and for hosts that already know. */\n\tresolveRuntime?: () => Promise<CanvasAvailability>;\n}\n\n/** Reference to a canvas: an extension id, optionally narrowed to one of its canvases. */\nexport interface CanvasRef {\n\textensionId: string;\n\tcanvasId?: string;\n}\n\n/**\n * Parse `extension` or `extension:canvas`.\n *\n * Extension ids are directory names and canvas ids are provider-local, so a single\n * colon is unambiguous and needs no quoting.\n */\nexport function parseCanvasRef(input: string): CanvasRef | undefined {\n\tconst trimmed = input.trim();\n\tif (trimmed.length === 0) return undefined;\n\tconst colon = trimmed.indexOf(\":\");\n\tif (colon === -1) return { extensionId: trimmed };\n\tconst extensionId = trimmed.slice(0, colon).trim();\n\tconst canvasId = trimmed.slice(colon + 1).trim();\n\tif (extensionId.length === 0 || canvasId.length === 0) return undefined;\n\treturn { extensionId, canvasId };\n}\n\nexport class CanvasSession {\n\tprivate readonly options: CanvasSessionOptions;\n\tprivate readonly roots: CanvasSearchRoot[];\n\tprivate readonly pluginExtensions: () => DiscoveredCanvasExtension[];\n\t/** Cached because resolving can spawn `node --version` and cannot change mid-session. */\n\tprivate availabilityPromise: Promise<CanvasAvailability> | undefined;\n\t/** Created on first successful open, not at construction: listing must not fork. */\n\tprivate registry: CanvasRegistry | undefined;\n\n\tconstructor(options: CanvasSessionOptions) {\n\t\tthis.options = options;\n\t\tthis.roots = options.roots ?? canvasSearchRoots(options.cwd, options.homeDir);\n\t\t// Re-read on every discover rather than cached: /plugin install can add a\n\t\t// canvas mid-session, and a listing that cannot see it is the bug this\n\t\t// exists to fix.\n\t\tthis.pluginExtensions =\n\t\t\toptions.pluginExtensions ?? (() => pluginCanvasExtensions(options.cwd, options.agentDir ?? getAgentDir()));\n\t}\n\n\t/** Discovered extensions, partitioned by the trust gate. Read-only and always safe. */\n\tdiscover(): { runnable: DiscoveredCanvasExtension[]; withheld: DiscoveredCanvasExtension[] } {\n\t\tconst gated = gateCanvasExtensions(\n\t\t\tdiscoverCanvasExtensions(this.roots, this.pluginExtensions()),\n\t\t\tthis.options.cwd,\n\t\t\tthis.options.agentDir ?? getAgentDir(),\n\t\t);\n\t\treturn { runnable: gated.runnable, withheld: gated.withheld.map((entry) => entry.extension) };\n\t}\n\n\t/** Whether canvases can run here. Resolved once per session and cached. */\n\tasync availability(): Promise<CanvasAvailability> {\n\t\tthis.availabilityPromise ??= (this.options.resolveRuntime ?? resolveCanvasRuntime)();\n\t\treturn this.availabilityPromise;\n\t}\n\n\t/**\n\t * What is installed, what is open, and what is being withheld.\n\t *\n\t * Deliberately does not fork anything: listing must stay free and safe, so a\n\t * `canvasId` is only known for extensions already running. That is the visible\n\t * consequence of a canvas having no passive half (§5.1) — even its name comes from\n\t * running its code.\n\t */\n\tasync list(): Promise<CanvasOverview> {\n\t\tconst availability = await this.availability();\n\t\tconst { runnable, withheld } = this.discover();\n\t\tconst open = this.registryOrUndefined()?.listInstances() ?? [];\n\n\t\tconst listings: CanvasListing[] = [];\n\t\tfor (const extension of runnable) {\n\t\t\tconst instances = open.filter((instance) => instance.extensionId === extension.id);\n\t\t\tif (instances.length === 0) {\n\t\t\t\tlistings.push({\n\t\t\t\t\textensionId: extension.id,\n\t\t\t\t\tcanvasId: undefined,\n\t\t\t\t\tdisplayName: undefined,\n\t\t\t\t\tscope: extension.scope,\n\t\t\t\t\twithheld: undefined,\n\t\t\t\t\topen: [],\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const canvasId of new Set(instances.map((instance) => instance.canvasId))) {\n\t\t\t\tconst forCanvas = instances.filter((instance) => instance.canvasId === canvasId);\n\t\t\t\tlistings.push({\n\t\t\t\t\textensionId: extension.id,\n\t\t\t\t\tcanvasId,\n\t\t\t\t\tdisplayName: forCanvas[0]?.title,\n\t\t\t\t\tscope: extension.scope,\n\t\t\t\t\twithheld: undefined,\n\t\t\t\t\topen: forCanvas,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfor (const extension of withheld) {\n\t\t\tlistings.push({\n\t\t\t\textensionId: extension.id,\n\t\t\t\tcanvasId: undefined,\n\t\t\t\tdisplayName: undefined,\n\t\t\t\tscope: extension.scope,\n\t\t\t\twithheld: \"untrusted-workspace\",\n\t\t\t\topen: [],\n\t\t\t});\n\t\t}\n\t\treturn { availability, listings, withheldCount: withheld.length };\n\t}\n\n\t/**\n\t * Open a canvas.\n\t *\n\t * `options.signal` comes from the caller's cancellable loader, so a person's Esc\n\t * reaches the registry's abandon path (§11.6) rather than merely hiding a spinner.\n\t */\n\tasync open(ref: CanvasRef, options?: CanvasCallOptions): Promise<CanvasInstance> {\n\t\tconst availability = await this.availability();\n\t\tif (!availability.available) throw new Error(availability.reason);\n\n\t\tconst { runnable, withheld } = this.discover();\n\t\tconst extension = runnable.find((candidate) => candidate.id === ref.extensionId);\n\t\tif (!extension) {\n\t\t\tif (withheld.some((candidate) => candidate.id === ref.extensionId)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Canvas extension \"${ref.extensionId}\" came with this repository, which is not a trusted workspace. ` +\n\t\t\t\t\t\t\"Run /plugin trust to allow this directory to run code it ships.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst known = runnable.map((candidate) => candidate.id).join(\", \") || \"none\";\n\t\t\tthrow new Error(`No canvas extension \"${ref.extensionId}\" (found: ${known}).`);\n\t\t}\n\n\t\tconst registry = this.ensureRegistry(availability);\n\t\tconst canvasId = ref.canvasId ?? (await this.soleCanvasId(registry, extension));\n\t\treturn registry.open(extension, canvasId, undefined, options);\n\t}\n\n\t/** Close one open instance. Unknown ids are a no-op, so closing twice is harmless. */\n\tasync close(instanceId: string): Promise<CanvasInstance | undefined> {\n\t\tconst registry = this.registryOrUndefined();\n\t\tconst instance = registry?.listInstances().find((open) => open.instanceId === instanceId);\n\t\tif (!registry || !instance) return undefined;\n\t\tawait registry.close(instance);\n\t\treturn instance;\n\t}\n\n\t/** Every open instance. */\n\tinstances(): CanvasInstance[] {\n\t\treturn this.registryOrUndefined()?.listInstances() ?? [];\n\t}\n\n\t/**\n\t * The live registry, or undefined if nothing has been opened yet.\n\t *\n\t * Exposed so the host can hand it to the canvas tools, which read\n\t * `listInstances()` and `activeActions()` from it.\n\t */\n\tregistryOrUndefined(): CanvasRegistry | undefined {\n\t\treturn this.registry;\n\t}\n\n\t/** Advisory cleanup, driven by whoever owns the session clock. */\n\tasync reapIdle(): Promise<string[]> {\n\t\treturn (await this.registryOrUndefined()?.reapIdle()) ?? [];\n\t}\n\n\t/** Close everything and stop every child. Safe to call twice. */\n\tasync dispose(): Promise<void> {\n\t\tawait this.registry?.shutdown();\n\t\tthis.registry = undefined;\n\t}\n\n\tprivate ensureRegistry(availability: Extract<CanvasAvailability, { available: true }>): CanvasRegistry {\n\t\tif (!this.registry) {\n\t\t\tthis.registry = new CanvasRegistry({\n\t\t\t\truntime: availability.runtime,\n\t\t\t\tcwd: this.options.cwd,\n\t\t\t\tagentDir: this.options.agentDir,\n\t\t\t\tonLog: this.options.onLog,\n\t\t\t\tonStray: this.options.onStray,\n\t\t\t\tonStderr: this.options.onStderr,\n\t\t\t\tonDiagnostic: this.options.onDiagnostic,\n\t\t\t});\n\t\t}\n\t\treturn this.registry;\n\t}\n\n\t/**\n\t * Pick the canvas when the caller named only an extension.\n\t *\n\t * Forking to read the declarations is unavoidable: they arrive in the child's\n\t * `ready` message. A multi-canvas extension must be named explicitly rather than\n\t * guessed at.\n\t */\n\tprivate async soleCanvasId(registry: CanvasRegistry, extension: DiscoveredCanvasExtension): Promise<string> {\n\t\tconst declarations = await registry.declarations(extension);\n\t\tif (declarations.length === 1) return declarations[0]?.id as string;\n\t\tif (declarations.length === 0) throw new Error(`Canvas extension \"${extension.id}\" declares no canvases.`);\n\t\tconst ids = declarations.map((declaration) => `${extension.id}:${declaration.id}`).join(\", \");\n\t\tthrow new Error(`Canvas extension \"${extension.id}\" declares several canvases; name one of: ${ids}.`);\n\t}\n}\n"]}
@@ -35,11 +35,10 @@ export declare class CanvasTrustError extends Error {
35
35
  * Whether an extension sits in the working tree, and therefore arrived with the
36
36
  * repository as far as anyone but its author can tell.
37
37
  *
38
- * Both project-scope homes count. `.agents/extensions/` is where hoocode would
39
- * author its own, and `.github/extensions/` is Copilot's project scope the one
40
- * that travels in every clone. Neither location can distinguish "I put this here"
41
- * from "this arrived in the clone", which is exactly why location is not the
42
- * question being asked; it only decides *whether* to ask about trust.
38
+ * Every project-scope home counts see {@link canvasProjectScopeRoots}. None of
39
+ * them can distinguish "I put this here" from "this arrived in the clone", which
40
+ * is exactly why location is not the question being asked; it only decides
41
+ * *whether* to ask about trust.
43
42
  *
44
43
  * User scope (`~/.copilot/extensions/`) is not project-supplied: it is outside any
45
44
  * repository and got there by a deliberate local act.