@kolisachint/hoocode-agent 0.5.25 → 0.5.26

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 (34) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/dist/core/canvas/lifecycle.d.ts +93 -0
  3. package/dist/core/canvas/lifecycle.d.ts.map +1 -0
  4. package/dist/core/canvas/lifecycle.js +165 -0
  5. package/dist/core/canvas/lifecycle.js.map +1 -0
  6. package/dist/core/canvas/registry.d.ts +89 -0
  7. package/dist/core/canvas/registry.d.ts.map +1 -1
  8. package/dist/core/canvas/registry.js +205 -10
  9. package/dist/core/canvas/registry.js.map +1 -1
  10. package/dist/core/canvas/scaffold.d.ts +123 -0
  11. package/dist/core/canvas/scaffold.d.ts.map +1 -0
  12. package/dist/core/canvas/scaffold.js +376 -0
  13. package/dist/core/canvas/scaffold.js.map +1 -0
  14. package/dist/core/canvas/session.d.ts +39 -1
  15. package/dist/core/canvas/session.d.ts.map +1 -1
  16. package/dist/core/canvas/session.js +83 -1
  17. package/dist/core/canvas/session.js.map +1 -1
  18. package/dist/core/tools/canvas.d.ts +23 -3
  19. package/dist/core/tools/canvas.d.ts.map +1 -1
  20. package/dist/core/tools/canvas.js +99 -4
  21. package/dist/core/tools/canvas.js.map +1 -1
  22. package/dist/extensions/core/canvas.d.ts +20 -2
  23. package/dist/extensions/core/canvas.d.ts.map +1 -1
  24. package/dist/extensions/core/canvas.js +279 -36
  25. package/dist/extensions/core/canvas.js.map +1 -1
  26. package/dist/extensions/core/scaffold.d.ts +7 -1
  27. package/dist/extensions/core/scaffold.d.ts.map +1 -1
  28. package/dist/extensions/core/scaffold.js +7 -185
  29. package/dist/extensions/core/scaffold.js.map +1 -1
  30. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  31. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  32. package/examples/extensions/sandbox/package.json +1 -1
  33. package/examples/extensions/with-deps/package.json +1 -1
  34. package/package.json +4 -4
@@ -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 { removeCanvasExtension, renameCanvasExtension, } from "./lifecycle.js";
19
20
  import { pluginCanvasExtensions } from "./plugin-canvases.js";
20
21
  import { CanvasRegistry } from "./registry.js";
21
22
  import { gateCanvasExtensions } from "./trust.js";
@@ -113,7 +114,13 @@ export class CanvasSession {
113
114
  open: [],
114
115
  });
115
116
  }
116
- return { availability, listings, withheldCount: withheld.length };
117
+ const actionsByInstance = new Map();
118
+ for (const binding of this.registryOrUndefined()?.activeActions() ?? []) {
119
+ const names = actionsByInstance.get(binding.instanceId) ?? [];
120
+ names.push(binding.action.name);
121
+ actionsByInstance.set(binding.instanceId, names);
122
+ }
123
+ return { availability, listings, withheldCount: withheld.length, actionsByInstance };
117
124
  }
118
125
  /**
119
126
  * Open a canvas.
@@ -148,6 +155,81 @@ export class CanvasSession {
148
155
  await registry.close(instance);
149
156
  return instance;
150
157
  }
158
+ /**
159
+ * Re-fork an open extension so an edit to its code takes effect.
160
+ *
161
+ * Reached by extension id rather than instance id because a reload restarts the
162
+ * *process*, and one child serves every instance of every canvas the extension
163
+ * declares — pretending it could reload one instance would be a lie about what
164
+ * happens. {@link CanvasRegistry.reload} carries the open instances across.
165
+ */
166
+ async reload(extensionId, options) {
167
+ const registry = this.registryOrUndefined();
168
+ if (!registry) {
169
+ throw new Error(`Canvas extension "${extensionId}" is not running, so there is nothing to reload. Open it first.`);
170
+ }
171
+ return registry.reload(extensionId, options);
172
+ }
173
+ /**
174
+ * Rename a canvas extension, closing anything it has open first.
175
+ *
176
+ * Closing is not politeness: the directory is about to move, and an instance
177
+ * left open would be serving from a path that no longer exists while the
178
+ * registry still believed it was there. The closed instance ids are returned so
179
+ * the caller can say what it cost.
180
+ */
181
+ async rename(extensionId, to) {
182
+ const extension = this.find(extensionId);
183
+ if (!extension)
184
+ return { reason: "unwritable", detail: this.notFound(extensionId) };
185
+ await this.closeAllOf(extensionId);
186
+ return renameCanvasExtension(extension, to, this.roots);
187
+ }
188
+ /** Delete a canvas extension, closing anything it has open first. */
189
+ async remove(extensionId) {
190
+ const extension = this.find(extensionId);
191
+ if (!extension)
192
+ return { reason: "unwritable", detail: this.notFound(extensionId) };
193
+ await this.closeAllOf(extensionId);
194
+ return removeCanvasExtension(extension, this.roots);
195
+ }
196
+ /**
197
+ * Close every instance of an extension and stop its child.
198
+ *
199
+ * `close` alone would leave the process alive for its linger period, still
200
+ * holding the code we are about to move or delete. Rename and remove both need
201
+ * it actually gone.
202
+ */
203
+ async closeAllOf(extensionId) {
204
+ const registry = this.registryOrUndefined();
205
+ if (!registry)
206
+ return [];
207
+ const open = registry.listInstances().filter((instance) => instance.extensionId === extensionId);
208
+ for (const instance of open)
209
+ await registry.close(instance);
210
+ await registry.stopChild(extensionId);
211
+ return open.map((instance) => instance.instanceId);
212
+ }
213
+ /** The discovered extension with this id, runnable or withheld. */
214
+ find(extensionId) {
215
+ const { runnable, withheld } = this.discover();
216
+ return [...runnable, ...withheld].find((candidate) => candidate.id === extensionId);
217
+ }
218
+ notFound(extensionId) {
219
+ const known = this.discover()
220
+ .runnable.map((candidate) => candidate.id)
221
+ .join(", ") || "none";
222
+ return `No canvas extension "${extensionId}" (found: ${known}).`;
223
+ }
224
+ /** Everything that could be renamed or removed, for completions and messages. */
225
+ knownExtensionIds() {
226
+ const { runnable, withheld } = this.discover();
227
+ return [...runnable, ...withheld].map((candidate) => candidate.id).sort();
228
+ }
229
+ /** The extension ids with at least one open instance — what {@link reload} accepts. */
230
+ runningExtensionIds() {
231
+ return [...new Set(this.instances().map((instance) => instance.extensionId))];
232
+ }
151
233
  /** Every open instance. */
152
234
  instances() {
153
235
  return this.registryOrUndefined()?.listInstances() ?? [];
@@ -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 { 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"]}
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 {\n\ttype CanvasLifecycleRefusal,\n\ttype CanvasRemoveResult,\n\ttype CanvasRenameResult,\n\tremoveCanvasExtension,\n\trenameCanvasExtension,\n} from \"./lifecycle.js\";\nimport { pluginCanvasExtensions } from \"./plugin-canvases.js\";\nimport { type CanvasInstance, CanvasRegistry, type CanvasRegistryEvents, type CanvasReloadResult } 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\t/**\n\t * Action names per open instance.\n\t *\n\t * Beside the listings rather than inside them because an action belongs to a\n\t * running instance, not to a canvas on disk: a listing exists for extensions\n\t * that have never been forked, and those have no actions to report — not zero\n\t * of them, none knowable.\n\t */\n\tactionsByInstance: Map<string, string[]>;\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\tconst actionsByInstance = new Map<string, string[]>();\n\t\tfor (const binding of this.registryOrUndefined()?.activeActions() ?? []) {\n\t\t\tconst names = actionsByInstance.get(binding.instanceId) ?? [];\n\t\t\tnames.push(binding.action.name);\n\t\t\tactionsByInstance.set(binding.instanceId, names);\n\t\t}\n\t\treturn { availability, listings, withheldCount: withheld.length, actionsByInstance };\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/**\n\t * Re-fork an open extension so an edit to its code takes effect.\n\t *\n\t * Reached by extension id rather than instance id because a reload restarts the\n\t * *process*, and one child serves every instance of every canvas the extension\n\t * declares — pretending it could reload one instance would be a lie about what\n\t * happens. {@link CanvasRegistry.reload} carries the open instances across.\n\t */\n\tasync reload(extensionId: string, options?: CanvasCallOptions): Promise<CanvasReloadResult> {\n\t\tconst registry = this.registryOrUndefined();\n\t\tif (!registry) {\n\t\t\tthrow new Error(\n\t\t\t\t`Canvas extension \"${extensionId}\" is not running, so there is nothing to reload. Open it first.`,\n\t\t\t);\n\t\t}\n\t\treturn registry.reload(extensionId, options);\n\t}\n\n\t/**\n\t * Rename a canvas extension, closing anything it has open first.\n\t *\n\t * Closing is not politeness: the directory is about to move, and an instance\n\t * left open would be serving from a path that no longer exists while the\n\t * registry still believed it was there. The closed instance ids are returned so\n\t * the caller can say what it cost.\n\t */\n\tasync rename(extensionId: string, to: string): Promise<CanvasRenameResult | CanvasLifecycleRefusal> {\n\t\tconst extension = this.find(extensionId);\n\t\tif (!extension) return { reason: \"unwritable\", detail: this.notFound(extensionId) };\n\t\tawait this.closeAllOf(extensionId);\n\t\treturn renameCanvasExtension(extension, to, this.roots);\n\t}\n\n\t/** Delete a canvas extension, closing anything it has open first. */\n\tasync remove(extensionId: string): Promise<CanvasRemoveResult | CanvasLifecycleRefusal> {\n\t\tconst extension = this.find(extensionId);\n\t\tif (!extension) return { reason: \"unwritable\", detail: this.notFound(extensionId) };\n\t\tawait this.closeAllOf(extensionId);\n\t\treturn removeCanvasExtension(extension, this.roots);\n\t}\n\n\t/**\n\t * Close every instance of an extension and stop its child.\n\t *\n\t * `close` alone would leave the process alive for its linger period, still\n\t * holding the code we are about to move or delete. Rename and remove both need\n\t * it actually gone.\n\t */\n\tprivate async closeAllOf(extensionId: string): Promise<string[]> {\n\t\tconst registry = this.registryOrUndefined();\n\t\tif (!registry) return [];\n\t\tconst open = registry.listInstances().filter((instance) => instance.extensionId === extensionId);\n\t\tfor (const instance of open) await registry.close(instance);\n\t\tawait registry.stopChild(extensionId);\n\t\treturn open.map((instance) => instance.instanceId);\n\t}\n\n\t/** The discovered extension with this id, runnable or withheld. */\n\tprivate find(extensionId: string): DiscoveredCanvasExtension | undefined {\n\t\tconst { runnable, withheld } = this.discover();\n\t\treturn [...runnable, ...withheld].find((candidate) => candidate.id === extensionId);\n\t}\n\n\tprivate notFound(extensionId: string): string {\n\t\tconst known =\n\t\t\tthis.discover()\n\t\t\t\t.runnable.map((candidate) => candidate.id)\n\t\t\t\t.join(\", \") || \"none\";\n\t\treturn `No canvas extension \"${extensionId}\" (found: ${known}).`;\n\t}\n\n\t/** Everything that could be renamed or removed, for completions and messages. */\n\tknownExtensionIds(): string[] {\n\t\tconst { runnable, withheld } = this.discover();\n\t\treturn [...runnable, ...withheld].map((candidate) => candidate.id).sort();\n\t}\n\n\t/** The extension ids with at least one open instance — what {@link reload} accepts. */\n\trunningExtensionIds(): string[] {\n\t\treturn [...new Set(this.instances().map((instance) => instance.extensionId))];\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,13 +1,19 @@
1
1
  /**
2
- * The two agent-facing canvas tools.
2
+ * The three agent-facing canvas tools.
3
3
  *
4
4
  * Design: `docs/canvas-extensions-design.md` §11.5. Copilot names its own shape in
5
5
  * the SDK types — `list_canvas_capabilities` to discover, `invoke_canvas_action` to
6
6
  * invoke — and hoocode mirrors it, for its own reason as well as fidelity:
7
7
  * `AGENTS.md` budgets the prompt at ~4,140 tokens with ~2,710 of it tool schemas,
8
8
  * and every active tool's schema is re-sent on every request. One tool per open
9
- * action would make that surface grow with how many canvases are open. Two fixed
10
- * tools keep it flat.
9
+ * action would make that surface grow with how many canvases are open. Fixed tools
10
+ * keep it flat.
11
+ *
12
+ * `reload_canvas` is hoocode's, not Copilot's, and it is what makes a canvas
13
+ * something the agent can *iterate on* rather than only drive: an edit to
14
+ * `extension.mjs` is invisible until the child forked from the old bytes is
15
+ * replaced. See {@link createReloadTool} for why reloading is the agent's to do
16
+ * while opening is not.
11
17
  *
12
18
  * **There is deliberately no "open a canvas" tool.** Opening forks a process and
13
19
  * binds a listening socket; that is a person's decision, gated by workspace trust
@@ -24,6 +30,11 @@ import { type ToolDefinition } from "../extensions/types.js";
24
30
  export declare const LIST_CANVAS_CAPABILITIES_TOOL_NAME = "list_canvas_capabilities";
25
31
  /** Tool name for action invocation, matching Copilot's. */
26
32
  export declare const INVOKE_CANVAS_ACTION_TOOL_NAME = "invoke_canvas_action";
33
+ /**
34
+ * Tool name for re-forking an edited extension. hoocode's own — Copilot has no
35
+ * equivalent because its `/create-canvas` flow reloads the panel itself.
36
+ */
37
+ export declare const RELOAD_CANVAS_TOOL_NAME = "reload_canvas";
27
38
  /**
28
39
  * Ceiling on a serialized action result, in characters.
29
40
  *
@@ -37,6 +48,15 @@ export interface CanvasCapabilitiesDetails {
37
48
  instances: number;
38
49
  actions: number;
39
50
  }
51
+ /** What `reload_canvas` reports. */
52
+ export interface CanvasReloadDetails {
53
+ extensionId: string;
54
+ reopened: number;
55
+ dropped: number;
56
+ actionsAdded: number;
57
+ actionsRemoved: number;
58
+ actionsChanged: number;
59
+ }
40
60
  /** What `invoke_canvas_action` reports. */
41
61
  export interface CanvasInvokeDetails {
42
62
  instanceId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../../../src/core/tools/canvas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAEzE,8DAA8D;AAC9D,eAAO,MAAM,kCAAkC,6BAA6B,CAAC;AAC7E,2DAA2D;AAC3D,eAAO,MAAM,8BAA8B,yBAAyB,CAAC;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAe7C,+CAA+C;AAC/C,MAAM,WAAW,yBAAyB;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,2CAA2C;AAC3C,MAAM,WAAW,mBAAmB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACnB;AAkHD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,cAAc,GAAG,cAAc,EAAE,CAGtF","sourcesContent":["/**\n * The two agent-facing canvas tools.\n *\n * Design: `docs/canvas-extensions-design.md` §11.5. Copilot names its own shape in\n * the SDK types — `list_canvas_capabilities` to discover, `invoke_canvas_action` to\n * invoke — and hoocode mirrors it, for its own reason as well as fidelity:\n * `AGENTS.md` budgets the prompt at ~4,140 tokens with ~2,710 of it tool schemas,\n * and every active tool's schema is re-sent on every request. One tool per open\n * action would make that surface grow with how many canvases are open. Two fixed\n * tools keep it flat.\n *\n * **There is deliberately no \"open a canvas\" tool.** Opening forks a process and\n * binds a listening socket; that is a person's decision, gated by workspace trust\n * (§5). The agent drives a surface a human has already opened. This also keeps the\n * injection surface flat: a poisoned issue title rendered into a canvas can at most\n * cause an action on an instance the person chose to open.\n *\n * These are optional tools, created only when canvas support is available and at\n * least one canvas is open — so a repository without canvases pays nothing.\n */\n\nimport { type Static, Type } from \"typebox\";\nimport type { CanvasRegistry } from \"../canvas/registry.js\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\n\n/** Tool name for capability discovery, matching Copilot's. */\nexport const LIST_CANVAS_CAPABILITIES_TOOL_NAME = \"list_canvas_capabilities\";\n/** Tool name for action invocation, matching Copilot's. */\nexport const INVOKE_CANVAS_ACTION_TOOL_NAME = \"invoke_canvas_action\";\n\n/**\n * Ceiling on a serialized action result, in characters.\n *\n * Whatever an action returns lands in the model's context window.\n * `pr-artifact-explorer` truncates its own payloads (`entries.slice(0, 200)`), but\n * nothing in the contract obliges a canvas to, so the host caps it too.\n */\nexport const CANVAS_RESULT_MAX_CHARS = 8_000;\n\nconst listParams = Type.Object({}, { additionalProperties: false });\n\nconst invokeParams = Type.Object(\n\t{\n\t\tinstanceId: Type.String({ description: \"From list_canvas_capabilities.\" }),\n\t\taction: Type.String({ description: \"Action name declared by that instance's canvas.\" }),\n\t\tinput: Type.Optional(Type.Unknown({ description: \"Action input, matching the action's declared schema.\" })),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype InvokeParams = Static<typeof invokeParams>;\n\n/** What `list_canvas_capabilities` reports. */\nexport interface CanvasCapabilitiesDetails {\n\tinstances: number;\n\tactions: number;\n}\n\n/** What `invoke_canvas_action` reports. */\nexport interface CanvasInvokeDetails {\n\tinstanceId: string;\n\taction: string;\n\ttruncated: boolean;\n}\n\nfunction textResult(text: string) {\n\treturn { content: [{ type: \"text\" as const, text }] };\n}\n\n/** Serialize an action result, capped so a chatty canvas cannot flood the context. */\nfunction renderResult(value: unknown): { text: string; truncated: boolean } {\n\tconst serialized = value === undefined ? \"null\" : JSON.stringify(value, null, 1);\n\tif (serialized.length <= CANVAS_RESULT_MAX_CHARS) return { text: serialized, truncated: false };\n\treturn {\n\t\ttext: `${serialized.slice(0, CANVAS_RESULT_MAX_CHARS)}\\n… truncated at ${CANVAS_RESULT_MAX_CHARS} characters.`,\n\t\ttruncated: true,\n\t};\n}\n\n/**\n * Discovery: every open instance, its canvas, and the actions it declares with\n * their input schemas.\n *\n * Takes no parameters. A filter would add schema bytes on every request to save\n * bytes in a response the model reads once.\n */\nfunction createListCapabilitiesTool(registry: CanvasRegistry): ToolDefinition {\n\treturn defineTool<typeof listParams, CanvasCapabilitiesDetails>({\n\t\tname: LIST_CANVAS_CAPABILITIES_TOOL_NAME,\n\t\tlabel: LIST_CANVAS_CAPABILITIES_TOOL_NAME,\n\t\tdescription:\n\t\t\t\"List the open canvases and the actions each one accepts, with their input schemas. Call this before invoke_canvas_action to learn the instanceId and the action's schema.\",\n\t\tpromptSnippet: \"Discover open canvases and the actions they accept\",\n\t\tparameters: listParams,\n\t\tasync execute() {\n\t\t\tconst instances = registry.listInstances();\n\t\t\tconst bindings = registry.activeActions();\n\t\t\tif (instances.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\t...textResult(\"No canvas is open. A person opens a canvas; you can then drive it.\"),\n\t\t\t\t\tdetails: { instances: 0, actions: 0 },\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst report = instances.map((instance) => ({\n\t\t\t\tinstanceId: instance.instanceId,\n\t\t\t\tcanvas: instance.canvasId,\n\t\t\t\textension: instance.extensionId,\n\t\t\t\ttitle: instance.title,\n\t\t\t\tstatus: instance.status,\n\t\t\t\tactions: bindings\n\t\t\t\t\t.filter((binding) => binding.instanceId === instance.instanceId)\n\t\t\t\t\t.map((binding) => binding.action),\n\t\t\t}));\n\t\t\treturn {\n\t\t\t\t...textResult(JSON.stringify(report, null, 1)),\n\t\t\t\tdetails: { instances: instances.length, actions: bindings.length },\n\t\t\t};\n\t\t},\n\t});\n}\n\n/** Invocation: run one declared action against one open instance. */\nfunction createInvokeActionTool(registry: CanvasRegistry): ToolDefinition {\n\treturn defineTool<typeof invokeParams, CanvasInvokeDetails>({\n\t\tname: INVOKE_CANVAS_ACTION_TOOL_NAME,\n\t\tlabel: INVOKE_CANVAS_ACTION_TOOL_NAME,\n\t\t// The description deliberately makes no safety claim. An earlier version said\n\t\t// actions \"cannot edit files or run commands\", which is false: a canvas\n\t\t// extension is arbitrary Node code running with the user's privileges, and\n\t\t// `pr-artifact-explorer` really does download artifacts to disk and call the\n\t\t// GitHub API. Those side effects never pass hoocode's permission gate, because\n\t\t// the gate sits in front of hoocode's own tools, not inside a forked\n\t\t// extension — the workspace-trust gate (canvas/trust.ts) is the control here,\n\t\t// not a sentence in a tool schema. Never tell the model a safety property the\n\t\t// runtime does not enforce.\n\t\tdescription:\n\t\t\t\"Invoke an action on an open canvas. Actions are implemented by the canvas extension itself: an action may change what the person is looking at and can have side effects of its own, so read the action's description before calling it.\",\n\t\tpromptSnippet: \"Act on an open canvas the user is looking at\",\n\t\tparameters: invokeParams,\n\t\tasync execute(_toolCallId, params: InvokeParams, signal) {\n\t\t\t// instanceId is a UUID and unique across every canvas, so the model does not\n\t\t\t// have to carry the extension and canvas ids too — the registry already knows\n\t\t\t// which instance a given id belongs to.\n\t\t\tconst instance = registry.listInstances().find((open) => open.instanceId === params.instanceId);\n\t\t\tif (!instance) {\n\t\t\t\t// Tools report failure by throwing here, as the built-ins do; the loop turns\n\t\t\t\t// a rejection into the model's tool result.\n\t\t\t\tconst open = registry.listInstances().map((other) => other.instanceId);\n\t\t\t\tthrow new Error(\n\t\t\t\t\topen.length === 0\n\t\t\t\t\t\t? \"No canvas is open, so there is nothing to act on.\"\n\t\t\t\t\t\t: `No open canvas instance \"${params.instanceId}\". Open instances: ${open.join(\", \")}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t// Honour the turn's abort signal: without this, aborting a turn leaves the\n\t\t\t\t// request running and its answer arriving for a turn nobody awaits.\n\t\t\t\tconst result = await registry.invokeAction(instance, params.action, params.input as never, { signal });\n\t\t\t\tconst { text, truncated } = renderResult(result);\n\t\t\t\treturn {\n\t\t\t\t\t...textResult(text),\n\t\t\t\t\tdetails: { instanceId: params.instanceId, action: params.action, truncated },\n\t\t\t\t};\n\t\t\t} catch (cause) {\n\t\t\t\t// Canvas handlers throw CanvasError with a machine-readable code, which the\n\t\t\t\t// runner preserves across the process boundary as CanvasCallError.code. Only\n\t\t\t\t// the message is rendered to the model, so fold the code into it rather than\n\t\t\t\t// letting the typed-error intent (§8) stop at the tool boundary.\n\t\t\t\tconst code = cause instanceof Error && \"code\" in cause ? String(cause.code) : undefined;\n\t\t\t\tconst message = cause instanceof Error ? cause.message : String(cause);\n\t\t\t\tthrow new Error(code ? `${code}: ${message}` : message);\n\t\t\t}\n\t\t},\n\t});\n}\n\n/**\n * The canvas tools, or none.\n *\n * Returns an empty array while nothing is open, so the two schemas are absent from\n * the prompt in the overwhelmingly common case of a repository with no canvases —\n * the same reason `registry.activeActions()` is empty until an instance exists\n * (§7). Callers re-derive this when the open set changes.\n */\nexport function createCanvasToolDefinitions(registry: CanvasRegistry): ToolDefinition[] {\n\tif (registry.listInstances().length === 0) return [];\n\treturn [createListCapabilitiesTool(registry), createInvokeActionTool(registry)];\n}\n"]}
1
+ {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../../../src/core/tools/canvas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAEzE,8DAA8D;AAC9D,eAAO,MAAM,kCAAkC,6BAA6B,CAAC;AAC7E,2DAA2D;AAC3D,eAAO,MAAM,8BAA8B,yBAAyB,CAAC;AACrE;;;GAGG;AACH,eAAO,MAAM,uBAAuB,kBAAkB,CAAC;AAEvD;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AA0B7C,+CAA+C;AAC/C,MAAM,WAAW,yBAAyB;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,oCAAoC;AACpC,MAAM,WAAW,mBAAmB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACvB;AAED,2CAA2C;AAC3C,MAAM,WAAW,mBAAmB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACnB;AAgND;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,cAAc,GAAG,cAAc,EAAE,CAGtF","sourcesContent":["/**\n * The three agent-facing canvas tools.\n *\n * Design: `docs/canvas-extensions-design.md` §11.5. Copilot names its own shape in\n * the SDK types — `list_canvas_capabilities` to discover, `invoke_canvas_action` to\n * invoke — and hoocode mirrors it, for its own reason as well as fidelity:\n * `AGENTS.md` budgets the prompt at ~4,140 tokens with ~2,710 of it tool schemas,\n * and every active tool's schema is re-sent on every request. One tool per open\n * action would make that surface grow with how many canvases are open. Fixed tools\n * keep it flat.\n *\n * `reload_canvas` is hoocode's, not Copilot's, and it is what makes a canvas\n * something the agent can *iterate on* rather than only drive: an edit to\n * `extension.mjs` is invisible until the child forked from the old bytes is\n * replaced. See {@link createReloadTool} for why reloading is the agent's to do\n * while opening is not.\n *\n * **There is deliberately no \"open a canvas\" tool.** Opening forks a process and\n * binds a listening socket; that is a person's decision, gated by workspace trust\n * (§5). The agent drives a surface a human has already opened. This also keeps the\n * injection surface flat: a poisoned issue title rendered into a canvas can at most\n * cause an action on an instance the person chose to open.\n *\n * These are optional tools, created only when canvas support is available and at\n * least one canvas is open — so a repository without canvases pays nothing.\n */\n\nimport { type Static, Type } from \"typebox\";\nimport type { CanvasRegistry } from \"../canvas/registry.js\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\n\n/** Tool name for capability discovery, matching Copilot's. */\nexport const LIST_CANVAS_CAPABILITIES_TOOL_NAME = \"list_canvas_capabilities\";\n/** Tool name for action invocation, matching Copilot's. */\nexport const INVOKE_CANVAS_ACTION_TOOL_NAME = \"invoke_canvas_action\";\n/**\n * Tool name for re-forking an edited extension. hoocode's own — Copilot has no\n * equivalent because its `/create-canvas` flow reloads the panel itself.\n */\nexport const RELOAD_CANVAS_TOOL_NAME = \"reload_canvas\";\n\n/**\n * Ceiling on a serialized action result, in characters.\n *\n * Whatever an action returns lands in the model's context window.\n * `pr-artifact-explorer` truncates its own payloads (`entries.slice(0, 200)`), but\n * nothing in the contract obliges a canvas to, so the host caps it too.\n */\nexport const CANVAS_RESULT_MAX_CHARS = 8_000;\n\nconst listParams = Type.Object({}, { additionalProperties: false });\n\nconst reloadParams = Type.Object(\n\t{\n\t\textensionId: Type.String({\n\t\t\tdescription: \"The extension whose code changed. From list_canvas_capabilities (the `extension` field).\",\n\t\t}),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype ReloadParams = Static<typeof reloadParams>;\n\nconst invokeParams = Type.Object(\n\t{\n\t\tinstanceId: Type.String({ description: \"From list_canvas_capabilities.\" }),\n\t\taction: Type.String({ description: \"Action name declared by that instance's canvas.\" }),\n\t\tinput: Type.Optional(Type.Unknown({ description: \"Action input, matching the action's declared schema.\" })),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype InvokeParams = Static<typeof invokeParams>;\n\n/** What `list_canvas_capabilities` reports. */\nexport interface CanvasCapabilitiesDetails {\n\tinstances: number;\n\tactions: number;\n}\n\n/** What `reload_canvas` reports. */\nexport interface CanvasReloadDetails {\n\textensionId: string;\n\treopened: number;\n\tdropped: number;\n\tactionsAdded: number;\n\tactionsRemoved: number;\n\tactionsChanged: number;\n}\n\n/** What `invoke_canvas_action` reports. */\nexport interface CanvasInvokeDetails {\n\tinstanceId: string;\n\taction: string;\n\ttruncated: boolean;\n}\n\nfunction textResult(text: string) {\n\treturn { content: [{ type: \"text\" as const, text }] };\n}\n\n/** Serialize an action result, capped so a chatty canvas cannot flood the context. */\nfunction renderResult(value: unknown): { text: string; truncated: boolean } {\n\tconst serialized = value === undefined ? \"null\" : JSON.stringify(value, null, 1);\n\tif (serialized.length <= CANVAS_RESULT_MAX_CHARS) return { text: serialized, truncated: false };\n\treturn {\n\t\ttext: `${serialized.slice(0, CANVAS_RESULT_MAX_CHARS)}\\n… truncated at ${CANVAS_RESULT_MAX_CHARS} characters.`,\n\t\ttruncated: true,\n\t};\n}\n\n/**\n * Discovery: every open instance, its canvas, and the actions it declares with\n * their input schemas.\n *\n * Takes no parameters. A filter would add schema bytes on every request to save\n * bytes in a response the model reads once.\n */\nfunction createListCapabilitiesTool(registry: CanvasRegistry): ToolDefinition {\n\treturn defineTool<typeof listParams, CanvasCapabilitiesDetails>({\n\t\tname: LIST_CANVAS_CAPABILITIES_TOOL_NAME,\n\t\tlabel: LIST_CANVAS_CAPABILITIES_TOOL_NAME,\n\t\tdescription:\n\t\t\t\"List the open canvases and the actions each one accepts, with their input schemas. Call this before invoke_canvas_action to learn the instanceId and the action's schema.\",\n\t\tpromptSnippet: \"Discover open canvases and the actions they accept\",\n\t\tparameters: listParams,\n\t\tasync execute() {\n\t\t\tconst instances = registry.listInstances();\n\t\t\tconst bindings = registry.activeActions();\n\t\t\tif (instances.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\t...textResult(\"No canvas is open. A person opens a canvas; you can then drive it.\"),\n\t\t\t\t\tdetails: { instances: 0, actions: 0 },\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst report = instances.map((instance) => ({\n\t\t\t\tinstanceId: instance.instanceId,\n\t\t\t\tcanvas: instance.canvasId,\n\t\t\t\textension: instance.extensionId,\n\t\t\t\ttitle: instance.title,\n\t\t\t\tstatus: instance.status,\n\t\t\t\tactions: bindings\n\t\t\t\t\t.filter((binding) => binding.instanceId === instance.instanceId)\n\t\t\t\t\t.map((binding) => binding.action),\n\t\t\t}));\n\t\t\treturn {\n\t\t\t\t...textResult(JSON.stringify(report, null, 1)),\n\t\t\t\tdetails: { instances: instances.length, actions: bindings.length },\n\t\t\t};\n\t\t},\n\t});\n}\n\n/** Invocation: run one declared action against one open instance. */\nfunction createInvokeActionTool(registry: CanvasRegistry): ToolDefinition {\n\treturn defineTool<typeof invokeParams, CanvasInvokeDetails>({\n\t\tname: INVOKE_CANVAS_ACTION_TOOL_NAME,\n\t\tlabel: INVOKE_CANVAS_ACTION_TOOL_NAME,\n\t\t// The description deliberately makes no safety claim. An earlier version said\n\t\t// actions \"cannot edit files or run commands\", which is false: a canvas\n\t\t// extension is arbitrary Node code running with the user's privileges, and\n\t\t// `pr-artifact-explorer` really does download artifacts to disk and call the\n\t\t// GitHub API. Those side effects never pass hoocode's permission gate, because\n\t\t// the gate sits in front of hoocode's own tools, not inside a forked\n\t\t// extension — the workspace-trust gate (canvas/trust.ts) is the control here,\n\t\t// not a sentence in a tool schema. Never tell the model a safety property the\n\t\t// runtime does not enforce.\n\t\tdescription:\n\t\t\t\"Invoke an action on an open canvas. Actions are implemented by the canvas extension itself: an action may change what the person is looking at and can have side effects of its own, so read the action's description before calling it.\",\n\t\tpromptSnippet: \"Act on an open canvas the user is looking at\",\n\t\tparameters: invokeParams,\n\t\tasync execute(_toolCallId, params: InvokeParams, signal) {\n\t\t\t// instanceId is a UUID and unique across every canvas, so the model does not\n\t\t\t// have to carry the extension and canvas ids too — the registry already knows\n\t\t\t// which instance a given id belongs to.\n\t\t\tconst instance = registry.listInstances().find((open) => open.instanceId === params.instanceId);\n\t\t\tif (!instance) {\n\t\t\t\t// Tools report failure by throwing here, as the built-ins do; the loop turns\n\t\t\t\t// a rejection into the model's tool result.\n\t\t\t\tconst open = registry.listInstances().map((other) => other.instanceId);\n\t\t\t\tthrow new Error(\n\t\t\t\t\topen.length === 0\n\t\t\t\t\t\t? \"No canvas is open, so there is nothing to act on.\"\n\t\t\t\t\t\t: `No open canvas instance \"${params.instanceId}\". Open instances: ${open.join(\", \")}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t// Honour the turn's abort signal: without this, aborting a turn leaves the\n\t\t\t\t// request running and its answer arriving for a turn nobody awaits.\n\t\t\t\tconst result = await registry.invokeAction(instance, params.action, params.input as never, { signal });\n\t\t\t\tconst { text, truncated } = renderResult(result);\n\t\t\t\treturn {\n\t\t\t\t\t...textResult(text),\n\t\t\t\t\tdetails: { instanceId: params.instanceId, action: params.action, truncated },\n\t\t\t\t};\n\t\t\t} catch (cause) {\n\t\t\t\t// Canvas handlers throw CanvasError with a machine-readable code, which the\n\t\t\t\t// runner preserves across the process boundary as CanvasCallError.code. Only\n\t\t\t\t// the message is rendered to the model, so fold the code into it rather than\n\t\t\t\t// letting the typed-error intent (§8) stop at the tool boundary.\n\t\t\t\tconst code = cause instanceof Error && \"code\" in cause ? String(cause.code) : undefined;\n\t\t\t\tconst message = cause instanceof Error ? cause.message : String(cause);\n\t\t\t\tthrow new Error(code ? `${code}: ${message}` : message);\n\t\t\t}\n\t\t},\n\t});\n}\n\n/**\n * Reload: re-fork an extension whose source changed, carrying its open instances.\n *\n * This is the tool that makes a canvas *iterable* by the agent — \"add a column\",\n * \"make the header sticky\" — which is the whole point of authoring one in a\n * session. Editing `extension.mjs` alone changes nothing: the child forked from\n * the old bytes keeps serving until it is replaced.\n *\n * It reloads; it does not open. That distinction is what keeps §11.5's reasoning\n * intact. Opening is a person's decision because it starts a process from a\n * directory nobody has vouched for; reloading only restarts an extension the\n * person already opened, in a workspace they already trusted, from a path the\n * host already resolved. The model cannot reach a new extension through it, and\n * a poisoned string in some canvas's data still cannot cause one to start.\n *\n * It is not a safety boundary on the *contents* of the file, and must not be\n * described as one: whatever wrote `extension.mjs` — the model's own edit,\n * through the permission gate — is what runs.\n */\nfunction createReloadTool(registry: CanvasRegistry): ToolDefinition {\n\treturn defineTool<typeof reloadParams, CanvasReloadDetails>({\n\t\tname: RELOAD_CANVAS_TOOL_NAME,\n\t\tlabel: RELOAD_CANVAS_TOOL_NAME,\n\t\tdescription:\n\t\t\t\"Restart an open canvas extension so your edits to its source take effect. Editing the extension's file does nothing on its own — the running process was forked from the old code. Call this after every edit. It reports which actions you added, removed or changed, so use it to confirm an action you just wrote is really callable. Open instances are carried across and keep their instanceId, but each gets a NEW url: tell the person the new url, because the tab they have open is now dead.\",\n\t\tpromptSnippet: \"Restart an edited canvas so the change is live\",\n\t\tparameters: reloadParams,\n\t\tasync execute(_toolCallId, params: ReloadParams, signal) {\n\t\t\tconst running = [...new Set(registry.listInstances().map((instance) => instance.extensionId))];\n\t\t\tif (!running.includes(params.extensionId)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\trunning.length === 0\n\t\t\t\t\t\t? \"No canvas is open, so there is nothing to reload.\"\n\t\t\t\t\t\t: `Canvas extension \"${params.extensionId}\" has nothing open. Running: ${running.join(\", \")}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// A failed reload is the common case while iterating — the edit did not\n\t\t\t// parse, or threw at module scope. The registry leaves the old child\n\t\t\t// serving in that case, so this reads as \"your edit is broken and the\n\t\t\t// canvas is untouched\", which is what the model needs to hear to fix it.\n\t\t\tconst result = await registry.reload(params.extensionId, { signal });\n\n\t\t\tconst lines = [`Reloaded ${params.extensionId}. It declares: ${result.canvases.join(\", \") || \"no canvases\"}.`];\n\n\t\t\t// The capability delta is the answer to the question an author actually has\n\t\t\t// after an edit — did the host see the action I just wrote? Silence would read\n\t\t\t// as success, so \"nothing changed\" is said out loud too.\n\t\t\tconst { added, removed, changed, current } = result.actions;\n\t\t\tif (added.length + removed.length + changed.length === 0) {\n\t\t\t\tlines.push(`Actions unchanged: ${current.join(\", \") || \"none\"}.`);\n\t\t\t} else {\n\t\t\t\tif (added.length > 0) lines.push(`Actions added: ${added.join(\", \")}.`);\n\t\t\t\tif (removed.length > 0) lines.push(`Actions removed: ${removed.join(\", \")}.`);\n\t\t\t\tif (changed.length > 0) {\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\t`Actions changed (description or inputSchema): ${changed.join(\", \")}. Any schema you were holding for these is stale.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tlines.push(`Now callable: ${current.join(\", \") || \"none\"}.`);\n\t\t\t}\n\t\t\tif (result.reopened.length > 0) {\n\t\t\t\tlines.push(\n\t\t\t\t\t\"Re-opened (give the person the new url — their old tab points at a closed port):\",\n\t\t\t\t\t...result.reopened.map(\n\t\t\t\t\t\t(instance) =>\n\t\t\t\t\t\t\t` ${instance.canvasId} (${instance.instanceId})${instance.url ? ` — ${instance.url}` : \"\"}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (result.dropped.length > 0) {\n\t\t\t\tlines.push(\n\t\t\t\t\t\"Did not come back:\",\n\t\t\t\t\t...result.dropped.map((drop) => ` ${drop.canvasId} (${drop.instanceId}): ${drop.reason}`),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (result.reopened.length === 0 && result.dropped.length === 0) {\n\t\t\t\tlines.push(\"Nothing was open, so nothing was re-opened.\");\n\t\t\t}\n\t\t\treturn {\n\t\t\t\t...textResult(lines.join(\"\\n\")),\n\t\t\t\tdetails: {\n\t\t\t\t\textensionId: params.extensionId,\n\t\t\t\t\treopened: result.reopened.length,\n\t\t\t\t\tdropped: result.dropped.length,\n\t\t\t\t\tactionsAdded: added.length,\n\t\t\t\t\tactionsRemoved: removed.length,\n\t\t\t\t\tactionsChanged: changed.length,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n}\n\n/**\n * The canvas tools, or none.\n *\n * Returns an empty array while nothing is open, so the two schemas are absent from\n * the prompt in the overwhelmingly common case of a repository with no canvases —\n * the same reason `registry.activeActions()` is empty until an instance exists\n * (§7). Callers re-derive this when the open set changes.\n */\nexport function createCanvasToolDefinitions(registry: CanvasRegistry): ToolDefinition[] {\n\tif (registry.listInstances().length === 0) return [];\n\treturn [createListCapabilitiesTool(registry), createInvokeActionTool(registry), createReloadTool(registry)];\n}\n"]}
@@ -1,13 +1,19 @@
1
1
  /**
2
- * The two agent-facing canvas tools.
2
+ * The three agent-facing canvas tools.
3
3
  *
4
4
  * Design: `docs/canvas-extensions-design.md` §11.5. Copilot names its own shape in
5
5
  * the SDK types — `list_canvas_capabilities` to discover, `invoke_canvas_action` to
6
6
  * invoke — and hoocode mirrors it, for its own reason as well as fidelity:
7
7
  * `AGENTS.md` budgets the prompt at ~4,140 tokens with ~2,710 of it tool schemas,
8
8
  * and every active tool's schema is re-sent on every request. One tool per open
9
- * action would make that surface grow with how many canvases are open. Two fixed
10
- * tools keep it flat.
9
+ * action would make that surface grow with how many canvases are open. Fixed tools
10
+ * keep it flat.
11
+ *
12
+ * `reload_canvas` is hoocode's, not Copilot's, and it is what makes a canvas
13
+ * something the agent can *iterate on* rather than only drive: an edit to
14
+ * `extension.mjs` is invisible until the child forked from the old bytes is
15
+ * replaced. See {@link createReloadTool} for why reloading is the agent's to do
16
+ * while opening is not.
11
17
  *
12
18
  * **There is deliberately no "open a canvas" tool.** Opening forks a process and
13
19
  * binds a listening socket; that is a person's decision, gated by workspace trust
@@ -24,6 +30,11 @@ import { defineTool } from "../extensions/types.js";
24
30
  export const LIST_CANVAS_CAPABILITIES_TOOL_NAME = "list_canvas_capabilities";
25
31
  /** Tool name for action invocation, matching Copilot's. */
26
32
  export const INVOKE_CANVAS_ACTION_TOOL_NAME = "invoke_canvas_action";
33
+ /**
34
+ * Tool name for re-forking an edited extension. hoocode's own — Copilot has no
35
+ * equivalent because its `/create-canvas` flow reloads the panel itself.
36
+ */
37
+ export const RELOAD_CANVAS_TOOL_NAME = "reload_canvas";
27
38
  /**
28
39
  * Ceiling on a serialized action result, in characters.
29
40
  *
@@ -33,6 +44,11 @@ export const INVOKE_CANVAS_ACTION_TOOL_NAME = "invoke_canvas_action";
33
44
  */
34
45
  export const CANVAS_RESULT_MAX_CHARS = 8_000;
35
46
  const listParams = Type.Object({}, { additionalProperties: false });
47
+ const reloadParams = Type.Object({
48
+ extensionId: Type.String({
49
+ description: "The extension whose code changed. From list_canvas_capabilities (the `extension` field).",
50
+ }),
51
+ }, { additionalProperties: false });
36
52
  const invokeParams = Type.Object({
37
53
  instanceId: Type.String({ description: "From list_canvas_capabilities." }),
38
54
  action: Type.String({ description: "Action name declared by that instance's canvas." }),
@@ -143,6 +159,85 @@ function createInvokeActionTool(registry) {
143
159
  },
144
160
  });
145
161
  }
162
+ /**
163
+ * Reload: re-fork an extension whose source changed, carrying its open instances.
164
+ *
165
+ * This is the tool that makes a canvas *iterable* by the agent — "add a column",
166
+ * "make the header sticky" — which is the whole point of authoring one in a
167
+ * session. Editing `extension.mjs` alone changes nothing: the child forked from
168
+ * the old bytes keeps serving until it is replaced.
169
+ *
170
+ * It reloads; it does not open. That distinction is what keeps §11.5's reasoning
171
+ * intact. Opening is a person's decision because it starts a process from a
172
+ * directory nobody has vouched for; reloading only restarts an extension the
173
+ * person already opened, in a workspace they already trusted, from a path the
174
+ * host already resolved. The model cannot reach a new extension through it, and
175
+ * a poisoned string in some canvas's data still cannot cause one to start.
176
+ *
177
+ * It is not a safety boundary on the *contents* of the file, and must not be
178
+ * described as one: whatever wrote `extension.mjs` — the model's own edit,
179
+ * through the permission gate — is what runs.
180
+ */
181
+ function createReloadTool(registry) {
182
+ return defineTool({
183
+ name: RELOAD_CANVAS_TOOL_NAME,
184
+ label: RELOAD_CANVAS_TOOL_NAME,
185
+ description: "Restart an open canvas extension so your edits to its source take effect. Editing the extension's file does nothing on its own — the running process was forked from the old code. Call this after every edit. It reports which actions you added, removed or changed, so use it to confirm an action you just wrote is really callable. Open instances are carried across and keep their instanceId, but each gets a NEW url: tell the person the new url, because the tab they have open is now dead.",
186
+ promptSnippet: "Restart an edited canvas so the change is live",
187
+ parameters: reloadParams,
188
+ async execute(_toolCallId, params, signal) {
189
+ const running = [...new Set(registry.listInstances().map((instance) => instance.extensionId))];
190
+ if (!running.includes(params.extensionId)) {
191
+ throw new Error(running.length === 0
192
+ ? "No canvas is open, so there is nothing to reload."
193
+ : `Canvas extension "${params.extensionId}" has nothing open. Running: ${running.join(", ")}.`);
194
+ }
195
+ // A failed reload is the common case while iterating — the edit did not
196
+ // parse, or threw at module scope. The registry leaves the old child
197
+ // serving in that case, so this reads as "your edit is broken and the
198
+ // canvas is untouched", which is what the model needs to hear to fix it.
199
+ const result = await registry.reload(params.extensionId, { signal });
200
+ const lines = [`Reloaded ${params.extensionId}. It declares: ${result.canvases.join(", ") || "no canvases"}.`];
201
+ // The capability delta is the answer to the question an author actually has
202
+ // after an edit — did the host see the action I just wrote? Silence would read
203
+ // as success, so "nothing changed" is said out loud too.
204
+ const { added, removed, changed, current } = result.actions;
205
+ if (added.length + removed.length + changed.length === 0) {
206
+ lines.push(`Actions unchanged: ${current.join(", ") || "none"}.`);
207
+ }
208
+ else {
209
+ if (added.length > 0)
210
+ lines.push(`Actions added: ${added.join(", ")}.`);
211
+ if (removed.length > 0)
212
+ lines.push(`Actions removed: ${removed.join(", ")}.`);
213
+ if (changed.length > 0) {
214
+ lines.push(`Actions changed (description or inputSchema): ${changed.join(", ")}. Any schema you were holding for these is stale.`);
215
+ }
216
+ lines.push(`Now callable: ${current.join(", ") || "none"}.`);
217
+ }
218
+ if (result.reopened.length > 0) {
219
+ lines.push("Re-opened (give the person the new url — their old tab points at a closed port):", ...result.reopened.map((instance) => ` ${instance.canvasId} (${instance.instanceId})${instance.url ? ` — ${instance.url}` : ""}`));
220
+ }
221
+ if (result.dropped.length > 0) {
222
+ lines.push("Did not come back:", ...result.dropped.map((drop) => ` ${drop.canvasId} (${drop.instanceId}): ${drop.reason}`));
223
+ }
224
+ if (result.reopened.length === 0 && result.dropped.length === 0) {
225
+ lines.push("Nothing was open, so nothing was re-opened.");
226
+ }
227
+ return {
228
+ ...textResult(lines.join("\n")),
229
+ details: {
230
+ extensionId: params.extensionId,
231
+ reopened: result.reopened.length,
232
+ dropped: result.dropped.length,
233
+ actionsAdded: added.length,
234
+ actionsRemoved: removed.length,
235
+ actionsChanged: changed.length,
236
+ },
237
+ };
238
+ },
239
+ });
240
+ }
146
241
  /**
147
242
  * The canvas tools, or none.
148
243
  *
@@ -154,6 +249,6 @@ function createInvokeActionTool(registry) {
154
249
  export function createCanvasToolDefinitions(registry) {
155
250
  if (registry.listInstances().length === 0)
156
251
  return [];
157
- return [createListCapabilitiesTool(registry), createInvokeActionTool(registry)];
252
+ return [createListCapabilitiesTool(registry), createInvokeActionTool(registry), createReloadTool(registry)];
158
253
  }
159
254
  //# sourceMappingURL=canvas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../../src/core/tools/canvas.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The two agent-facing canvas tools.\n *\n * Design: `docs/canvas-extensions-design.md` §11.5. Copilot names its own shape in\n * the SDK types — `list_canvas_capabilities` to discover, `invoke_canvas_action` to\n * invoke — and hoocode mirrors it, for its own reason as well as fidelity:\n * `AGENTS.md` budgets the prompt at ~4,140 tokens with ~2,710 of it tool schemas,\n * and every active tool's schema is re-sent on every request. One tool per open\n * action would make that surface grow with how many canvases are open. Two fixed\n * tools keep it flat.\n *\n * **There is deliberately no \"open a canvas\" tool.** Opening forks a process and\n * binds a listening socket; that is a person's decision, gated by workspace trust\n * (§5). The agent drives a surface a human has already opened. This also keeps the\n * injection surface flat: a poisoned issue title rendered into a canvas can at most\n * cause an action on an instance the person chose to open.\n *\n * These are optional tools, created only when canvas support is available and at\n * least one canvas is open — so a repository without canvases pays nothing.\n */\n\nimport { type Static, Type } from \"typebox\";\nimport type { CanvasRegistry } from \"../canvas/registry.js\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\n\n/** Tool name for capability discovery, matching Copilot's. */\nexport const LIST_CANVAS_CAPABILITIES_TOOL_NAME = \"list_canvas_capabilities\";\n/** Tool name for action invocation, matching Copilot's. */\nexport const INVOKE_CANVAS_ACTION_TOOL_NAME = \"invoke_canvas_action\";\n\n/**\n * Ceiling on a serialized action result, in characters.\n *\n * Whatever an action returns lands in the model's context window.\n * `pr-artifact-explorer` truncates its own payloads (`entries.slice(0, 200)`), but\n * nothing in the contract obliges a canvas to, so the host caps it too.\n */\nexport const CANVAS_RESULT_MAX_CHARS = 8_000;\n\nconst listParams = Type.Object({}, { additionalProperties: false });\n\nconst invokeParams = Type.Object(\n\t{\n\t\tinstanceId: Type.String({ description: \"From list_canvas_capabilities.\" }),\n\t\taction: Type.String({ description: \"Action name declared by that instance's canvas.\" }),\n\t\tinput: Type.Optional(Type.Unknown({ description: \"Action input, matching the action's declared schema.\" })),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype InvokeParams = Static<typeof invokeParams>;\n\n/** What `list_canvas_capabilities` reports. */\nexport interface CanvasCapabilitiesDetails {\n\tinstances: number;\n\tactions: number;\n}\n\n/** What `invoke_canvas_action` reports. */\nexport interface CanvasInvokeDetails {\n\tinstanceId: string;\n\taction: string;\n\ttruncated: boolean;\n}\n\nfunction textResult(text: string) {\n\treturn { content: [{ type: \"text\" as const, text }] };\n}\n\n/** Serialize an action result, capped so a chatty canvas cannot flood the context. */\nfunction renderResult(value: unknown): { text: string; truncated: boolean } {\n\tconst serialized = value === undefined ? \"null\" : JSON.stringify(value, null, 1);\n\tif (serialized.length <= CANVAS_RESULT_MAX_CHARS) return { text: serialized, truncated: false };\n\treturn {\n\t\ttext: `${serialized.slice(0, CANVAS_RESULT_MAX_CHARS)}\\n… truncated at ${CANVAS_RESULT_MAX_CHARS} characters.`,\n\t\ttruncated: true,\n\t};\n}\n\n/**\n * Discovery: every open instance, its canvas, and the actions it declares with\n * their input schemas.\n *\n * Takes no parameters. A filter would add schema bytes on every request to save\n * bytes in a response the model reads once.\n */\nfunction createListCapabilitiesTool(registry: CanvasRegistry): ToolDefinition {\n\treturn defineTool<typeof listParams, CanvasCapabilitiesDetails>({\n\t\tname: LIST_CANVAS_CAPABILITIES_TOOL_NAME,\n\t\tlabel: LIST_CANVAS_CAPABILITIES_TOOL_NAME,\n\t\tdescription:\n\t\t\t\"List the open canvases and the actions each one accepts, with their input schemas. Call this before invoke_canvas_action to learn the instanceId and the action's schema.\",\n\t\tpromptSnippet: \"Discover open canvases and the actions they accept\",\n\t\tparameters: listParams,\n\t\tasync execute() {\n\t\t\tconst instances = registry.listInstances();\n\t\t\tconst bindings = registry.activeActions();\n\t\t\tif (instances.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\t...textResult(\"No canvas is open. A person opens a canvas; you can then drive it.\"),\n\t\t\t\t\tdetails: { instances: 0, actions: 0 },\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst report = instances.map((instance) => ({\n\t\t\t\tinstanceId: instance.instanceId,\n\t\t\t\tcanvas: instance.canvasId,\n\t\t\t\textension: instance.extensionId,\n\t\t\t\ttitle: instance.title,\n\t\t\t\tstatus: instance.status,\n\t\t\t\tactions: bindings\n\t\t\t\t\t.filter((binding) => binding.instanceId === instance.instanceId)\n\t\t\t\t\t.map((binding) => binding.action),\n\t\t\t}));\n\t\t\treturn {\n\t\t\t\t...textResult(JSON.stringify(report, null, 1)),\n\t\t\t\tdetails: { instances: instances.length, actions: bindings.length },\n\t\t\t};\n\t\t},\n\t});\n}\n\n/** Invocation: run one declared action against one open instance. */\nfunction createInvokeActionTool(registry: CanvasRegistry): ToolDefinition {\n\treturn defineTool<typeof invokeParams, CanvasInvokeDetails>({\n\t\tname: INVOKE_CANVAS_ACTION_TOOL_NAME,\n\t\tlabel: INVOKE_CANVAS_ACTION_TOOL_NAME,\n\t\t// The description deliberately makes no safety claim. An earlier version said\n\t\t// actions \"cannot edit files or run commands\", which is false: a canvas\n\t\t// extension is arbitrary Node code running with the user's privileges, and\n\t\t// `pr-artifact-explorer` really does download artifacts to disk and call the\n\t\t// GitHub API. Those side effects never pass hoocode's permission gate, because\n\t\t// the gate sits in front of hoocode's own tools, not inside a forked\n\t\t// extension — the workspace-trust gate (canvas/trust.ts) is the control here,\n\t\t// not a sentence in a tool schema. Never tell the model a safety property the\n\t\t// runtime does not enforce.\n\t\tdescription:\n\t\t\t\"Invoke an action on an open canvas. Actions are implemented by the canvas extension itself: an action may change what the person is looking at and can have side effects of its own, so read the action's description before calling it.\",\n\t\tpromptSnippet: \"Act on an open canvas the user is looking at\",\n\t\tparameters: invokeParams,\n\t\tasync execute(_toolCallId, params: InvokeParams, signal) {\n\t\t\t// instanceId is a UUID and unique across every canvas, so the model does not\n\t\t\t// have to carry the extension and canvas ids too — the registry already knows\n\t\t\t// which instance a given id belongs to.\n\t\t\tconst instance = registry.listInstances().find((open) => open.instanceId === params.instanceId);\n\t\t\tif (!instance) {\n\t\t\t\t// Tools report failure by throwing here, as the built-ins do; the loop turns\n\t\t\t\t// a rejection into the model's tool result.\n\t\t\t\tconst open = registry.listInstances().map((other) => other.instanceId);\n\t\t\t\tthrow new Error(\n\t\t\t\t\topen.length === 0\n\t\t\t\t\t\t? \"No canvas is open, so there is nothing to act on.\"\n\t\t\t\t\t\t: `No open canvas instance \"${params.instanceId}\". Open instances: ${open.join(\", \")}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t// Honour the turn's abort signal: without this, aborting a turn leaves the\n\t\t\t\t// request running and its answer arriving for a turn nobody awaits.\n\t\t\t\tconst result = await registry.invokeAction(instance, params.action, params.input as never, { signal });\n\t\t\t\tconst { text, truncated } = renderResult(result);\n\t\t\t\treturn {\n\t\t\t\t\t...textResult(text),\n\t\t\t\t\tdetails: { instanceId: params.instanceId, action: params.action, truncated },\n\t\t\t\t};\n\t\t\t} catch (cause) {\n\t\t\t\t// Canvas handlers throw CanvasError with a machine-readable code, which the\n\t\t\t\t// runner preserves across the process boundary as CanvasCallError.code. Only\n\t\t\t\t// the message is rendered to the model, so fold the code into it rather than\n\t\t\t\t// letting the typed-error intent (§8) stop at the tool boundary.\n\t\t\t\tconst code = cause instanceof Error && \"code\" in cause ? String(cause.code) : undefined;\n\t\t\t\tconst message = cause instanceof Error ? cause.message : String(cause);\n\t\t\t\tthrow new Error(code ? `${code}: ${message}` : message);\n\t\t\t}\n\t\t},\n\t});\n}\n\n/**\n * The canvas tools, or none.\n *\n * Returns an empty array while nothing is open, so the two schemas are absent from\n * the prompt in the overwhelmingly common case of a repository with no canvases —\n * the same reason `registry.activeActions()` is empty until an instance exists\n * (§7). Callers re-derive this when the open set changes.\n */\nexport function createCanvasToolDefinitions(registry: CanvasRegistry): ToolDefinition[] {\n\tif (registry.listInstances().length === 0) return [];\n\treturn [createListCapabilitiesTool(registry), createInvokeActionTool(registry)];\n}\n"]}
1
+ {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../../../src/core/tools/canvas.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The three agent-facing canvas tools.\n *\n * Design: `docs/canvas-extensions-design.md` §11.5. Copilot names its own shape in\n * the SDK types — `list_canvas_capabilities` to discover, `invoke_canvas_action` to\n * invoke — and hoocode mirrors it, for its own reason as well as fidelity:\n * `AGENTS.md` budgets the prompt at ~4,140 tokens with ~2,710 of it tool schemas,\n * and every active tool's schema is re-sent on every request. One tool per open\n * action would make that surface grow with how many canvases are open. Fixed tools\n * keep it flat.\n *\n * `reload_canvas` is hoocode's, not Copilot's, and it is what makes a canvas\n * something the agent can *iterate on* rather than only drive: an edit to\n * `extension.mjs` is invisible until the child forked from the old bytes is\n * replaced. See {@link createReloadTool} for why reloading is the agent's to do\n * while opening is not.\n *\n * **There is deliberately no \"open a canvas\" tool.** Opening forks a process and\n * binds a listening socket; that is a person's decision, gated by workspace trust\n * (§5). The agent drives a surface a human has already opened. This also keeps the\n * injection surface flat: a poisoned issue title rendered into a canvas can at most\n * cause an action on an instance the person chose to open.\n *\n * These are optional tools, created only when canvas support is available and at\n * least one canvas is open — so a repository without canvases pays nothing.\n */\n\nimport { type Static, Type } from \"typebox\";\nimport type { CanvasRegistry } from \"../canvas/registry.js\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\n\n/** Tool name for capability discovery, matching Copilot's. */\nexport const LIST_CANVAS_CAPABILITIES_TOOL_NAME = \"list_canvas_capabilities\";\n/** Tool name for action invocation, matching Copilot's. */\nexport const INVOKE_CANVAS_ACTION_TOOL_NAME = \"invoke_canvas_action\";\n/**\n * Tool name for re-forking an edited extension. hoocode's own — Copilot has no\n * equivalent because its `/create-canvas` flow reloads the panel itself.\n */\nexport const RELOAD_CANVAS_TOOL_NAME = \"reload_canvas\";\n\n/**\n * Ceiling on a serialized action result, in characters.\n *\n * Whatever an action returns lands in the model's context window.\n * `pr-artifact-explorer` truncates its own payloads (`entries.slice(0, 200)`), but\n * nothing in the contract obliges a canvas to, so the host caps it too.\n */\nexport const CANVAS_RESULT_MAX_CHARS = 8_000;\n\nconst listParams = Type.Object({}, { additionalProperties: false });\n\nconst reloadParams = Type.Object(\n\t{\n\t\textensionId: Type.String({\n\t\t\tdescription: \"The extension whose code changed. From list_canvas_capabilities (the `extension` field).\",\n\t\t}),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype ReloadParams = Static<typeof reloadParams>;\n\nconst invokeParams = Type.Object(\n\t{\n\t\tinstanceId: Type.String({ description: \"From list_canvas_capabilities.\" }),\n\t\taction: Type.String({ description: \"Action name declared by that instance's canvas.\" }),\n\t\tinput: Type.Optional(Type.Unknown({ description: \"Action input, matching the action's declared schema.\" })),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype InvokeParams = Static<typeof invokeParams>;\n\n/** What `list_canvas_capabilities` reports. */\nexport interface CanvasCapabilitiesDetails {\n\tinstances: number;\n\tactions: number;\n}\n\n/** What `reload_canvas` reports. */\nexport interface CanvasReloadDetails {\n\textensionId: string;\n\treopened: number;\n\tdropped: number;\n\tactionsAdded: number;\n\tactionsRemoved: number;\n\tactionsChanged: number;\n}\n\n/** What `invoke_canvas_action` reports. */\nexport interface CanvasInvokeDetails {\n\tinstanceId: string;\n\taction: string;\n\ttruncated: boolean;\n}\n\nfunction textResult(text: string) {\n\treturn { content: [{ type: \"text\" as const, text }] };\n}\n\n/** Serialize an action result, capped so a chatty canvas cannot flood the context. */\nfunction renderResult(value: unknown): { text: string; truncated: boolean } {\n\tconst serialized = value === undefined ? \"null\" : JSON.stringify(value, null, 1);\n\tif (serialized.length <= CANVAS_RESULT_MAX_CHARS) return { text: serialized, truncated: false };\n\treturn {\n\t\ttext: `${serialized.slice(0, CANVAS_RESULT_MAX_CHARS)}\\n… truncated at ${CANVAS_RESULT_MAX_CHARS} characters.`,\n\t\ttruncated: true,\n\t};\n}\n\n/**\n * Discovery: every open instance, its canvas, and the actions it declares with\n * their input schemas.\n *\n * Takes no parameters. A filter would add schema bytes on every request to save\n * bytes in a response the model reads once.\n */\nfunction createListCapabilitiesTool(registry: CanvasRegistry): ToolDefinition {\n\treturn defineTool<typeof listParams, CanvasCapabilitiesDetails>({\n\t\tname: LIST_CANVAS_CAPABILITIES_TOOL_NAME,\n\t\tlabel: LIST_CANVAS_CAPABILITIES_TOOL_NAME,\n\t\tdescription:\n\t\t\t\"List the open canvases and the actions each one accepts, with their input schemas. Call this before invoke_canvas_action to learn the instanceId and the action's schema.\",\n\t\tpromptSnippet: \"Discover open canvases and the actions they accept\",\n\t\tparameters: listParams,\n\t\tasync execute() {\n\t\t\tconst instances = registry.listInstances();\n\t\t\tconst bindings = registry.activeActions();\n\t\t\tif (instances.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\t...textResult(\"No canvas is open. A person opens a canvas; you can then drive it.\"),\n\t\t\t\t\tdetails: { instances: 0, actions: 0 },\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst report = instances.map((instance) => ({\n\t\t\t\tinstanceId: instance.instanceId,\n\t\t\t\tcanvas: instance.canvasId,\n\t\t\t\textension: instance.extensionId,\n\t\t\t\ttitle: instance.title,\n\t\t\t\tstatus: instance.status,\n\t\t\t\tactions: bindings\n\t\t\t\t\t.filter((binding) => binding.instanceId === instance.instanceId)\n\t\t\t\t\t.map((binding) => binding.action),\n\t\t\t}));\n\t\t\treturn {\n\t\t\t\t...textResult(JSON.stringify(report, null, 1)),\n\t\t\t\tdetails: { instances: instances.length, actions: bindings.length },\n\t\t\t};\n\t\t},\n\t});\n}\n\n/** Invocation: run one declared action against one open instance. */\nfunction createInvokeActionTool(registry: CanvasRegistry): ToolDefinition {\n\treturn defineTool<typeof invokeParams, CanvasInvokeDetails>({\n\t\tname: INVOKE_CANVAS_ACTION_TOOL_NAME,\n\t\tlabel: INVOKE_CANVAS_ACTION_TOOL_NAME,\n\t\t// The description deliberately makes no safety claim. An earlier version said\n\t\t// actions \"cannot edit files or run commands\", which is false: a canvas\n\t\t// extension is arbitrary Node code running with the user's privileges, and\n\t\t// `pr-artifact-explorer` really does download artifacts to disk and call the\n\t\t// GitHub API. Those side effects never pass hoocode's permission gate, because\n\t\t// the gate sits in front of hoocode's own tools, not inside a forked\n\t\t// extension — the workspace-trust gate (canvas/trust.ts) is the control here,\n\t\t// not a sentence in a tool schema. Never tell the model a safety property the\n\t\t// runtime does not enforce.\n\t\tdescription:\n\t\t\t\"Invoke an action on an open canvas. Actions are implemented by the canvas extension itself: an action may change what the person is looking at and can have side effects of its own, so read the action's description before calling it.\",\n\t\tpromptSnippet: \"Act on an open canvas the user is looking at\",\n\t\tparameters: invokeParams,\n\t\tasync execute(_toolCallId, params: InvokeParams, signal) {\n\t\t\t// instanceId is a UUID and unique across every canvas, so the model does not\n\t\t\t// have to carry the extension and canvas ids too — the registry already knows\n\t\t\t// which instance a given id belongs to.\n\t\t\tconst instance = registry.listInstances().find((open) => open.instanceId === params.instanceId);\n\t\t\tif (!instance) {\n\t\t\t\t// Tools report failure by throwing here, as the built-ins do; the loop turns\n\t\t\t\t// a rejection into the model's tool result.\n\t\t\t\tconst open = registry.listInstances().map((other) => other.instanceId);\n\t\t\t\tthrow new Error(\n\t\t\t\t\topen.length === 0\n\t\t\t\t\t\t? \"No canvas is open, so there is nothing to act on.\"\n\t\t\t\t\t\t: `No open canvas instance \"${params.instanceId}\". Open instances: ${open.join(\", \")}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t// Honour the turn's abort signal: without this, aborting a turn leaves the\n\t\t\t\t// request running and its answer arriving for a turn nobody awaits.\n\t\t\t\tconst result = await registry.invokeAction(instance, params.action, params.input as never, { signal });\n\t\t\t\tconst { text, truncated } = renderResult(result);\n\t\t\t\treturn {\n\t\t\t\t\t...textResult(text),\n\t\t\t\t\tdetails: { instanceId: params.instanceId, action: params.action, truncated },\n\t\t\t\t};\n\t\t\t} catch (cause) {\n\t\t\t\t// Canvas handlers throw CanvasError with a machine-readable code, which the\n\t\t\t\t// runner preserves across the process boundary as CanvasCallError.code. Only\n\t\t\t\t// the message is rendered to the model, so fold the code into it rather than\n\t\t\t\t// letting the typed-error intent (§8) stop at the tool boundary.\n\t\t\t\tconst code = cause instanceof Error && \"code\" in cause ? String(cause.code) : undefined;\n\t\t\t\tconst message = cause instanceof Error ? cause.message : String(cause);\n\t\t\t\tthrow new Error(code ? `${code}: ${message}` : message);\n\t\t\t}\n\t\t},\n\t});\n}\n\n/**\n * Reload: re-fork an extension whose source changed, carrying its open instances.\n *\n * This is the tool that makes a canvas *iterable* by the agent — \"add a column\",\n * \"make the header sticky\" — which is the whole point of authoring one in a\n * session. Editing `extension.mjs` alone changes nothing: the child forked from\n * the old bytes keeps serving until it is replaced.\n *\n * It reloads; it does not open. That distinction is what keeps §11.5's reasoning\n * intact. Opening is a person's decision because it starts a process from a\n * directory nobody has vouched for; reloading only restarts an extension the\n * person already opened, in a workspace they already trusted, from a path the\n * host already resolved. The model cannot reach a new extension through it, and\n * a poisoned string in some canvas's data still cannot cause one to start.\n *\n * It is not a safety boundary on the *contents* of the file, and must not be\n * described as one: whatever wrote `extension.mjs` — the model's own edit,\n * through the permission gate — is what runs.\n */\nfunction createReloadTool(registry: CanvasRegistry): ToolDefinition {\n\treturn defineTool<typeof reloadParams, CanvasReloadDetails>({\n\t\tname: RELOAD_CANVAS_TOOL_NAME,\n\t\tlabel: RELOAD_CANVAS_TOOL_NAME,\n\t\tdescription:\n\t\t\t\"Restart an open canvas extension so your edits to its source take effect. Editing the extension's file does nothing on its own — the running process was forked from the old code. Call this after every edit. It reports which actions you added, removed or changed, so use it to confirm an action you just wrote is really callable. Open instances are carried across and keep their instanceId, but each gets a NEW url: tell the person the new url, because the tab they have open is now dead.\",\n\t\tpromptSnippet: \"Restart an edited canvas so the change is live\",\n\t\tparameters: reloadParams,\n\t\tasync execute(_toolCallId, params: ReloadParams, signal) {\n\t\t\tconst running = [...new Set(registry.listInstances().map((instance) => instance.extensionId))];\n\t\t\tif (!running.includes(params.extensionId)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\trunning.length === 0\n\t\t\t\t\t\t? \"No canvas is open, so there is nothing to reload.\"\n\t\t\t\t\t\t: `Canvas extension \"${params.extensionId}\" has nothing open. Running: ${running.join(\", \")}.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// A failed reload is the common case while iterating — the edit did not\n\t\t\t// parse, or threw at module scope. The registry leaves the old child\n\t\t\t// serving in that case, so this reads as \"your edit is broken and the\n\t\t\t// canvas is untouched\", which is what the model needs to hear to fix it.\n\t\t\tconst result = await registry.reload(params.extensionId, { signal });\n\n\t\t\tconst lines = [`Reloaded ${params.extensionId}. It declares: ${result.canvases.join(\", \") || \"no canvases\"}.`];\n\n\t\t\t// The capability delta is the answer to the question an author actually has\n\t\t\t// after an edit — did the host see the action I just wrote? Silence would read\n\t\t\t// as success, so \"nothing changed\" is said out loud too.\n\t\t\tconst { added, removed, changed, current } = result.actions;\n\t\t\tif (added.length + removed.length + changed.length === 0) {\n\t\t\t\tlines.push(`Actions unchanged: ${current.join(\", \") || \"none\"}.`);\n\t\t\t} else {\n\t\t\t\tif (added.length > 0) lines.push(`Actions added: ${added.join(\", \")}.`);\n\t\t\t\tif (removed.length > 0) lines.push(`Actions removed: ${removed.join(\", \")}.`);\n\t\t\t\tif (changed.length > 0) {\n\t\t\t\t\tlines.push(\n\t\t\t\t\t\t`Actions changed (description or inputSchema): ${changed.join(\", \")}. Any schema you were holding for these is stale.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tlines.push(`Now callable: ${current.join(\", \") || \"none\"}.`);\n\t\t\t}\n\t\t\tif (result.reopened.length > 0) {\n\t\t\t\tlines.push(\n\t\t\t\t\t\"Re-opened (give the person the new url — their old tab points at a closed port):\",\n\t\t\t\t\t...result.reopened.map(\n\t\t\t\t\t\t(instance) =>\n\t\t\t\t\t\t\t` ${instance.canvasId} (${instance.instanceId})${instance.url ? ` — ${instance.url}` : \"\"}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (result.dropped.length > 0) {\n\t\t\t\tlines.push(\n\t\t\t\t\t\"Did not come back:\",\n\t\t\t\t\t...result.dropped.map((drop) => ` ${drop.canvasId} (${drop.instanceId}): ${drop.reason}`),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (result.reopened.length === 0 && result.dropped.length === 0) {\n\t\t\t\tlines.push(\"Nothing was open, so nothing was re-opened.\");\n\t\t\t}\n\t\t\treturn {\n\t\t\t\t...textResult(lines.join(\"\\n\")),\n\t\t\t\tdetails: {\n\t\t\t\t\textensionId: params.extensionId,\n\t\t\t\t\treopened: result.reopened.length,\n\t\t\t\t\tdropped: result.dropped.length,\n\t\t\t\t\tactionsAdded: added.length,\n\t\t\t\t\tactionsRemoved: removed.length,\n\t\t\t\t\tactionsChanged: changed.length,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n}\n\n/**\n * The canvas tools, or none.\n *\n * Returns an empty array while nothing is open, so the two schemas are absent from\n * the prompt in the overwhelmingly common case of a repository with no canvases —\n * the same reason `registry.activeActions()` is empty until an instance exists\n * (§7). Callers re-derive this when the open set changes.\n */\nexport function createCanvasToolDefinitions(registry: CanvasRegistry): ToolDefinition[] {\n\tif (registry.listInstances().length === 0) return [];\n\treturn [createListCapabilitiesTool(registry), createInvokeActionTool(registry), createReloadTool(registry)];\n}\n"]}
@@ -10,11 +10,29 @@
10
10
  * reaches the abandon path (§11.6) and the extension is told to release the port it
11
11
  * may already have bound, rather than the spinner merely disappearing.
12
12
  *
13
- * The two agent tools register on the first successful open and stay for the session:
13
+ * `/new-canvas` is registered here rather than beside `/new-skill` and friends
14
+ * because it is not a file-writing command any more: it opens what it scaffolds
15
+ * and hands the agent a brief to build it, which needs this file's session and
16
+ * `pi.sendUserMessage`. Its decisions live in `core/canvas/scaffold.ts`.
17
+ *
18
+ * The agent tools register on the first successful open and stay for the session:
14
19
  * `registerTool` has no counterpart to remove a tool. So a session that never opens a
15
20
  * canvas pays nothing for them, which is the case that matters (§11.5); after the
16
21
  * first open they cost ~235 tokens and answer honestly when nothing is open.
17
22
  */
23
+ import { type CanvasSessionOptions } from "../../core/canvas/session.js";
18
24
  import type { ExtensionAPI } from "../../core/extensions/types.js";
19
- export declare function setupCanvas(pi: ExtensionAPI): void;
25
+ /**
26
+ * Test seams, and only that.
27
+ *
28
+ * `/new-canvas` opens what it writes, so driving it without a terminal needs a
29
+ * runtime that does not depend on hoocode having been built, and a home
30
+ * directory that is not the developer's. Everything else this file does is
31
+ * decided in `core/canvas/`, where it is testable without any of this.
32
+ */
33
+ export interface CanvasSetupOverrides {
34
+ homeDir?: string;
35
+ resolveRuntime?: CanvasSessionOptions["resolveRuntime"];
36
+ }
37
+ export declare function setupCanvas(pi: ExtensionAPI, overrides?: CanvasSetupOverrides): void;
20
38
  //# sourceMappingURL=canvas.d.ts.map